diff --git a/.gitignore b/.gitignore index 605b2a3..34e20d4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ public/uploads/ # Session scratch .tmp/ + +# Local secrets and editor state +.env* +.vscode/ +.idea/ diff --git a/DESIGN.md b/DESIGN.md index a140625..fa0c837 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -8,9 +8,14 @@ presentation. Pages are `.astro` templates (plain HTML). ## Brand (official: CTAO Brand Guidelines, April 2024 — PDF in the project docs) - Colors: Galaxy Blue `#00004A`, Cherenkov Blue `#00E4D8`, Moon Gray - `#F5F5F5`, Cosmic Azure `#007AFF` (links/focus), Interstellar Indigo - `#00009C`. Cherenkov is an ACCENT (brand book rations it — currently: - primary CTA, hero subtitle, logo flash); it fails AA as text on white. + `#F5F5F5`, Cosmic Azure `#007AFF` (focus rings only; links use the + darkened `--link` so they pass AA on white), Interstellar Indigo `#00009C`. + Cherenkov is an ACCENT (brand book rations it — currently: primary CTA, + hero subtitle, logo flash); it fails AA as text on white. +- Comments in the code cite `SPEC §…`, `BRAND D.x` and `REQUIREMENTS`: those + are the CTAO specification, the brand guidelines PDF and the team's + consolidated requirements, kept in the project's document folder at + Cyfronet, not in this repo. - Type: **Inter** for everything, **Space Grotesk** (weight 500) only for communicative headlines (h1/h2). Sizes come from the `--fs-*` scale. - Logo files in `public/brand/`; always "CTAO", min 100px wide, no effects. diff --git a/README.md b/README.md index 0716403..3bb596c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ gitignored here. ## Develop +Prerequisites: Node 22.19 or newer (the machine builds on Node 24), and +network access to the demo machine's Gitea (Cyfronet network / VPN) for the +content clone below. Without content the site still builds, but with no +articles and `npm run check` reports the footer's page links as broken. + ``` npm ci git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ../ctao-content diff --git a/astro.config.mjs b/astro.config.mjs index 34ffb95..1116727 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -19,7 +19,6 @@ export default defineConfig({ // cost of 'viewport' on a 24-card grid; auto-falls back to 'tap' on // data-saver / slow connections (Astro handles that). prefetch: { prefetchAll: true }, - server: { port: 4321, host: true }, vite: { build: { // Lightning CSS (the default minifier) illegally folds animation-timeline @@ -28,10 +27,5 @@ export default defineConfig({ // esbuild minifies without property merging. cssMinify: 'esbuild', }, - server: { - // Vite blocks unknown Host headers by default; the demo is viewed through an - // ephemeral cloudflared quick tunnel, so allow any *.trycloudflare.com host. - allowedHosts: ['.trycloudflare.com'], - }, }, }); diff --git a/deploy/DEPLOY-LOG.md b/deploy/DEPLOY-LOG.md deleted file mode 100644 index d364947..0000000 --- a/deploy/DEPLOY-LOG.md +++ /dev/null @@ -1,199 +0,0 @@ -# Deployment log — CTAO portal demo on strapi-experimental.cyfronet - -Living document: what was done, what broke, what is left. Update it as work -proceeds so any agent (or human) can resume without the chat history. -Started 2026-07-28. - -## Goal - -Run the git-based CMS demo (Gitea + Sveltia + Astro static build) on the -Cyfronet test machine so the SUSS-PORT team can click through it. Shape must -be production-viable, not throwaway. Machine footprint: one directory -(`~/ctao-portal-demo/`) + 4 unit files, fully removable (README "Uninstall"). - -## Ground rules (from the user, non-negotiable) - -- **Write every command out in chat, marked read-only / write, BEFORE running - it** — the user reviews on a phone and cannot see truncated tool calls. -- Non-invasive verification first, debugging second: confirm each precondition - instead of fixing after the fact. -- Never read secrets (`~/.ssh/*`, tokens). Mask token URLs in any output - (`sed 's|://[^@]*@|://***@|g'`). Secrets stay in 0600 files on the machine. -- No sudo, nothing outside `$HOME` on the machine, no traces for the admin. -- Temp/working files under `.tmp/-/` in the repo, never `/tmp`. -- Commit messages: **no "Co-Authored-By: Claude"**, no session links. - -## Architecture (decided, see also README.md in this dir) - -``` -editor → Sveltia (/admin, static) → commit → Gitea :3000 (container, SQLite) - │ polled every 10 s - build.sh (systemd user timer) - └─ podman run node+git, --network=container:ctao-demo-gitea - npm ci (only if lockfile changed) → astro build - → releases// → atomic symlink flip - │ - nginx :8080 serves releases/current - │ (TODO) ingress vhost → team access -``` - -## Machine facts (verified 2026-07-28, read-only) - -- Rocky Linux 9.7, host `outline.openstacklocal`, IP 192.168.10.15, 2 vCPU, - 3.6 GB RAM, no swap. User `strapi`, no sudo, `Linger=yes`, user systemd running. -- Podman 5.6.0 rootless. Host has node 24.17, npm, curl, jq, rsync — **no git**, - **no nginx** (hence the build container / nginx container). -- Neighbours: pod-strapi (`:1337`), Outline (`:9091`), code-server (`127.0.0.1:41787`). -- **VPN does NOT route to machine ports** (`curl 192.168.10.15:1337` times out); - only the admin-managed HTTPS ingress reaches the machine - (`https://strapi.isl-dev.grid.cyfronet.pl` → 204). Hence ports 3000/8080 are - reachable for us only via `ssh -L`, and the team needs vhosts from Hubert. - -## DONE - -### 1. Local: files authored + reviewed + tested (before touching the machine) -- Wrote quadlets, `build.sh`, systemd service/timer, `nginx.conf`, - `Containerfile.build`, `README.md` (runbook incl. uninstall). -- **Opus security review** — 8 findings, every one verified personally: - BLOCKER `--network=host` (build code could reach host loopback: code-server - = RCE, Strapi) → replaced; `OnUnitActiveSec` is start-relative, not - end-relative (agent corrected me) → `OnUnitInactiveSec`; missing `mkdir` for - `~/.config/*`; nginx-before-first-build race; prune `pipefail` landmine; - noisy unit failure when Gitea is down; CORS `SCHEME` note; RAM headroom. - All fixed. No hallucinated findings. -- **Local end-to-end test** against the local Gitea (`:3010`), sandbox - `.tmp/2026-07-28-deploy-test/`: cold build 23 s · no-op poll 45 ms · - incremental rebuild 4 s · prune ✓ · Gitea-down grace ✓ · anonymous clone - (no token in `.git/config`) ✓. -- Commits (local Gitea repo `ctao/portal`): `ed7a9c0`, `80d886c`. - -### 2. Machine: install steps 1–7 of README -- Created `~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config}` - and `~/.config/{containers/systemd,systemd/user}`. -- Copied unit files, `build.sh`, `nginx.conf`, `Containerfile.build`. -- Pulled + **pinned by digest**: gitea 1.27-rootless, nginx stable-alpine, - node:24-alpine; built `localhost/ctao-portal-build:1`. -- Gitea running (`:3000`, API version 1.27.1), admin `ctao` created with - `--random-password` → `~/ctao-portal-demo/state/initial-admin.txt` (0600), - push token → `state/push-token.txt` (0600). **Never printed in chat.** -- Content pushed via git bundle (78 MB, one-off bootstrap): repo `ctao/portal` - at `80d886c`, public, anonymously clonable. Bundle deleted afterwards. -- **First build on the machine: 44 s** (`npm ci` ~35 s + astro build 6.5 s), - 225 pages. Incremental rebuilds should be ~10 s (measure to confirm). -- nginx container running, `http://localhost:8080/` → **200, `CTAO - Science Portal`**. -- Poll timer enabled (`ctao-portal-build.timer`), ticking every ~10 s, journal - clean. - -### 3. Problems hit on the machine (and the fixes) -1. **Rootless bridge network impossible**: `netavark: modprobe ip_tables: - Operation not permitted` → quadlet restart loop. Root cause: kernel module - not loaded, rootless cannot modprobe, no sudo. **Fix:** dropped - `ctao-demo.network`; build joins Gitea's netns - (`--network=container:ctao-demo-gitea`), so it reaches Gitea on - `localhost:3000` while the host loopback stays unreachable (pasta). - Same security property as the bridge, zero privileges. Lesson written to - `.skills/podman-quadlet/SKILL.md`. -2. **Push rejected**: admin was created with `--must-change-password` → - `remote: Update your password`. **Fix:** `gitea admin user - must-change-password --unset ctao`. -3. **My rc-chain bug**: `... | sed` made `$?` the sed's status, so a failed - push printed "push-ok" and the bundle was deleted prematurely → had to - re-upload 78 MB. **Fix:** capture output in a var, check rc of the real - command. (Also why the upload "took so long" — it ran twice.) -4. **Push-created repo was private** despite `DEFAULT_PRIVATE=public` — - push-to-create has its own key. **Fix:** PATCH via API to public + - `GITEA__repository__DEFAULT_PUSH_CREATE_PRIVATE=false` in the quadlet. -5. **`npm ci` failed: `ETXTBSY` spawning `esbuild`** (postinstall race in a - rootless container, ~5 min wasted). **Fix:** `npm ci --ignore-scripts` - (also removes the malicious-postinstall vector) + persistent npm cache - volume `~/ctao-portal-demo/npm-cache`. Validated locally, then on machine. - -### 4. Left on the machine (known, not cleaned — user asked to be told, not tidied) -- Orphan systemd entry `ctao-demo-network.service` (not-found/active-exited), - ghost of the removed network quadlet. Harmless; clears with - `systemctl --user stop ctao-demo-network.service`. -- `/tmp/storage-run-1003/` (~136 KB) — podman runroot from ssh calls without - `XDG_RUNTIME_DIR`. Disappears on reboot. (`storage-run-1001` is NOT ours.) -- Red journal entries from the netavark restart loop (history, not a live fault). -- Disk: 11 G → 9.6 G free (images ~590 MB + repo/content). RAM available ~1.3 G. - -### 5. Pipeline proven end-to-end on the machine (2026-07-28) -- Pushed `46ec5e2` to the machine's Gitea (incremental bundle, 6 KB — this is - what ongoing syncs cost, vs the 78 MB one-off bootstrap). -- **The poller published it automatically in 26 s end-to-end** (push → - visible), of which the build itself was 21 s. That is the demo's publish - latency: **~30 s worst case** on 2 vCPU (vs 44 s for the very first build - which included `npm ci`). Cold-cache builds only happen when the lockfile - changes. -- Verified after publish: `/` 200, `/admin/` 200 (Sveltia), an article page - 200; neighbours untouched (Strapi 204, Outline 200); RAM available 1.2 G, - disk free 9.0 G. - -### 6. Sveltia wired to the machine + editor cycle proven (2026-07-28) -- **OAuth app created via API, no UI clicking**: temp `write:user` token from - `gitea admin user generate-access-token` (CLI in container) → POST - `/user/applications/oauth2` → temp token deleted (needed basic auth from - `initial-admin.txt`; token-auth DELETE returns 401). App: "Sveltia CMS - (demo)", `client_id cf1c44ac-ebe6-4a97-bf2f-f7f26ef1126c`, PKCE - (`confidential_client=false`), redirect_uris: `http://localhost:18080/admin/` - and `http://localhost:8080/admin/`. -- **Found & fixed**: `public/admin/config.yml` still pointed at the MAC's Gitea - (`localhost:3010` + old app_id) — CMS login on the machine could never work. - Switched to `localhost:3000` + machine app_id (`bc02012`). localhost:3000 is - valid both on the machine and through the tunnel (same-port trick). -- **Editor cycle simulated exactly like Sveltia does it** (Gitea contents API): - create article via API → **live on the portal in 12 s**; delete via API → - **gone in 21 s**. Repo left clean (`29385a5`). Earlier full-push rebuild: - 16 s build. Publish latency ≈ **10–30 s** depending on poll-tick alignment. -- Tunnel run from the Mac with keepalive (`-o ServerAliveInterval=30`), portal - mapped to **18080** locally (user request: non-default port), Gitea kept at - **3000** (must match ROOT_URL). First tunnel died after idle — keepalive fixed. -- Push token scope confirmed minimal (`write:repository` only) — cannot manage - users/apps; file format is `:<40-hex>` (grep the hex out). - -## TODO (next agent starts here) - -1. ~~Commit the `--ignore-scripts` fix and mirror to the machine~~ — DONE - (`46ec5e2`, and it doubled as the live pipeline test above). -2. ~~Measure incremental publish latency on the machine~~ — DONE: **26 s - end-to-end**, build 21 s. -3. ~~Verify the loop through `ssh -L`~~ — mostly DONE: tunnel - `-L 18080:localhost:8080 -L 3000:localhost:3000` (with ServerAlive - keepalive) verified with real content. **Remaining: the user clicks - "Sign in with Gitea" at `http://localhost:18080/admin/`** and edits an - article in the browser — the only step that needs a human + browser - (PKCE login). Credentials: `initial-admin.txt` on the machine. -4. ~~Sveltia OAuth app~~ — DONE via API (see §6). Config committed and - auto-published. Only the browser login test remains (see 3). -5. **Message to Hubert** (only after we have seen it working): - - two ingress vhosts → `192.168.10.15:8080` (portal) and `:3000` (gitea), - same mechanism as `strapi.isl-dev…:1337`; - - are vhosts public-internet or VPN-scopable? (Gitea preferably VPN-only); - - does Cyfronet offer static-file hosting on the ingress itself? If yes we - drop our nginx container and just rsync builds there. -6. **After vhosts arrive**: replace both `TODO(vhost)` in - `ctao-demo-gitea.container` (ROOT_URL, CORS origin + `GITEA__cors__SCHEME=https`), - `daemon-reload` + restart, update Sveltia config, re-test login. -7. **Backup**: nightly `tar` of `~/ctao-portal-demo/gitea-data` (the only - stateful thing; everything else is rebuildable). Not yet written. -8. **Decided but not implemented — split into two repos** (`portal` = code, - `portal-content` = markdown + uploads). Rationale: editors get write access - ONLY to content, so repo-push cannot alter `package.json`/templates that the - build executes; also keeps code history clean. Cost: `build.sh` clones two - repos and overlays content (~10 lines), polls two SHAs. Do this for the - production version, not mid-demo. -9. Demo choreography: the test article is `draft: true`; the user creates one - live during the demo. With the poller running, a Sveltia save publishes - automatically — no manual rebuild needed (unlike the Mac preview setup). - -## Command cheat-sheet (machine) - -``` -export XDG_RUNTIME_DIR=/run/user/$(id -u) # needed for systemctl --user over ssh -systemctl --user status ctao-demo-gitea ctao-demo-web ctao-portal-build.timer -journalctl --user -u ctao-portal-build -n 20 --no-pager # build times land here -~/ctao-portal-demo/bin/build.sh # manual build -readlink ~/ctao-portal-demo/releases/current # which sha is live -curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ -``` diff --git a/deploy/ctao-demo-gitea.container b/deploy/ctao-demo-gitea.container index 055e70a..515d61d 100644 --- a/deploy/ctao-demo-gitea.container +++ b/deploy/ctao-demo-gitea.container @@ -40,7 +40,7 @@ Environment=GITEA__service__DISABLE_REGISTRATION=true Environment=GITEA__mailer__ENABLED=false # Repos default to public so the build pipeline can clone anonymously — # content is the public site anyway. The `ctao/portal` repo itself is -# created in the UI (README step 6). +# created in the UI (README step 7). Environment=GITEA__repository__DEFAULT_PRIVATE=public # Sveltia is served from the portal vhost and calls the Gitea API cross-origin # — CORS locked to exactly that origin. ALLOW_DOMAIN takes FULL origins with diff --git a/package-lock.json b/package-lock.json index 9b107e9..5fa8b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3591,9 +3591,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -3791,9 +3791,9 @@ } }, "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "funding": [ { "type": "opencollective", @@ -3810,7 +3810,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index 0d1ec3a..ea43c3d 100644 --- a/package.json +++ b/package.json @@ -15,5 +15,8 @@ "devDependencies": { "@astrojs/check": "0.9.10", "typescript": "6.0.3" + }, + "engines": { + "node": ">=22.19.0" } } diff --git a/public/diagrams/architektura-cms-z-baza.svg b/public/diagrams/architektura-cms-z-baza.svg deleted file mode 100644 index 8b4db31..0000000 --- a/public/diagrams/architektura-cms-z-baza.svg +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - MASZYNA · CMS Z BAZĄ DANYCH - np. WordPress, Strapi, Drupal — wspólny schemat tej klasy rozwiązań - - - - - - - - Redaktor - panel admina w przeglądarce - logowanie do aplikacji - - - - - - - - Czytelnik - publiczna strona portalu - HTML składany na żądanie - - - - - - APLIKACJA CMS - - NON STOP - - panel admina - publiczny adres + logowanie - z internetu - - renderowanie stron - każde wejście czytelnika = - praca aplikacji i bazy - - pluginy / rozszerzenia - własny cykl aktualizacji - i podatności - - - - - - BAZA DANYCH - - NON STOP - treść + konta użytkowników - wymaga backupów i testów odtwarzania - migracje schematu przy aktualizacjach - historia zmian: zależnie od produktu, - bywa funkcją płatną - - - - - - media/ - - WOLUMEN - obrazy i pliki poza bazą - osobny backup - - - - logowanie do panelu - - - HTML na żądanie - - - SQL - - - upload - - - - 2 procesy działają bez przerwy, także nocą - - aktualizacje bezpieczeństwa = stały obowiązek - diff --git a/public/diagrams/architektura-maszyna.svg b/public/diagrams/architektura-maszyna.svg deleted file mode 100644 index bda902b..0000000 --- a/public/diagrams/architektura-maszyna.svg +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - MASZYNA DEMO · PODMAN - osobne kontenery pod systemd · wszystko w jednym katalogu użytkownika - - - - - - - - Redaktor - przeglądarka · /admin/ (Sveltia) - edycja WYSIWYG, podgląd 1:1 - - - - - - - - Czytelnik - publiczna strona portalu - czysty, szybki HTML - - - - - - GITEA - - :3000 - - repo ctao/portal - treść: Markdown + obrazy (WebP) - każda zmiana = commit - pełna historia + rollback za darmo - - - - - - BUILD · ASTRO - - KONTENER JEDNORAZOWY - timer systemd (co ~10 s) sprawdza, - czy w repo jest nowy commit - jeśli tak: git fetch → checkout → - Markdown → statyczny HTML - po buildzie kontener znika - - - - - - releases/ - - WOLUMEN - 9f2c1d/ poprzednia wersja - 71b980/ ← current (symlink) - podmiana atomowa — zero przestoju - - - - - - NGINX - - :8080 - serwuje releases/current - wyłącznie statyczne pliki - brak aplikacji · brak bazy · brak PHP - - - - zapis przez API (OAuth) - - - git fetch - - - nowa wersja strony - - - current/ - - - statyczny HTML - diff --git a/public/diagrams/cover-git-cms.svg b/public/diagrams/cover-git-cms.svg deleted file mode 100644 index 565f5fe..0000000 --- a/public/diagrams/cover-git-cms.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - M↓ - - diff --git a/public/diagrams/od-zapisu-do-publikacji.svg b/public/diagrams/od-zapisu-do-publikacji.svg deleted file mode 100644 index 471b2ad..0000000 --- a/public/diagrams/od-zapisu-do-publikacji.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 2 - 3 - 4 - 5 - - - Zapis w edytorze - Sveltia · WYSIWYG - podgląd artykułu 1:1 - obrazy → WebP przy uploadzie - - - Commit w repo - Gitea · logowanie OAuth - zapis przez API = commit - historia każdej zmiany - - - Detekcja zmiany - timer systemd - sprawdza repo co ~10 s - brak zmian = brak pracy - - - Build strony - jednorazowy kontener - Astro: Markdown → - statyczny HTML - - - Publikacja - atomowa podmiana symlinka - nginx od razu serwuje - nową wersję - - - - awaria builda? strona działa dalej na starej wersji - - rollback = git revert — jedna komenda - diff --git a/public/diagrams/powierzchnia-ataku.svg b/public/diagrams/powierzchnia-ataku.svg deleted file mode 100644 index b887430..0000000 --- a/public/diagrams/powierzchnia-ataku.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - CMS Z BAZĄ DANYCH - internet - - - ▪ aplikacja CMS (PHP / Node) — publiczna - ▪ panel logowania — publiczny - ▪ baza danych z treścią - ▪ pluginy i zależności - wszystko działa non stop i wymaga patchowania - - - - - - TO ROZWIĄZANIE - internet - - - ▪ pliki HTML — i nic więcej - nie ma czego zhakować ani patchować - - edycja - - - ▪ git (Gitea) — tylko zalogowani - poza ścieżką czytelnika — awaria nie kładzie strony - diff --git a/src/content.config.ts b/src/content.config.ts index d0c0922..10023f4 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -30,7 +30,8 @@ const news = defineCollection({ // because the content type differs (no dates/covers/categories), and editors // get a distinct "Pages" section in the CMS. const pages = defineCollection({ - loader: glob({ pattern: '**/*.md', base: './src/content/pages' }), + // Top-level only, same reason as news: the [slug] route is non-rest. + loader: glob({ pattern: '*.md', base: './src/content/pages' }), schema: z.object({ title: z.string(), description: z.string().optional(), diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 14b7317..fae7bcf 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -1,6 +1,6 @@ --- import '../styles/global.css'; -// `ambient` opts a page into the whole-page drifting brand wash (home only — see DESIGN.md) +// `ambient` opts a page into the whole-page drifting brand wash (home only) // `type`/`image` feed the social meta: articles pass type="article" + their cover. const { title = 'CTAO Science Portal', description = 'CTAO Science Portal demo', ambient = false, type = 'website', image } = Astro.props; // Absolute URLs for canonical/OG/RSS (head pattern from the official Astro diff --git a/src/pages/404.astro b/src/pages/404.astro index 1859ed5..130c600 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -2,7 +2,7 @@ import Base from '../layouts/Base.astro'; --- - {/* Document archetype (DESIGN.md): the document IS the page — h1, one line, + {/* Document archetype: the document IS the page — h1, one line, recovery links, and a real GET search form as the way forward. */}
diff --git a/src/pages/index.astro b/src/pages/index.astro index 458e107..7fa2335 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -6,10 +6,10 @@ import NewsCard from '../components/NewsCard.astro'; const posts = (await getCollection('news', ({ data }) => !data.draft)) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) .slice(0, 3); -// Services per the Science Portal spec. Tiles carry no status chrome — honesty -// lives at the interaction point (REQUIREMENTS §5): each target page opens with -// its own whisper badge. Planned services stay in the grid but subdued (muted, -// no link) with availability folded into the description text. +// Services per the Science Portal spec (REQUIREMENTS §5). Tiles carry no +// status chrome; the footer's demo note is the single mock disclosure. +// Planned services stay in the grid but subdued (muted, no link) with +// availability folded into the description text. // Tile icons: hand-drawn stroke glyphs in the header-icon family (.ico — // currentColor, round caps), aria-hidden. NN/g icon research: icons alone are // ambiguous, but NEXT TO an always-visible label they differentiate otherwise @@ -35,7 +35,7 @@ const planned = ['Scheduling', 'Science alerts']; {/* Photographic hero: LST-1 under the La Palma night sky (copied from the content library to /brand/hero.jpg) beneath the Galaxy scrim — text - contrast computed worst-case in DESIGN.md. Three elements only: + contrast computed worst-case at design time. Three elements only: headline, brand subtitle, CTA pair (Apple hero restraint). */}
@@ -60,7 +60,7 @@ const planned = ['Scheduling', 'Science alerts']; for the user's top tasks; for scientists that is proposals/data/support, not reading news. Freshness is still signalled above the fold by the one-line hero teaser, so the 3-card news section can sit below. - Surface rhythm (DESIGN.md): services on Moon Gray, editorial news on white. */} + Surface rhythm: services on Moon Gray, editorial news on white. */}

