Merge pull request 'Feature/portal topbar' (#1) from feature/portal-topbar into main
Deploy auf den Timer-Server / deploy (push) Successful in 21s
Deploy auf den Timer-Server / deploy (push) Successful in 21s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
commit
e41e2b38e3
7 files changed
+119
-14
No files matched your search
@@ -265,6 +265,23 @@ Wichtig in beiden Fällen:
|
|||||||
* `/health` ist der Endpunkt für Health-Checks (antwortet **ohne** Anmeldung)
|
* `/health` ist der Endpunkt für Health-Checks (antwortet **ohne** Anmeldung)
|
||||||
* Websockets werden **nicht** gebraucht
|
* Websockets werden **nicht** gebraucht
|
||||||
|
|
||||||
|
### Portal-Kopfleiste
|
||||||
|
|
||||||
|
`frontend/index.html` lädt die zentrale Handy-Insel-Kopfleiste
|
||||||
|
(`https://portal.handy-insel.de/topbar.js`, `data-app="timer"`). Sie bringt
|
||||||
|
App-Umschalter und Benutzer-Menü mit; Benutzername und „Abmelden“ der App
|
||||||
|
stehen deshalb dort, die eigenen im Header erscheinen nur, wenn das Skript
|
||||||
|
nicht lädt.
|
||||||
|
|
||||||
|
* **Abmelden** in der Leiste ruft vorher `POST /api/logout` auf (Ereignis
|
||||||
|
`hi-abmelden`) und meldet danach bei Portal und Authentik ab.
|
||||||
|
* **Launch-URL** der Kachel im Portal: `https://timer.handy-insel.de/auth/login`
|
||||||
|
(startet direkt die Anmeldung über Authentik).
|
||||||
|
* **Content-Security-Policy:** Die App setzt keine. Kommt im Proxy eine dazu,
|
||||||
|
muss sie `script-src`/`connect-src https://portal.handy-insel.de` und
|
||||||
|
`img-src https://portal.handy-insel.de https://login.handy-insel.de`
|
||||||
|
erlauben (Details in den Proxy-Beispielen).
|
||||||
|
|
||||||
### Bondrucker
|
### Bondrucker
|
||||||
|
|
||||||
Vollständige Einrichtung: **`deploy/bondrucker/README.md`**. Hier nur das
|
Vollständige Einrichtung: **`deploy/bondrucker/README.md`**. Hier nur das
|
||||||
|
|||||||
@@ -153,5 +153,10 @@ der Bondrucker hängt am USB-Anschluss.
|
|||||||
|
|
||||||
## Abmelden
|
## Abmelden
|
||||||
|
|
||||||
**🚪 Abmelden** meldet nur dieses Gerät ab. Laufende Timer bleiben davon
|
Ganz oben steht die Handy-Insel-Leiste. Über den Avatar rechts öffnet sich das
|
||||||
unberührt und laufen weiter.
|
Benutzer-Menü, dort steht **Abmelden**. Das meldet dieses Gerät beim Timer, im
|
||||||
|
Portal und bei Authentik ab. Laufende Timer bleiben davon unberührt und laufen
|
||||||
|
weiter. Laufen noch welche, fragt die App vorher nach.
|
||||||
|
|
||||||
|
Ist das Portal einmal nicht erreichbar, fehlt die Leiste. Dann steht
|
||||||
|
**🚪 Abmelden** wie früher oben rechts in der App.
|
||||||
+7
-2
@@ -487,9 +487,14 @@ app.get('/auth/logout', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Passwort-Logout (Fallback, wird per fetch aufgerufen)
|
// Logout per fetch: beendet nur die lokale Sitzung. Aufgerufen von der
|
||||||
|
// Portal-Kopfleiste (Ereignis "hi-abmelden"), die danach selbst bei Portal
|
||||||
|
// und Authentik abmeldet.
|
||||||
app.post('/api/logout', (req, res) => {
|
app.post('/api/logout', (req, res) => {
|
||||||
req.session.destroy(() => res.json({ success: true }));
|
req.session.destroy(() => {
|
||||||
|
res.clearCookie('connect.sid');
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ timer.firma.example {
|
|||||||
X-Frame-Options "SAMEORIGIN"
|
X-Frame-Options "SAMEORIGIN"
|
||||||
X-Content-Type-Options "nosniff"
|
X-Content-Type-Options "nosniff"
|
||||||
Referrer-Policy "no-referrer"
|
Referrer-Policy "no-referrer"
|
||||||
|
# Die Seite laedt die Portal-Kopfleiste von portal.handy-insel.de.
|
||||||
|
# Wer hier eine Content-Security-Policy setzt, muss diese Quellen erlauben
|
||||||
|
# (sonst blockiert der Browser die Leiste), jeweils zusaetzlich zu den
|
||||||
|
# vorhandenen Quellen (die App nutzt Inline-Styles und onclick-Handler):
|
||||||
|
# script-src ... https://portal.handy-insel.de
|
||||||
|
# connect-src ... https://portal.handy-insel.de
|
||||||
|
# img-src ... https://portal.handy-insel.de https://login.handy-insel.de
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse_proxy 127.0.0.1:3003 {
|
reverse_proxy 127.0.0.1:3003 {
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ server {
|
|||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header Referrer-Policy "no-referrer" always;
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
# Die Seite laedt die Portal-Kopfleiste von portal.handy-insel.de.
|
||||||
|
# Wer hier eine Content-Security-Policy setzt, muss diese Quellen erlauben
|
||||||
|
# (sonst blockiert der Browser die Leiste), jeweils zusaetzlich zu den
|
||||||
|
# vorhandenen Quellen (die App nutzt Inline-Styles und onclick-Handler):
|
||||||
|
# script-src ... https://portal.handy-insel.de
|
||||||
|
# connect-src ... https://portal.handy-insel.de
|
||||||
|
# img-src ... https://portal.handy-insel.de https://login.handy-insel.de
|
||||||
|
|
||||||
# CSV-Export darf auch bei vielen Einträgen dauern
|
# CSV-Export darf auch bei vielen Einträgen dauern
|
||||||
client_max_body_size 1m;
|
client_max_body_size 1m;
|
||||||
|
|||||||
+50
-7
@@ -161,28 +161,71 @@ async function enterApp() {
|
|||||||
startLoops();
|
startLoops();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeigt den angemeldeten Authentik-Nutzer oben rechts im Header.
|
// Zeigt den angemeldeten Authentik-Nutzer: im Benutzer-Menue der
|
||||||
|
// Portal-Kopfleiste, ohne Leiste oben rechts im eigenen Header.
|
||||||
function renderCurrentUser() {
|
function renderCurrentUser() {
|
||||||
const el = document.getElementById('currentUser');
|
|
||||||
if (!el) return;
|
|
||||||
const u = loggedInUser;
|
const u = loggedInUser;
|
||||||
el.textContent = u ? (u.displayName || u.username || '') : '';
|
const name = u ? (u.displayName || u.username || '') : '';
|
||||||
|
const el = document.getElementById('currentUser');
|
||||||
|
el.textContent = name;
|
||||||
el.title = u && u.email ? u.email : 'Angemeldet';
|
el.title = u && u.email ? u.email : 'Angemeldet';
|
||||||
|
renderTopbarSession(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
function confirmLogout() {
|
||||||
const running = state.timers.filter(t => t.isRunning).length;
|
const running = state.timers.filter(t => t.isRunning).length;
|
||||||
if (running > 0 && !confirm(
|
return running === 0 || confirm(
|
||||||
`${running} Timer ${running === 1 ? 'läuft' : 'laufen'} noch.\n\n` +
|
`${running} Timer ${running === 1 ? 'läuft' : 'laufen'} noch.\n\n` +
|
||||||
`Sie laufen serverseitig weiter und sind nach dem nächsten Anmelden wieder da. Trotzdem abmelden?`
|
`Sie laufen serverseitig weiter und sind nach dem nächsten Anmelden wieder da. Trotzdem abmelden?`
|
||||||
)) return;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eigener Abmelden-Knopf, nur sichtbar ohne Portal-Kopfleiste
|
||||||
|
async function logout() {
|
||||||
|
if (!confirmLogout()) return;
|
||||||
stopLoops();
|
stopLoops();
|
||||||
state.timers = [];
|
state.timers = [];
|
||||||
// GET-Navigation: beendet lokale Sitzung und – bei Authentik-Login – auch die SSO-Sitzung
|
// GET-Navigation: beendet lokale Sitzung und – bei Authentik-Login – auch die SSO-Sitzung
|
||||||
window.location.href = '/auth/logout';
|
window.location.href = '/auth/logout';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Portal-Kopfleiste
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// topbar.js laeuft per defer erst nach diesem Skript, die Leiste steht also
|
||||||
|
// fruehestens ab DOMContentLoaded im DOM. Die Sitzungsanzeige wird einmal
|
||||||
|
// eingehaengt, sobald die Anmeldung steht, danach aendert sich nur ihr Text.
|
||||||
|
const topbarSession = document.createElement('div');
|
||||||
|
topbarSession.slot = 'benutzer';
|
||||||
|
topbarSession.className = 'topbar-session';
|
||||||
|
topbarSession.append(Object.assign(document.createElement('span'), { textContent: 'Timer – angemeldet als' }),
|
||||||
|
document.createElement('strong'));
|
||||||
|
let topbarSessionName = null; // null = (noch) nicht angemeldet
|
||||||
|
|
||||||
|
function renderTopbarSession(name) {
|
||||||
|
topbarSessionName = name || 'Angemeldet';
|
||||||
|
topbarSession.lastChild.textContent = topbarSessionName;
|
||||||
|
if (!topbarSession.isConnected) document.querySelector('hi-topbar')?.append(topbarSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (topbarSessionName !== null) renderTopbarSession(topbarSessionName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Abmelden" in der Leiste: erst die eigene Sitzung beenden, danach meldet
|
||||||
|
// die Leiste bei Portal und Authentik ab. Abbrechen bei laufenden Timern
|
||||||
|
// verhindert das ganze Abmelden.
|
||||||
|
document.addEventListener('hi-abmelden', (e) => {
|
||||||
|
if (!confirmLogout()) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopLoops();
|
||||||
|
state.timers = [];
|
||||||
|
e.detail.warten(fetch('/api/logout', { method: 'POST', credentials: 'same-origin' }));
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Timer-Synchronisation
|
// Timer-Synchronisation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+24
-3
@@ -1,10 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<!-- data-theme: die App ist immer dunkel, die Portal-Kopfleiste soll es auch sein -->
|
||||||
|
<html lang="de" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Timer - Zeiterfassung</title>
|
<title>Timer - Zeiterfassung</title>
|
||||||
<link rel="icon" type="image/jpeg" href="/favicon.jpg">
|
<link rel="icon" type="image/jpeg" href="/favicon.jpg">
|
||||||
|
<!-- Zentrale Handy-Insel-Kopfleiste (App-Umschalter + Benutzermenue).
|
||||||
|
Einzige externe Ressource der Seite; Details: portal.handy-insel.de -->
|
||||||
|
<script src="https://portal.handy-insel.de/topbar.js" data-app="timer" defer></script>
|
||||||
<style>
|
<style>
|
||||||
/* Bewusst KEIN Google-Fonts-Import: kein externer Abruf zur Laufzeit.
|
/* Bewusst KEIN Google-Fonts-Import: kein externer Abruf zur Laufzeit.
|
||||||
Der System-Font-Stack sieht praktisch identisch aus (Segoe UI /
|
Der System-Font-Stack sieht praktisch identisch aus (Segoe UI /
|
||||||
@@ -784,6 +788,23 @@
|
|||||||
.history-item-right { justify-content: center; }
|
.history-item-right { justify-content: center; }
|
||||||
.filter-field, .filter-field input { width: 100%; }
|
.filter-field, .filter-field input { width: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Portal-Kopfleiste ---------- */
|
||||||
|
|
||||||
|
/* Die Leiste steht im Fluss als erstes Kind von <body>; body hat nur
|
||||||
|
min-height: 100vh, dadurch entsteht kein Ueberlauf. Beim Drucken
|
||||||
|
soll sie nicht erscheinen. */
|
||||||
|
hi-topbar { flex-shrink: 0; }
|
||||||
|
@media print { hi-topbar { display: none !important; } }
|
||||||
|
|
||||||
|
/* Benutzername und Abmelden sitzen im Benutzer-Menue der Leiste. Die
|
||||||
|
eigenen bleiben nur als Rueckfall, wenn topbar.js nicht laedt. */
|
||||||
|
hi-topbar ~ .container #currentUser,
|
||||||
|
hi-topbar ~ .container #logoutBtn { display: none; }
|
||||||
|
|
||||||
|
/* Sitzungsanzeige im Benutzer-Menue (slot="benutzer"), Schriftfarbe kommt von der Leiste */
|
||||||
|
.topbar-session { padding: 10px 16px; font-size: 13px; line-height: 1.4; }
|
||||||
|
.topbar-session span { display: block; opacity: 0.7; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -795,7 +816,7 @@
|
|||||||
<button class="btn btn-secondary" onclick="showTimer()">⏱️ Timer</button>
|
<button class="btn btn-secondary" onclick="showTimer()">⏱️ Timer</button>
|
||||||
<button class="btn btn-secondary" onclick="showHistory()">📊 Historie</button>
|
<button class="btn btn-secondary" onclick="showHistory()">📊 Historie</button>
|
||||||
<button class="btn btn-secondary" onclick="showSettings()">⚙️ Einstellungen</button>
|
<button class="btn btn-secondary" onclick="showSettings()">⚙️ Einstellungen</button>
|
||||||
<button class="btn btn-secondary" onclick="logout()">🚪 Abmelden</button>
|
<button id="logoutBtn" class="btn btn-secondary" onclick="logout()">🚪 Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -1008,6 +1029,6 @@
|
|||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script src="/app.js?v=20260912-devices"></script>
|
<script src="/app.js?v=20261025-topbar"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user