Website Build
- Web -Waiting for action
- -diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml old mode 100644 new mode 100755 index 15cc453..53a475b --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -26,6 +26,9 @@ jobs: - name: Install dependencies run: npm install + - name: Validate manifest v2 + run: node ./scripts/validate-manifest.mjs + - name: Run gated analysis if: always() env: diff --git a/README.md b/README.md new file mode 100755 index 0000000..5ce8dda --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Zone dashboard + +Home-server control UI served at https://zone.zainezq.com. Configuration is data-driven via [`data/manifest.json`](data/manifest.json). + +## Local development + +```bash +cd /home/zaine/master-folder/org-platform/zone +python3 -m http.server 8080 +# open http://127.0.0.1:8080 — API calls need org_backend on :9010 or will show offline +``` + +## Tests and CI + +```bash +npm test +``` + +Gitea Actions runs `scripts/gated-analysis.ps1` (manifest schema, smell checks, tests). + +## Manifest + +- **links** — quick-nav cards (`platform` then `infra` groups) +- **builds** — run/cancel/status/logs bindings to `org_backend` BuildController +- **status** — health, uptime, last-runs endpoints + +Production loads enriched manifest from `GET /api/zone/manifest` (injections: `integrations`, `widgets`). Live widgets poll `GET /api/zone/status` (authoring queue, watcher, timesheet week summary). + +Timesheet SPA is served from [`../time-tracking/`](../time-tracking/) at `/time-tracking/` (nginx alias). diff --git a/css/dashboard.css b/css/dashboard.css new file mode 100755 index 0000000..fcc73f3 --- /dev/null +++ b/css/dashboard.css @@ -0,0 +1,725 @@ +.page { + width: min(1180px, calc(100% - 32px)); + margin: 0 auto; + padding: 1.25rem 0 2rem; +} + +.topbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + align-items: end; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.25rem; + box-shadow: var(--shadow); +} + +.topbar h1 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; +} + +.header-meta { + min-width: 200px; + text-align: right; + color: var(--text-muted); + font-size: 0.85rem; + line-height: 1.6; +} + +.clock-display { + color: var(--text); + font-size: 1.5rem; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +#globalStatus { + margin: 1rem 0; + display: flex; + align-items: center; + gap: 0.625rem; + border: 1px solid; + border-radius: var(--radius); + padding: 0.75rem 1rem; + font-size: 0.875rem; + font-weight: 600; +} + +.global-ok { + color: var(--positive); + background: var(--positive-bg); + border-color: #86efac; +} + +.global-error { + color: var(--negative); + background: var(--negative-bg); + border-color: #fca5a5; +} + +.status-pulse { + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + flex-shrink: 0; +} + +.dashboard-grid { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 1rem; + align-items: start; +} + +.rail { + position: sticky; + top: 1rem; + display: grid; + gap: 1rem; +} + +.main-stack { + display: grid; + gap: 1rem; +} + +.panel { + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: var(--shadow); + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: #f8fafc; + border-bottom: 1px solid var(--border); +} + +.panel-title { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: var(--text); +} + +.panel-body { + padding: 1rem; +} + +.status-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.625rem; +} + +.status-grid .stat-card { + min-height: 72px; + display: grid; + align-content: space-between; + gap: 0.5rem; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.75rem; + background: var(--bg); +} + +.stat-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.status-grid .stat-value { + font-size: 0.95rem; + font-weight: 600; + color: var(--text); +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex: 0 0 auto; +} + +.dot.green { + background: var(--positive); +} + +.dot.red { + background: var(--negative); +} + +.dot.yellow { + background: var(--amber); + animation: blink 0.8s step-end infinite; +} + +@keyframes blink { + 50% { + opacity: 0.4; + } +} + +.badge { + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.2rem 0.5rem; + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.badge.ok { + color: var(--positive); + border-color: #86efac; + background: var(--positive-bg); +} + +.badge.error { + color: var(--negative); + border-color: #fca5a5; + background: var(--negative-bg); +} + +.badge.neutral { + color: var(--text-muted); +} + +.badge.warn { + color: var(--amber); + border-color: #fcd34d; + background: #fef3c7; +} + +.quick-links { + display: grid; + gap: 0.5rem; +} + +.link-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.625rem; + min-height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; +} + +.link-card:hover, +.link-card:focus-visible { + outline: none; + border-color: var(--accent); + color: var(--accent); + background: #eff6ff; +} + +.utility-grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(240px, 0.8fr); + gap: 1rem; +} + +.daily-message { + color: var(--text); + font-size: 1rem; + line-height: 1.6; + border-left: 3px solid var(--accent); + padding-left: 0.75rem; +} + +.countdown-display { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.5rem; +} + +.time-unit { + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.625rem; + background: var(--bg); + text-align: center; +} + +.time-number { + display: block; + color: var(--accent); + font-size: clamp(1.25rem, 3vw, 1.75rem); + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1; +} + +.time-label { + margin-top: 0.25rem; + color: var(--text-muted); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.uptime-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.625rem; +} + +.uptime-item { + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.625rem; + background: var(--bg); +} + +.uptime-item-label { + color: var(--text-muted); + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.uptime-item-value { + margin-top: 0.35rem; + color: var(--text); + font-size: 0.95rem; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.uptime-bar-wrap { + margin-top: 1rem; +} + +.uptime-label { + display: flex; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.5rem; + color: var(--text-muted); + font-size: 0.75rem; +} + +.uptime-bar { + height: 8px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg); + overflow: hidden; +} + +.uptime-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, var(--positive), var(--accent)); + transition: width 1s ease; +} + +.ping-history { + display: flex; + align-items: end; + gap: 4px; + min-height: 36px; + margin-top: 0.625rem; + overflow: hidden; +} + +.ping-bar { + width: 8px; + flex: 0 0 8px; + border-radius: 2px 2px 0 0; + opacity: 0.85; +} + +.run-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.run-card .panel-body { + display: grid; + gap: 0.75rem; +} + +.run-summary { + display: grid; + gap: 0.5rem; + padding: 0.75rem; + border: 1px dashed var(--border); + border-radius: var(--radius); + background: var(--bg); +} + +.status-text { + min-height: 1.3em; + color: var(--text-muted); + font-size: 0.875rem; + line-height: 1.5; +} + +.status-text.running { + color: var(--amber); +} + +.status-text.success { + color: var(--positive); +} + +.status-text.failed { + color: var(--negative); +} + +.last-run-meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 0.875rem; + color: var(--text-muted); + font-size: 0.75rem; +} + +.last-run-meta span { + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.btn-row { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.btn-danger { + color: var(--negative); + border-color: #fca5a5; + background: var(--surface); +} + +.btn-danger:hover:not(:disabled) { + background: var(--negative-bg); +} + +.btn-combined { + color: var(--amber); + border-color: #fcd34d; + background: #fffbeb; +} + +.btn-combined:hover:not(:disabled) { + background: #fef3c7; +} + +.kill-btn { + display: none; +} + +.kill-btn.visible { + display: inline-flex; +} + +.log-tools { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.625rem; +} + +.log-label { + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 500; +} + +.log-box { + height: 260px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.75rem; + background: #f8fafc; + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.8rem; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-word; +} + +.log-line { + display: block; +} + +.ansi-bold { + font-weight: 700; +} + +.ansi-dim { + opacity: 0.6; +} + +.ansi-red { + color: #dc2626; +} + +.ansi-green { + color: var(--positive); +} + +.ansi-yellow { + color: var(--amber); +} + +.ansi-blue { + color: var(--accent); +} + +.ansi-magenta { + color: #9333ea; +} + +.ansi-cyan { + color: #0891b2; +} + +.ansi-white { + color: var(--text); +} + +.ansi-reset { + color: inherit; + font-weight: inherit; + opacity: inherit; +} + +.pipeline-steps { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.375rem; +} + +.pipeline-step { + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.25rem 0.625rem; + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 600; +} + +.pipeline-step.active { + color: var(--amber); + border-color: #fcd34d; + background: #fef3c7; +} + +.pipeline-step.done { + color: var(--positive); + border-color: #86efac; + background: var(--positive-bg); +} + +.pipeline-step.failed { + color: var(--negative); + border-color: #fca5a5; + background: var(--negative-bg); +} + +.pipeline-arrow { + color: var(--text-muted); + font-size: 0.75rem; +} + +#toast-container { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 20; + display: grid; + gap: 0.5rem; +} + +.toast { + max-width: 320px; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.625rem 0.75rem; + background: var(--surface); + color: var(--text); + font-size: 0.875rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} + +.toast.success { + border-color: #86efac; + color: var(--positive); + background: var(--positive-bg); +} + +.toast.error { + border-color: #fca5a5; + color: var(--negative); + background: var(--negative-bg); +} + +.toast.info { + border-color: #93c5fd; + color: var(--accent); + background: #eff6ff; +} + +.toast.warn { + border-color: #fcd34d; + color: var(--amber); + background: #fef3c7; +} + +.modal-overlay { + position: fixed; + inset: 0; + z-index: 15; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(0, 0, 0, 0.4); +} + +.modal-overlay .modal { + width: min(380px, 100%); + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.modal-overlay .modal h3 { + margin: 0; + padding: 1rem; + border-bottom: 1px solid var(--border); + font-size: 1rem; + font-weight: 600; +} + +.modal-overlay .modal p { + padding: 1rem; + color: var(--text-muted); + font-size: 0.875rem; + line-height: 1.6; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + padding: 0 1rem 1rem; +} + +@media (max-width: 940px) { + .dashboard-grid, + .utility-grid, + .run-grid { + grid-template-columns: 1fr; + } + + .rail { + position: static; + } + + .quick-links { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .page { + width: min(100% - 20px, 1180px); + padding-top: 0.625rem; + } + + .topbar { + grid-template-columns: 1fr; + align-items: start; + } + + .header-meta { + min-width: 0; + text-align: left; + } + + .status-grid, + .quick-links, + .uptime-grid, + .countdown-display { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .btn-row .btn { + width: 100%; + } + + .log-tools { + align-items: stretch; + flex-direction: column; + } +} + +.widget-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 0.75rem; +} + +.widget-card { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.75rem 1rem; + background: var(--surface-2); +} + +.widget-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.widget-title { + margin: 0; + font-size: 0.95rem; + font-weight: 600; +} + +.widget-body { + font-size: 0.85rem; + color: var(--text-muted); + line-height: 1.45; +} diff --git a/css/shared.css b/css/shared.css new file mode 100755 index 0000000..99c8b3f --- /dev/null +++ b/css/shared.css @@ -0,0 +1,115 @@ +:root { + --bg: #f4f5f7; + --surface: #ffffff; + --border: #d8dce3; + --text: #1a1d23; + --text-muted: #5c6573; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --positive: #15803d; + --positive-bg: #dcfce7; + --negative: #b91c1c; + --negative-bg: #fee2e2; + --zero: #6b7280; + --weekend-bg: #f8fafc; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + --radius: 8px; + --radius-sm: 6px; + --surface-2: #f8fafc; + --font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + + /* Aliases for dashboard JS inline styles */ + --green: var(--positive); + --red: var(--negative); + --amber: #d97706; + --blue: var(--accent); +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: var(--font); + font-size: 15px; + line-height: 1.5; + color: var(--text); + background: var(--bg); + min-height: 100vh; +} + +a { + color: inherit; +} + +button { + font: inherit; +} + +.tagline { + margin: 0.25rem 0 0; + color: var(--text-muted); + font-size: 0.9rem; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font: inherit; + font-size: 0.875rem; + padding: 0.45rem 0.9rem; + border-radius: var(--radius); + border: 1px solid var(--border); + cursor: pointer; + background: var(--surface); + color: var(--text); + text-decoration: none; +} + +.btn:hover:not(:disabled) { + background: var(--bg); +} + +.btn:focus-visible:not(:disabled) { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-hover); +} + +.btn-secondary { + background: var(--surface); +} + +.btn-ghost { + border-color: transparent; + color: var(--text-muted); +} + +.hidden { + display: none !important; +} diff --git a/data/manifest.json b/data/manifest.json new file mode 100755 index 0000000..faa9b0b --- /dev/null +++ b/data/manifest.json @@ -0,0 +1,112 @@ +{ + "version": 2, + "dashboard": { + "title": "Home Server Control", + "tagline": "Local console and build deck" + }, + "integrations": { + "authoring": { "pollMs": 5000, "path": "/api/zone/status" }, + "watcher": { "pollMs": 10000, "path": "/api/zone/status" }, + "timesheet": { "pollMs": 60000, "path": "/api/zone/status" }, + "scripts": { "pollMs": 120000, "path": "/api/zone/status" }, + "builds": { "pollMs": 15000, "path": "/api/zone/status" } + }, + "widgets": [ + { + "id": "authoring-queue", + "type": "queue", + "title": "Authoring builds", + "integration": "authoring" + }, + { + "id": "authoring-detail", + "type": "authoringDetail", + "title": "Authoring detail", + "integration": "authoring" + }, + { + "id": "watcher", + "type": "badge", + "title": "Lima watcher", + "integration": "watcher" + }, + { + "id": "timesheet-summary", + "type": "timesheet", + "title": "Hours this week", + "integration": "timesheet" + }, + { + "id": "scripts-health", + "type": "log", + "title": "Script logs", + "integration": "scripts", + "logKey": "orgWebBuild" + }, + { + "id": "calibre-sync", + "type": "log", + "title": "Calibre export", + "integration": "scripts", + "logKey": "calibreSync" + }, + { + "id": "org-web-build", + "type": "buildStatus", + "title": "Website build", + "integration": "builds", + "lastRunKey": "org_web" + } + ], + "links": [ + { "id": "timesheet", "label": "Timesheet", "href": "/time-tracking/", "group": "platform" }, + { "id": "website", "label": "Website", "href": "https://zainezq.com", "group": "platform", "external": true }, + { "id": "author", "label": "Author", "href": "https://author.zainezq.com", "group": "platform", "external": true }, + { "id": "files", "label": "Files", "href": "https://filebrowser.zainezq.com", "group": "infra", "external": true }, + { "id": "library", "label": "Library", "href": "https://calibre.zainezq.com", "group": "infra", "external": true }, + { "id": "news", "label": "News", "href": "https://miniflux.zainezq.com", "group": "infra", "external": true }, + { "id": "rdp", "label": "RDP", "href": "https://guac.zainezq.com", "group": "infra", "external": true }, + { "id": "portainer", "label": "Portainer", "href": "https://portainer.zainezq.com", "group": "infra", "external": true }, + { "id": "nextcloud", "label": "Nextcloud", "href": "https://nextcloud.zainezq.com", "group": "infra", "external": true }, + { "id": "pgadmin", "label": "Databases", "href": "https://pgadmin.zainezq.com", "group": "infra", "external": true }, + { "id": "gitea", "label": "Gitea", "href": "https://gitea.zainezq.com", "group": "infra", "external": true } + ], + "builds": [ + { + "id": "org_web", + "label": "Website Build", + "badge": "Web", + "confirmTitle": "Update Website", + "confirmBody": "This will rebuild and deploy the website. Continue?", + "runLabel": "Run website build", + "start": { "method": "POST", "path": "/api/build-web" }, + "cancel": { "method": "DELETE", "path": "/api/build-web" }, + "status": { "path": "/api/build-web/status" }, + "logs": { "path": "/api/build-web/logs" }, + "statusDotId": "webDot", + "statusTextId": "webStatus", + "lastRunKey": "org_web" + }, + { + "id": "emacs", + "label": "Emacs", + "badge": "Emacs", + "confirmTitle": "Rerun Emacs", + "confirmBody": "This will rerun Emacs. Continue?", + "runLabel": "Rerun Emacs", + "start": { "method": "POST", "path": "/api/rerun-emacs" }, + "cancel": { "method": "DELETE", "path": "/api/rerun-emacs" }, + "status": { "path": "/api/rerun-emacs/status" }, + "logs": { "path": "/api/rerun-emacs/logs" }, + "statusDotId": "emacsDot", + "statusTextId": "emacsStatus", + "lastRunKey": "emacs", + "successExitCodes": [0, 124] + } + ], + "status": { + "health": "/api/health", + "uptime": "/api/uptime", + "lastRuns": "/api/last-runs" + } +} diff --git a/index.html b/index.html old mode 100644 new mode 100755 index 6c90ffd..b464d1d --- a/index.html +++ b/index.html @@ -4,680 +4,8 @@
Local console and build deck
Waiting for action
- -Waiting for action
- -