diff --git a/opax-web/README-opax-web.md b/opax-web/README-opax-web.md new file mode 100644 index 0000000..65dde98 --- /dev/null +++ b/opax-web/README-opax-web.md @@ -0,0 +1,55 @@ +# OPAX / Web TUI Intern Dokumentasjon + +Denne filen beskriver hvordan man kjører og overvåker backend-tjenesten for OPAX / Web TUI. + +## Kjøre i produksjonsmodus + +Applikasjonen er designet for å kjøre i en Node.js-container. For å starte serveren i produksjonsmodus, sørg for at følgende er satt: + +1. **Miljøvariabel:** `NODE_ENV` må være satt til `production`. +2. **Secrets:** Følgende miljøvariabler må være tilgjengelige i kjøremiljøet, f.eks. via Secret Manager: + * `GOOGLE_CLIENT_ID` + * `GOOGLE_CLIENT_SECRET` + * `SESSION_SECRET` +3. **Konfigurasjon:** + * `FRONTEND_URL`: Den fulle URL-en til frontend-applikasjonen (f.eks. `https://opax.vauco.no`). + * `ALLOWED_EMAILS`: En komma-separert liste med e-postadresser som har lov til å logge inn. + * `PORT`: Porten serveren skal lytte på (default er `8080`). + +Serveren startes ved å kjøre hovedfilen: + +```bash +node opax-web/backend/server.js +``` + +## Health Check + +Tjenesten har et health check-endepunkt for ekstern overvåking. + +- **URL:** `/health` +- **Metode:** `GET` +- **Suksessrespons (HTTP 200):** + ```json + { + "status": "ok" + } + ``` + +## API-endepunkter (Socket.IO) + +Klienten kommuniserer med serveren via Socket.IO for å hente Git-informasjon. All kommunikasjon krever en aktiv, autentisert sesjon. + +1. **Hent Git Status** + - **Klient-event:** `git:getStatus` + - **Server-respons (suksess):** `git:status:result` med payload `{ data: { status: string, details: string } }` + - **Server-respons (feil):** `git:status:error` med payload `{ message: string }` + +2. **Hent Aktiv Branch** + - **Klient-event:** `git:getBranch` + - **Server-respons (suksess):** `git:branch:result` med payload `{ data: { current_branch: string } }` + - **Server-respons (feil):** `git:branch:error` med payload `{ message: string }` + +## Driftstips + +- **Overvåking:** Health-check-endepunktet `/health` kan brukes av en ekstern monitortjeneste (f.eks. Google Cloud Monitoring Uptime Check). Konfigurer monitoren til å sende en GET-forespørsel hvert minutt og varsle ved ikke-200-status over en periode på for eksempel 3-5 minutter. +- **Logging:** All applikasjonslogging (inkludert feil) skrives til `stdout`/`stderr`. I et container-miljø som Cloud Run, blir disse loggene automatisk samlet inn og kan sees i Google Cloud Logging. Bruk filter i Logging for å isolere logger fra denne spesifikke Cloud Run-tjenesten. diff --git a/opax-web/backend/server.js b/opax-web/backend/server.js index 7a89287..8248dce 100644 --- a/opax-web/backend/server.js +++ b/opax-web/backend/server.js @@ -50,15 +50,15 @@ app.get('/health', (_, r) => r.json({ status: 'ok' })); app.get('/auth/google', (q, r) => r.redirect(oauth.generateAuthUrl({ scope: ['openid', 'email', 'profile'], prompt: 'consent' }))); -app.get('/auth/callback', async (q, r) => { +app.get('/auth/callback', async (req, res, next) => { try { - let { tokens } = await oauth.getToken(q.query.code); + let { tokens } = await oauth.getToken(req.query.code); let p = (await oauth.verifyIdToken({ idToken: tokens.id_token, audience: process.env.GOOGLE_CLIENT_ID })).getPayload(); - if (!p?.email || !emails.has(p.email)) return r.status(403).send('Not authorised'); - q.session.user = { email: p.email, name: p.name || p.email }; - r.redirect('/'); + if (!p?.email || !emails.has(p.email)) return res.status(403).send('Not authorised'); + req.session.user = { email: p.email, name: p.name || p.email }; + res.redirect('/'); } catch (e) { - r.status(500).send('Login failed'); + next(e); } }); @@ -95,4 +95,17 @@ app.get('*' , (req, res) => { } }); +// Central Error Handler +app.use((err, req, res, next) => { + // TODO: Integrate with an external error tracking service like Sentry + console.error(`[Express Error] Path: ${req.path}, Error:`, err); + + // Avoid leaking stack trace to client + if (res.headersSent) { + return next(err); + } + + res.status(500).json({ error: 'Internal Server Error' }); +}); + http.listen(Number(process.env.PORT || 8080), '0.0.0.0'); diff --git a/opax-web/backend/socketHandlers.js b/opax-web/backend/socketHandlers.js index 26c84ad..b8a5d2a 100644 --- a/opax-web/backend/socketHandlers.js +++ b/opax-web/backend/socketHandlers.js @@ -92,6 +92,7 @@ export function registerBrowserSocketHandlers(socket, { callAgentFn }) { const normalizedData = config.normalize(result); socket.emit(`${config.responseEvent}:result`, { data: normalizedData }); } catch (e) { + console.error(`[Socket Handler Error] Event: ${eventName}, User: ${socket.request.session.user.email}, Error:`, e); // e.message logges ikke eller sendes til klienten. socket.emit(`${config.responseEvent}:error`, { message: config.errorMessage }); } diff --git a/opax-web/frontend/src/App.tsx b/opax-web/frontend/src/App.tsx index d0e3423..ac1260c 100644 --- a/opax-web/frontend/src/App.tsx +++ b/opax-web/frontend/src/App.tsx @@ -124,6 +124,8 @@ export default function App() { } setStatusError(null); setStatusData(null); + setBranchData(null); // Clear other result for clarity + setBranchError(null); setStatusLoading(true); socketRef.current.emit('git:getStatus'); }; @@ -137,6 +139,8 @@ export default function App() { } setBranchError(null); setBranchData(null); + setStatusData(null); // Clear other result for clarity + setStatusError(null); setBranchLoading(true); socketRef.current.emit('git:getBranch'); }; @@ -156,33 +160,41 @@ export default function App() { OPAX / Web TUI {user.email} -
-
-