Services

diff --git a/src/pages/news/[...page].astro b/src/pages/news/[...page].astro index 3df5590..e183974 100644 --- a/src/pages/news/[...page].astro +++ b/src/pages/news/[...page].astro @@ -49,7 +49,7 @@ const hrefFor = (n: number) => (n === 1 ? '/news' : `/news/${n}`); description={first ? 'News and announcements of the CTAO' : `News and announcements of the CTAO, page ${page.currentPage} of ${page.lastPage}`} > {/* Section-landing archetype: white head, display Galaxy h1 + standfirst - (the navy band is reserved for / and /login — DESIGN.md). */} + (the navy band is reserved for / and /login). */}

News & Announcements

Science highlights and updates from the observatory and its partners.

diff --git a/src/pages/news/[slug].astro b/src/pages/news/[slug].astro index 39f6cff..cd88b1c 100644 --- a/src/pages/news/[slug].astro +++ b/src/pages/news/[slug].astro @@ -25,7 +25,7 @@ const readMin = readMinOf(post); animation-timeline is supported; elsewhere it stays an empty div. */} {/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed
- under the title below that. CSS shows exactly one of the two (DESIGN.md). */} + under the title below that. CSS shows exactly one of the two. */} {/* lang sits on the article, not : the chrome (nav/footer) stays English for screen readers even when the article body is not. */}
diff --git a/src/pages/search.astro b/src/pages/search.astro index 9770dc3..df18c25 100644 --- a/src/pages/search.astro +++ b/src/pages/search.astro @@ -4,7 +4,7 @@ import Base from '../layouts/Base.astro'; {/* Utility archetype: ONE surface per page — Moon canvas from the landing head down (borderless cards/panels pop against it); editorial pages stay - all-white. No mid-page surface seams (DESIGN.md). */} + all-white. No mid-page surface seams. */}

