Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,7 +57,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Validate the app server config
run: |
Expand All @@ -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
Expand All @@ -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
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
8 changes: 7 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand All @@ -76,13 +76,18 @@ 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
├── package.json
└── 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:
Expand Down
37 changes: 37 additions & 0 deletions memory-bank/activeContext.md
Original file line number Diff line number Diff line change
@@ -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).
34 changes: 34 additions & 0 deletions memory-bank/productContext.md
Original file line number Diff line number Diff line change
@@ -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).
36 changes: 36 additions & 0 deletions memory-bank/progress.md
Original file line number Diff line number Diff line change
@@ -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`.
41 changes: 41 additions & 0 deletions memory-bank/projectbrief.md
Original file line number Diff line number Diff line change
@@ -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).
50 changes: 50 additions & 0 deletions memory-bank/systemPatterns.md
Original file line number Diff line number Diff line change
@@ -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-<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.
47 changes: 47 additions & 0 deletions memory-bank/techContext.md
Original file line number Diff line number Diff line change
@@ -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 `<style>` or style attributes
anywhere, including generated pages.
- Base images digest-pinned; update tag + digest together, deliberately.
- Prod deploys are automatic on `main` — never trigger prod targets unasked.
Loading