Git Status

- - {statusError &&

{statusError}

} - {statusData && ( -
-              Status: {statusData.status}\n\n
-              Detaljer:\n{statusData.details || '(ingen endringer)'}
-            
- )} +
+
+

Git Web TUI

+

Et enkelt grensesnitt for å se Git-status og aktiv branch for repoet.

-
-

Aktiv Branch

- - {branchError &&

{branchError}

} - {branchData && ( -

- Nåværende branch: {branchData.current_branch} -

- )} -
-
+
+
+

Git Status

+ + {statusError &&

{statusError}

} + {statusData && ( +
+ Status: {statusData.status} + + Detaljer: + {statusData.details || '(ingen endringer)'} +
+ )} +
+
+

Aktiv Branch

+ + {branchError &&

{branchError}

} + {branchData && ( +
+ Nåværende branch: {branchData.current_branch} +
+ )} +
+
+ ); } diff --git a/opax-web/frontend/src/style.css b/opax-web/frontend/src/style.css index 8892bec..c8e88b2 100644 --- a/opax-web/frontend/src/style.css +++ b/opax-web/frontend/src/style.css @@ -1 +1,165 @@ -*{box-sizing:border-box}body{margin:0;background:#0d1117;color:#e6edf3;font:14px monospace}main{height:100vh;display:flex;flex-direction:column}header{padding:16px;background:#161b22;border-bottom:1px solid #30363d}header span{float:right;color:#8b949e}section{display:flex;flex:1;min-height:0}aside{width:260px;padding:12px;background:#161b22;border-right:1px solid #30363d}button{display:block;width:100%;margin:5px 0;background:#21262d;color:#e6edf3;border:1px solid #30363d;padding:7px;text-align:left}.term{padding:18px;display:flex;flex:1;flex-direction:column}.term pre{white-space:pre-wrap;overflow:auto;flex:1}.term form{border-top:1px solid #30363d;padding-top:10px;color:#58a6ff}.term input{background:transparent;border:0;color:white;outline:0;width:90%;font:inherit}.login{display:grid;place-content:center;gap:20px;text-align:center}.login a{color:#58a6ff} \ No newline at end of file +:root { + --font-family: monospace; + --background-color: #0d1117; + --text-color: #e6edf3; + --text-color-secondary: #8b949e; + --border-color: #30363d; + --container-background: #161b22; + --button-background: #21262d; + --button-background-hover: #30363d; + --button-primary-background: #238636; + --button-primary-background-hover: #2ea043; + --error-color: #f85149; + --spacing-unit: 16px; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + background-color: var(--background-color); + color: var(--text-color); + font-family: var(--font-family); + font-size: 14px; +} + +main { + height: 100vh; + display: flex; + flex-direction: column; +} + +header { + padding: var(--spacing-unit); + background-color: var(--container-background); + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 16px; +} + +header span { + color: var(--text-color-secondary); +} + +.content-wrapper { + padding: calc(var(--spacing-unit) * 2); + max-width: 800px; + margin: 0 auto; + width: 100%; +} + +.page-header { + margin-bottom: calc(var(--spacing-unit) * 2); + text-align: center; +} + +.page-header h1 { + font-size: 24px; + margin-bottom: calc(var(--spacing-unit) / 2); +} + +.page-header p { + font-size: 16px; + color: var(--text-color-secondary); + max-width: 600px; + margin: 0 auto; +} + +.actions-container { + display: grid; + grid-template-columns: 1fr; + gap: calc(var(--spacing-unit) * 2); +} + +@media (min-width: 768px) { + .actions-container { + grid-template-columns: 1fr 1fr; + } +} + +.action-card { + background-color: var(--container-background); + border: 1px solid var(--border-color); + padding: var(--spacing-unit); + border-radius: 6px; +} + +.action-card h2 { + margin-top: 0; + font-size: 18px; + border-bottom: 1px solid var(--border-color); + padding-bottom: var(--spacing-unit); + margin-bottom: var(--spacing-unit); +} + +button { + width: 100%; + background: var(--button-background); + color: var(--text-color); + border: 1px solid var(--border-color); + padding: 10px var(--spacing-unit); + border-radius: 6px; + font-family: inherit; + font-size: 14px; + cursor: pointer; + transition: background-color 0.2s; +} + +button:hover:not(:disabled) { + background-color: var(--button-background-hover); +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +button.primary { + background-color: var(--button-primary-background); + border-color: var(--button-primary-background-hover); +} + +button.primary:hover:not(:disabled) { + background-color: var(--button-primary-background-hover); +} + +.result-display { + margin-top: var(--spacing-unit); + padding: var(--spacing-unit); + background-color: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 6px; + white-space: pre-wrap; + word-wrap: break-word; + font-size: 13px; + min-height: 100px; +} + +.result-display strong { + color: var(--text-color-secondary); +} + +.error { + color: var(--error-color); + margin-top: calc(var(--spacing-unit) / 2); + font-size: 13px; +} + +.login { + height: 100vh; + display: grid; + place-content: center; + gap: 20px; + text-align: center; +} + +.login a { + color: #58a6ff; + font-size: 18px; +} \ No newline at end of file