Search

diff --git a/src/styles/global.css b/src/styles/global.css index 0e9f03a..360e574 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -2,9 +2,9 @@ CTAO Science Portal — design tokens per the official CTAO Brand Guidelines (April 2024): colors D.2, typography D.3 (Inter body / Space Grotesk display at medium weight per D.3.4), flash D.4. All look-and-feel lives in the tokens - below; WCAG 2.1 AA contrast (photo-hero worst cases computed — see DESIGN.md). + below; WCAG 2.1 AA contrast (photo-hero worst cases computed at design time; recompute if scrim or glow alphas change). Light theme only (brand is light-first; navy bands carry the dark identity). - System rules (spacing / elevation / surfaces / motion): see DESIGN.md. + System rules (spacing / elevation / surfaces / motion) live in the comments next to each block below. ========================================================================== */ :root { color-scheme: light; @@ -13,7 +13,7 @@ --cherenkov: #00e4d8; /* Cherenkov Blue — primary CTA, flash, hero subtitle, logo cluster ONLY */ --moon: #f5f5f5; /* Moon Gray — surfaces */ --azure: #007aff; /* Cosmic Azure — focus rings (UI only on light) */ - --indigo: #00009c; /* Interestellar Indigo — gradient depth */ + --indigo: #00009c; /* Interstellar Indigo — gradient depth */ /* Derived, WCAG-adjusted */ --text: #101228; /* near-Galaxy body ink */ --muted: #46536a; @@ -54,7 +54,7 @@ --dur: 0.25s; --ease: ease-out; /* Nebula glow (BRAND D.5) — auth band, static AND animated. Alphas are - contrast ceilings (DESIGN.md): Cherenkov ≤.38 / Azure ≤.35; cores sit in + contrast ceilings (computed for AA at design time): Cherenkov ≤.38 / Azure ≤.35; cores sit in opposite corners — cores never meet. */ /* Aurora rule (Gemini-style): gradient-only surfaces (login) get LARGE, soft, slowly flowing color fields — one protagonist per surface: the photo hero @@ -63,7 +63,7 @@ radial-gradient(60% 70% at 78% 78%, rgba(0, 228, 216, 0.42), transparent 70%), radial-gradient(55% 62% at 10% 12%, rgba(0, 122, 255, 0.36), transparent 68%), radial-gradient(80% 90% at 42% 55%, rgba(0, 0, 156, 0.8), transparent 74%); - /* Hero scrim over the photo — worst-case AA computed in DESIGN.md: + /* Hero scrim over the photo — worst-case AA computed at design time: small text lives left of 52% band width (α ≥ .76 there). */ --hero-scrim: linear-gradient(to right, rgba(0, 0, 74, 0.84), rgba(0, 0, 74, 0.76) 52%, @@ -123,7 +123,7 @@ html { -webkit-text-size-adjust: 100%; } to the in-content anchor targets fixes typing and keeps every jump (TOC headings, skip link → #main) landing below the header. */ main, main [id] { scroll-margin-top: calc(var(--header-h) + 20px); } -/* Guard, not a solution (DESIGN.md): every wide element is contained at its own +/* Guard, not a solution: every wide element is contained at its own level (tables scroll in their box, long words wrap); clip only catches regressions so one offender can never remove the page gutters. `clip`, unlike `hidden`, creates no scroll container — sticky header keeps working. */ @@ -334,7 +334,7 @@ img { max-width: 100%; height: auto; display: block; } } /* Hero (home only) — image-led: LST-1 under the night sky, full-bleed beneath a - Galaxy scrim (--hero-scrim; worst-case AA computed in DESIGN.md). With motion + Galaxy scrim (--hero-scrim; worst-case AA computed at design time). With motion allowed a Cherenkov/Azure glow layer drifts above the photo (see motion block). */ .hero-band { background: var(--hero-scrim), url("/brand/hero.jpg") center / cover, var(--galaxy); @@ -692,38 +692,10 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive functional state, brand tint, no new hues */ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(var(--radius) - 12px); padding: 0 2px; } -/* Mock/status badges — whisper-quiet hairline pill, ONLY where a mock - interaction actually happens (/proposals, /dashboard, /login, /support head); - REQUIREMENTS §5 honesty lives at the interaction point, not on every tile. */ -.badge-mock, .badge-ext { - display: inline-flex; align-items: center; gap: 6px; max-width: 100%; - font-size: var(--fs-xs); font-weight: 500; color: var(--muted); - background: var(--moon); border: 1px solid var(--border); - border-radius: var(--radius-pill); padding: 2px 10px; margin-bottom: 14px; -} -.badge-mock::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--galaxy); flex: none; } - -/* Dashboard mock — aggregation panels with status pills */ +/* Dashboard — aggregation panels (empty states until integrations exist) */ .dash-grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; margin-top: var(--space-s); } @media (min-width: 940px) { .dash-grid { grid-template-columns: repeat(2, 1fr); } } .panel-title { color: var(--galaxy); font-size: var(--fs-l); margin: 0 0 var(--space-s); font-family: var(--font-body); font-weight: 600; } -.rows { list-style: none; margin: 0; padding: 0; } -.rows li { - display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; - padding: 10px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-s); -} -.rows li:last-child { border-bottom: 0; } -.row-id { color: var(--muted); font-size: var(--fs-xs); min-width: 7.5em; } -.row-main { flex: 1; min-width: 12em; } -.row-date { color: var(--muted); font-size: var(--fs-xs); } -/* Status pills — neutral chrome; the label text carries the state */ -.pill { - font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; - padding: 3px 10px; border-radius: var(--radius-pill); border: 1px solid var(--border); -} -.pill--ok { color: var(--galaxy); background: var(--moon); } -.pill--review { color: var(--text); background: #fff; } -.pill--draft { color: var(--muted); background: #fff; } /* Auth (login mock) — nebula backdrop + floating card; the ONLY navy band besides the home hero */ @@ -740,10 +712,6 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va } /* Logo ≥100px digital (BRAND B.1.3): 26px height ≈ 125px wordmark */ .auth-logo { height: 26px; width: auto; margin-bottom: var(--space-m); align-self: flex-start; } -/* Whisper badge sits top-right, off the reading flow (back in flow ≤480px - where it would collide with the logo) */ -.auth-card .badge-mock { position: absolute; top: var(--space-l); right: var(--space-l); margin: 0; } -@media (max-width: 480px) { .auth-card .badge-mock { position: static; margin-bottom: 14px; } } /* Hierarchy per BRAND D.3.4: SG-medium Galaxy headline, Inter body */ .auth-card h1 { color: var(--galaxy); margin: 0; font-size: var(--fs-h2); } .auth-card .muted { color: var(--muted); font-size: var(--fs-s); margin: 0; } @@ -790,7 +758,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va /* NO cross-document view transitions: the root crossfade snapshots the new page at its first render opportunity — on slow networks that is a half- parsed page, so every navigation reads as a full-page blink. Plain MPA - paint + prefetch is the intended feel (see DESIGN.md, Motion policy). */ + paint + prefetch is the intended feel (motion policy: this block is the only place animations live). */ html { scroll-behavior: smooth; } /* The auth band hands its glow to a roaming layer inside the .band-bg clip frame (same --nebula stack); the photo hero keeps its scrimmed photo and