diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36e422c..b9d48f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version-file: web/.nvmrc cache: "npm" @@ -57,7 +57,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Validate the app server config run: | @@ -80,7 +80,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Validate the dev stack run: docker compose -f docker-compose.yml config -q @@ -103,28 +103,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Lint the app images - uses: hadolint/hadolint-action@v3.1.0 + uses: hadolint/hadolint-action@v3.5.0 with: dockerfile: web/Dockerfile config: .hadolint.yaml - name: Lint the production app image - uses: hadolint/hadolint-action@v3.1.0 + uses: hadolint/hadolint-action@v3.5.0 with: dockerfile: web/Dockerfile.prod config: .hadolint.yaml - name: Lint the edge image - uses: hadolint/hadolint-action@v3.1.0 + uses: hadolint/hadolint-action@v3.5.0 with: dockerfile: nginx/Dockerfile config: .hadolint.yaml - name: Lint the webhook image - uses: hadolint/hadolint-action@v3.1.0 + uses: hadolint/hadolint-action@v3.5.0 with: dockerfile: webhook/Dockerfile config: .hadolint.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2819b2..018b040 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,22 +41,22 @@ jobs: steps: - name: Checkout the commit CI validated - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ github.event.workflow_run.head_sha }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.context }}/${{ matrix.dockerfile }} diff --git a/.husky/pre-commit b/.husky/pre-commit index 466cbf4..0ae4bb1 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,7 @@ -cd web && npx lint-staged +# lint-staged runs from web/ and its globs (web/package.json) only match +# staged files under web/. Skip it entirely when the commit touches nothing +# it can check — otherwise it prints a confusing "could not find any staged +# files" warning on docs-, workflow-, or root-config-only commits. +if git diff --cached --name-only --diff-filter=ACMR | grep -qE '^web/.*\.(ts|js|json|css|md)$'; then + cd web && npx lint-staged +fi diff --git a/README.md b/README.md index 17c7296..1aa525f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ One directory per buildable artifact: | `webhook/` | Deploy webhook image, hooks, and entrypoint | | `scripts/` | Host-side helpers (deploy, tunnel bridge, bootstrap) | | `docs/` | Architecture decision records and runbooks | +| `tasks/` | Backlog and sprint tracking | The built app is served by **two** containers. `nginx/` is the edge: it owns the security headers, the real client IP, and `/healthz`. `web/` is a plain static @@ -61,7 +62,6 @@ web/ # Application workspace │ │ └── dom/query.ts # Typed querySelector helpers │ ├── public/ # Copied verbatim to the build root │ │ ├── favicon.svg -│ │ ├── robots.txt │ │ ├── 404.html # Served by nginx with a real 404 status │ │ └── 404.css # Separate file — the CSP forbids inline styles │ └── styles/ @@ -76,6 +76,7 @@ web/ # Application workspace ├── tests/ │ └── unit/ # Mirrors src/ one-to-one │ ├── features/alarm/alarm.core.test.ts +│ ├── features/alarm/alarm.storage.test.ts │ ├── features/clock/clock.core.test.ts │ ├── features/timer/timer.core.test.ts │ └── shared/time/format.test.ts @@ -83,6 +84,10 @@ web/ # Application workspace └── vite.config.ts ``` +`robots.txt` and `sitemap.xml` are **generated at build time** from +`VITE_SITE_URL` (see [Deployment](#deployment)) — never hand-add them to +`src/public/`. + ## Architecture The codebase is organized **by feature**, with a shared primitives layer: diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md new file mode 100644 index 0000000..33a35e1 --- /dev/null +++ b/memory-bank/activeContext.md @@ -0,0 +1,37 @@ +# Active Context + +Updated: 2026-09-13 + +## Current focus + +**Repo housekeeping — DONE on branch `chore/repo-housekeeping`** +(5 commits off `dev`, awaiting PR into `dev` + review): + +1. ✅ Memory bank initialized (HK-4). +2. ✅ README corrections (HK-1): `robots.txt` removed from `src/public/` tree, + build-time generation note added, alarm storage test added to tests tree, + `tasks/` added to the repo-layout table. +3. ✅ Actions bump (HK-2): checkout@v7, setup-node@v7, setup-buildx@v4, + login@v4, build-push@v7, hadolint-action@v3.5.0. All four Dockerfiles + pre-validated locally with hadolint 2.15.1 (repo config: only info-level + DL3059 in the webhook image). +4. ✅ Husky guard (HK-3): hook skips lint-staged unless staged files match + `^web/.*\.(ts|js|json|css|md)$`; verified silent on yml- and md-only + commits (noise previously confirmed on both). +5. ✅ `tasks/` close-out: US-001 marked merged+deployed (Release #5, + healthz 200); HK-1..4 removed; HK-5 (nginx 1.27 EOL re-pin) added as a + backlog candidate. + +## Environment state + +- `chore/repo-housekeeping` = dev + 5 commits; `dev` = `origin/dev` = `223d41f`. +- Local `main` = `origin/main` = `c7ecdca` (post-rebase-merge sync). +- US-001 deployed; homepage edge cache may lag up to 2 h. + +## Next steps + +- Open PR `chore/repo-housekeeping` → `dev`; merge (no production risk — + workflow changes ride CI on next dev push). +- Owner: production audio check (alarm tone) → US-001 fully Done. +- Pick next epic: US-002 (recurring alarms) vs US-004 (multi-clock). +- Schedule HK-5 (nginx 1.27 → current stable, tag + digest together). diff --git a/memory-bank/productContext.md b/memory-bank/productContext.md new file mode 100644 index 0000000..ffcc05b --- /dev/null +++ b/memory-bank/productContext.md @@ -0,0 +1,34 @@ +# Product Context + +## Why + +A personal productivity site: a clean, dark, always-visible clock for a wall +screen or a spare tab, growing into timer and alarms so one page replaces +several utilities. Hosted under the taylormadetech.net domain. + +## Problems it solves + +- No-install clock/timer/alarm with persistence (localStorage, no accounts). +- Distraction-free: no ads, no login, no tracking; single static page. +- Self-hosted learning vehicle for a production-grade deploy pipeline + (GHCR images, webhook deploys, Cloudflare Tunnel, edge caching). + +## How it should work + +- **Clock** — always ticking, 12-hour format with AM/PM, seconds shown, + timezone-aware, dev marker only in dev builds. Tab title mirrors the time. +- **Timer** — manual duration entry + presets, start/pause/reset, survives + reload via `localStorage`, beep + visual overlay on completion. +- **Alarms (US-001)** — create/name/enable/disable/delete multiple alarms, + one-shot firing with 9-minute snooze, ring overlay with Snooze/Dismiss, + keyboard dismissal (Esc/Enter), persisted across reloads. +- **Limitation (documented in-panel)** — alarms/timer ring only while the tab + is open; background-tab throttling can delay rings up to ~1 minute. No + OS/browser notifications (out of scope per US-001). +- **404 page** — served by the edge with a real 404 status; separate CSS file + because the CSP forbids inline styles. + +## UX notes + +- Dark theme, large digits, responsive; keyboard-friendly controls. +- Persistence is per-browser; no server state at all (static app). diff --git a/memory-bank/progress.md b/memory-bank/progress.md new file mode 100644 index 0000000..c296477 --- /dev/null +++ b/memory-bank/progress.md @@ -0,0 +1,36 @@ +# Progress + +## Status + +US-001 (Multiple Alarms) is **deployed to production** (PR #13 merged +2026-09-13 21:17Z; Release run #5 succeeded; `healthz` verified 200). +Site is live at `clock.taylormadetech.net`. + +## What works + +- Clock (tick, title mirror, timezone), timer (presets, persistence, beep), + alarms (multi, snooze, persistence, ring overlay) — 135 unit tests green. +- Full CI gate: prettier, eslint, typecheck, vitest+coverage, build, + `nginx -t` × 2, compose config × 2, hadolint × 4. +- Automated deploys: `main` → GHCR → webhook → swap with auto-rollback. +- Cloudflare edge caching of `index.html` (2 h Cache Rule; `/healthz` + uncacheable) per ADR 0006. + +## Remaining / known issues + +- Production audio QA: alarm tone not yet verified by a human (jsdom can't + test audio) — last open US-001 DoD item. +- Edge-cached homepage can lag up to 2 h behind a deploy (expected; judge + freshness via `/healthz` or hashed assets). +- CI actions pinned to Node-20-runtime majors (HK-2 — fix in progress). +- `nginxinc/nginx-unprivileged:1.27-alpine` is an EOL mainline branch + (HK-5 candidate: re-pin to current stable + digest). +- npm minor drift: eslint/prettier/lint-staged/TS patch bumps available; + vite 7+/vitest 5 majors deliberately deferred (not housekeeping). +- Multi-clock scaffold (`clock-registry.ts`) unwired (backlog US-004). +- FLIP helper duplicated in timer/alarm UI (backlog US-003). + +## Next epic decision (owner) + +US-002 recurring alarms vs US-004 multi-clock (US-004 wants US-003 item 1 +first). See `tasks/backlog.md` + `tasks/sprint.md`. diff --git a/memory-bank/projectbrief.md b/memory-bank/projectbrief.md new file mode 100644 index 0000000..4aa95c5 --- /dev/null +++ b/memory-bank/projectbrief.md @@ -0,0 +1,41 @@ +# Project Brief — Simple Clock + +A single-page digital clock website (clock + countdown timer + alarms) built +with TypeScript and Vite, served from a hardened container stack behind +Cloudflare Tunnel. Live at `clock.taylormadetech.net`. + +## Goals + +- Always-accurate time display, zero-friction UX (no accounts, no tracking). +- Countdown timer with presets; multiple named alarms with snooze and + persistence (US-001, deployed 2026-09-13). +- Fully automated, unattended production deploys (`main` → GHCR → webhook). +- Small attack surface: unprivileged containers, no inbound ports, CSP without + `unsafe-inline`. + +## Structure (folder-per-artifact monorepo) + +- `web/` — Vite app, tests, both app container images +- `nginx/` — edge reverse-proxy image (security headers, real client IP, `/healthz`) +- `webhook/` — deploy webhook image (listens for release trigger, runs deploy) +- `scripts/` — host-side helpers (deploy, tunnel bridge, bootstrap) +- `tasks/` — backlog + sprint tracking +- `docs/` — ADRs, runbooks, user stories + +## Constraints (see ADRs 0001–0006) + +- Both containers listen on 8080; host port mappings must target 8080. +- Base images pinned by digest; bump tag + digest together, deliberately. +- Production ingress is Cloudflare Tunnel only — dev and prod tunnels use + different tokens. +- `sitemap.xml` / `robots.txt` are build-time generated from `VITE_SITE_URL`. +- Edge caching: `index.html` cached via Cloudflare Cache Rule (origin + `cdn-cache-control` is ignored on the free plan); `/healthz` never cached. + +## Success criteria + +- `make check` green (lint + typecheck + tests) before any merge. +- Deploys require no manual host work; rollback is automatic on failed health + check. +- Alarms ring while the page is open (documented limitation: no background-tab + rings, no OS notifications). diff --git a/memory-bank/systemPatterns.md b/memory-bank/systemPatterns.md new file mode 100644 index 0000000..9bf573f --- /dev/null +++ b/memory-bank/systemPatterns.md @@ -0,0 +1,50 @@ +# System Patterns + +## App architecture (feature-based, see README) + +- **Pure cores** (`features/*/*.core.ts`, `shared/time`, `shared/audio`): all + business logic, zero DOM deps, fully unit-tested. Pure functions over + explicit state (`computeClockTick`, timer/alarm state machines). +- **UI layers** (`*.ui.ts`): query DOM, bind events, render from the core. +- **Adapters** (`*.storage.ts`, `shared/audio/beep.ts`): isolate side effects + (`localStorage`, `AudioContext`); platform deps are injectable for mocking. +- **Composition root** (`app/app.ts`): the ONLY place that knows page element + IDs; `main.ts` calls `bootstrap()`. `app/config.ts` holds build-time config + (mode, storage keys). +- Features are consumed via barrels (`@/features/clock`), path alias `@/` → + `src/`. `shared/dom/query.ts` provides typed querySelector helpers. +- Known duplication: FLIP panel-open helper duplicated between `timer.ui.ts` + and `alarm.ui.ts` (backlog US-003 item 1 → `shared/dom/`). +- `features/clock/clock-registry.ts`: multi-clock scaffold, not wired (US-004). + +## Container / pipeline architecture + +- **web**: dev image (Vite HMR, source-mounted) and prod image (build → + static serve on nginx-unprivileged). **nginx** edge: security headers, + `/healthz`, real client IP. **webhook**: receives release POST, runs + `scripts/deploy.sh` against the host through the Docker socket. +- CI (`.github/workflows/ci.yml`): prettier, eslint, typecheck, vitest with + coverage, build, `nginx -t` on both configs, `docker compose config` on both + stacks, hadolint on all four Dockerfiles. +- Release (`release.yml`): on CI success on `main` → build/push GHCR images + (`latest` + `sha-`, provenance + SBOM) → POST to deploy webhook. +- Git history: linear, rebase-merge only; PRs `dev` → `main`. + +## Conventions + +- Tests mirror `src/` one-to-one under `web/tests/unit/` (Vitest + jsdom); + behavior-documenting tests over DOM re-queries. +- Prettier: tabs, single quotes, semicolons, width 80, trailing commas. + ESLint: `consistent-type-imports`, no unused vars (`_` prefix), `no-console` + (error/warn only). +- Husky pre-commit: lint-staged on staged `web/` files. Never `--no-verify`. +- Conventional commits, scoped; each commit builds and passes checks alone. + +## Security invariants + +- Edge CSP has no `unsafe-inline`; every page (incl. 404) links a CSS file. +- Security headers live only in `nginx/nginx.conf`; the app container knows + nothing of the outside world. +- `.env` / `.env.prod` are secrets — never edit, echo, or copy tokens between + dev and prod tunnels. +- Prod deploys fire automatically on `main`; never run prod targets unasked. diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md new file mode 100644 index 0000000..bc21546 --- /dev/null +++ b/memory-bank/techContext.md @@ -0,0 +1,47 @@ +# Tech Context + +## Stack + +- **TypeScript** + **Vite 6** (vanilla DOM — no framework), Node ≥22 + (`web/.nvmrc`: 22). +- **Vitest 4 + jsdom** for tests; `@vitest/coverage-v8` for coverage. +- ESLint (flat config, `web/eslint.config.js`), Prettier, husky + lint-staged. +- Containers: `node:22-alpine`, `nginxinc/nginx-unprivileged:1.27-alpine` + (EOL mainline branch — see backlog HK-5), `golang:1.24-alpine`, `alpine:3.21` + — all digest-pinned (ADR 0005). Deployer: `github.com/adnanh/webhook` + pseudo-version pin. + +## Commands + +All npm commands run inside `web/`; make targets from the repo root. + +| Purpose | Command | +| :---------------------------------------- | :-------------------------------------------------- | +| Full quality gate (before declaring done) | `make check` (lint + typecheck + test) | +| Dev stack up/down/health | `make dev-up` / `make dev-down` / `make dev-health` | +| Fast host-only dev loop | `cd web && npm run dev` | +| Format check (CI-enforced) | `cd web && npm run format:check` | +| Coverage | `cd web && npm test -- --coverage` | +| Production build | `cd web && npm run build` → `web/dist/` | + +CI-relevant changes (compose, Dockerfiles, `nginx.conf`) must be validated +locally with the same checks CI runs (hadolint via `hadolint/hadolint` image, +`nginx -t` via the nginx image, `docker compose config -q`). + +## Setup facts + +- Dev edge at `http://localhost:8100` (compose); opt-in dev tunnel with its + own token. Prod stack uses GHCR images + prod tunnel + webhook. +- `VITE_SITE_URL` is the single source for canonical/OG URLs and the generated + `sitemap.xml` / `robots.txt` (see `web/vite.config.ts`). Unset → falls back + to `http://localhost:8100`. +- Env files: `.env` (dev), `.env.prod` (prod secrets) — gitignored, never + edit/echo. `.env.sample` documents the variables. + +## Constraints + +- Both containers listen on 8080; host mappings must target 8080. +- No `unsafe-inline` CSP (ADR 0002): no inline `