From ec24efd31625c73e3e1100c2c22baf4734a3f1b4 Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Sun, 13 Sep 2026 06:29:11 -0500 Subject: [PATCH 1/9] docs(stories): add US-001 multiple alarms user story --- docs/stories/US-001-multiple-alarms.md | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/stories/US-001-multiple-alarms.md diff --git a/docs/stories/US-001-multiple-alarms.md b/docs/stories/US-001-multiple-alarms.md new file mode 100644 index 0000000..02c2adf --- /dev/null +++ b/docs/stories/US-001-multiple-alarms.md @@ -0,0 +1,65 @@ +# User Story: Multiple Alarms + +**Story ID:** US-001 +**Epic:** Alarms +**Status:** Draft + +--- + +## 1. Description + +**As a** visitor who keeps the clock site open at their desk, +**I want to** create and manage several named alarms, +**So that** I get reminded of time-sensitive moments (breaks, meetings, cooking) without needing a phone app or an account. + +--- + +## 2. Context & Scope + +- **In Scope:** + - Create alarms with an optional name (defaults to "Alarm") and a time of day (HH:MM) + - A list showing all alarms, each with an enable/disable toggle and a delete action + - One-shot behavior: an alarm rings once, then automatically turns itself off + - While ringing: a clear visual alert and an audible tone, with **Dismiss** and **Snooze (+9 minutes)** actions + - Alarms are saved in the browser on the user's device and survive a page reload + - Known limitation, stated in the UI: an alarm only rings while the page is open +- **Out of Scope:** + - Daily-repeat / recurring alarms — v2 story if users ask + - Configurable snooze duration or snooze intervals + - Custom alarm sounds or sound picker + - Ringing when the tab is closed or the device is asleep; OS/browser notifications + - Accounts, cross-device sync, or any backend + +--- + +## 3. Acceptance Criteria + +_These criteria define "Done." Every criterion must be verified by QA._ + +- [ ] **AC-1:** Given the alarms panel is empty, When the user creates an alarm named "Standup" for 09:30, Then it appears in the list, enabled, with its name and time shown. +- [ ] **AC-2:** Given an enabled alarm set for 09:30, When the device clock reaches 09:30 with the page open, Then a visual and audible alert starts, and the alarm is automatically disabled afterward. +- [ ] **AC-3:** Given an alarm is ringing, When the user chooses Snooze, Then the alert stops and the alarm re-arms for 9 minutes later; When the user chooses Dismiss, Then the alert stops and the alarm stays disabled. +- [ ] **AC-4:** Given one or more alarms exist, When the page is reloaded, Then all alarms reappear with their names, times, and enabled/disabled states intact. +- [ ] **AC-5:** Given an alarm is disabled, When its time arrives, Then no alert occurs. +- [ ] **AC-6 (edge case):** Given the user enables an alarm whose time is earlier than the current time, Then it does not ring today and is scheduled for the next occurrence of that time (tomorrow). +- [ ] **AC-7:** Given an alarm is deleted, Then it disappears from the list, no longer rings, and no longer reappears after a reload. +- [ ] **AC-8 (validation):** Given the user creates an alarm, Then an invalid or empty time is rejected with a clear message; a blank name defaults to "Alarm". + +--- + +## 4. Technical Guidance & Architectural Constraints + +(Filled in by Architect) + +- **Target Files / Boundaries:** TBD +- **Constraints:** TBD + +--- + +## 5. Definition of Done Checklist + +- [ ] Code implemented strictly according to ACs. + +- [ ] Unit/Integration tests written and passing. +- [ ] QA assessment passed. +- [ ] Code Review approved. From 26cab4fe5b4fb28e5e2000fdce7c12cf5a9a05ff Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Sun, 13 Sep 2026 13:07:10 -0500 Subject: [PATCH 2/9] docs(stories): add architect constraints to US-001 --- docs/stories/US-001-multiple-alarms.md | 39 +++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/stories/US-001-multiple-alarms.md b/docs/stories/US-001-multiple-alarms.md index 02c2adf..c733d8b 100644 --- a/docs/stories/US-001-multiple-alarms.md +++ b/docs/stories/US-001-multiple-alarms.md @@ -49,10 +49,41 @@ _These criteria define "Done." Every criterion must be verified by QA._ ## 4. Technical Guidance & Architectural Constraints -(Filled in by Architect) - -- **Target Files / Boundaries:** TBD -- **Constraints:** TBD +(Filled in by Architect. Verdict: **Yes** — the existing stack covers this fully; no new dependency, service, or framework is approved, and no ADR is required because the feature follows the established feature-module pattern without any new architectural decision.) + +### Target Files / Boundaries + +- New feature module `web/src/features/alarm/`, mirroring the timer feature's structure exactly: + - `alarm.core.ts` — pure state machine: alarm data shape, add / remove / enable / disable, next-occurrence computation, the per-tick check (e.g. `checkAlarms(state, now)`), and snooze. **Zero DOM, zero storage.** + - `alarm.storage.ts` — localStorage adapter, mirroring `timer.storage.ts`. + - `alarm.ui.ts` — DOM wiring, the 1-second scheduler interval, the alarm list rendering, and the ringing overlay. Exposes `initAlarm(rootElement, config)` returning a handle with `destroy()` (same contract as `initTimer`), and degrades gracefully when the container is absent. + - `index.ts` — barrel; other modules may only import via `@/features/alarm`. + +- `web/src/app/config.ts` — add an `ALARM_STORAGE_KEY` constant. +- `web/src/app/app.ts` — wire `initAlarm` (composition root is the only place that resolves the container; `main.ts` stays a one-liner). +- `web/src/index.html` — alarm toggle button + panel markup inside `#clock-container`, mirroring the timer wrapper pattern; every interactive element gets an `aria-label`. +- `web/src/styles/alarm.css` (new) imported from `main.css`; reuse the design tokens in `variables.css`; any media-query rules go in `responsive.css`. +- Tests mirror the source one-to-one under `web/tests/unit/features/alarm/` (core, storage parsing; optional UI tests via jsdom as the existing suite does). + +### Constraints + +- **No new dependencies.** The alert tone reuses `playBeep()` from `@/shared/audio/beep` (inject an `AudioContext` in tests); a repeating beep while ringing follows the timer's existing beep-interval pattern. +- **Purity boundary:** `alarm.core.ts` must not import the DOM, `localStorage`, or `import.meta.env`; current time is always passed in as a parameter so ticks are deterministic in tests. +- **Persistence:** a single JSON document under `ALARM_STORAGE_KEY`, read/written only through `alarm.storage.ts`; malformed stored data is discarded safely (fall back to empty list), matching the timer's parser. +- **IDs:** follow the existing `clock-registry` convention (`alarm--`). Do **not** wire `clock-registry.ts` into this story — it belongs to the separate multi-clock epic. +- **Scheduling:** the alarm feature owns its own 1-second interval, independent of the clock's tick; it must be cleared in `destroy()`. The tick check must be idempotent — an alarm may not ring twice for the same occurrence (including the minute it fires in and after page reload). +- **One-shot semantics:** firing disables the alarm (persisted immediately); snooze is modeled as a future `snoozedUntil` timestamp on the alarm, never as an edit to the alarm's target time. Snooze duration is the fixed constant 9 minutes. +- **Next occurrence (AC-6):** today if HH:MM is still ahead of `now`, otherwise tomorrow; computed in the pure core. +- **CSP:** inline styles and scripts are forbidden — all ringing visuals and dynamically rendered list items are styled via classes in `alarm.css` / `animations.css`. +- **Accessibility:** toggles are real labeled inputs/buttons; the ringing overlay is keyboard-reachable, and pressing Escape (or Enter) dismisses. +- The panel must display the "alarms ring only while the page is open" limitation (story In-Scope note). +- **Forbidden moves:** no Service Workers, Notification API, background timers beyond the page interval, new npm packages, changes to `nginx/`, compose files, cache policies, or the timer/clock features' behavior. +- **Verification gate:** `npm run typecheck`, `npm run lint`, `npx prettier --check .`, and `npm test` must pass locally before commit; CI additionally validates Docker/compose, which this story must not touch. + +### Risks for QA + +- Background-tab throttling may delay a ring by up to a minute — acceptable, and the panel copy must communicate the limitation. +- The double-fire guard across page reloads (reload at 09:30:30 must not re-ring a 09:30 alarm) is the trickiest acceptance criterion and needs explicit QA coverage. --- From 9197f738a567b8026376cebf258b86196a5d90f8 Mon Sep 17 00:00:00 2001 From: Hector Alvarez Date: Sun, 13 Sep 2026 13:15:20 -0500 Subject: [PATCH 3/9] chore(cline): add committed Cline project rules --- .clinerules/01-project.md | 31 ++++++++++++++++++++++++++++++ .clinerules/02-standards.md | 36 +++++++++++++++++++++++++++++++++++ .clinerules/03-memory-bank.md | 31 ++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 .clinerules/01-project.md create mode 100644 .clinerules/02-standards.md create mode 100644 .clinerules/03-memory-bank.md diff --git a/.clinerules/01-project.md b/.clinerules/01-project.md new file mode 100644 index 0000000..51618e2 --- /dev/null +++ b/.clinerules/01-project.md @@ -0,0 +1,31 @@ +# Clock Website — Project Rules + +Folder-per-artifact monorepo: `web/` (TypeScript + Vite 6 app, vanilla DOM — no framework), `nginx/` (edge reverse proxy), `webhook/` (deploy webhook), `scripts/` (host helpers), `docs/` (ADRs, runbooks, stories). Production ingress is a Cloudflare Tunnel; `main` triggers release.yml → GHCR images → webhook auto-deploy on the host. + +## Canonical Commands + +`make` targets run from the repo root; `npm` commands **only work inside `web/`** (`cd web && ...`). Node ≥22, pinned in `web/.nvmrc`. + +| Purpose | Command | +| :--- | :--- | +| Full quality gate — run 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 enforces it) | `cd web && npm run format:check` | +| Coverage | `cd web && npm test -- --coverage` | + +- CI (`.github/workflows/ci.yml`) also runs `npm run build`, `nginx -t` on both configs, `docker compose config -q` on both stacks, and hadolint on all four Dockerfiles. Any guard CI enforces must be reproducible locally — if you change pipeline-relevant files (compose, Dockerfiles, `nginx.conf`), run the equivalent local checks and say so. +- Git hooks: husky pre-commit runs lint-staged on staged `web/` files. **Never bypass with `--no-verify`** (merge commits/conflict resolutions excepted). + +## Workflow + +- Branch from `dev`, PR into `dev`; `main` is the release branch and only moves forward. Keep history **linear — rebase, don't merge**. +- Conventional commits, scoped where helpful (`feat(timer):`, `fix(web):`, `docs:`, `ci:`), small and self-contained — each commit must build and pass checks on its own (see global Commit Discipline rule). +- Any architecture change (container layout, ingress, deploy mechanism) requires adding or updating an ADR in `docs/adr/` in the same PR. New features get a user story in `docs/stories/` (see `US-001` for format). + +## Safety + +- Never edit `.env` or `.env.prod` (Cloudflare tunnel tokens, webhook token). Never echo or commit their contents. Dev and prod tunnels must use different tokens — never copy one into the other. +- Production deploys are automatic on `main` (webhook). Never run `make prod-deploy`, `make deploy-remote`, `make prod-*`, or `scripts/deploy.sh` unless explicitly asked. +- `make dev-reset` and `docker compose down -v` wipe volumes — confirm with the user first. +- Base images in all Dockerfiles are pinned by digest (ADR 0005). Never drop the pin; update it deliberately. \ No newline at end of file diff --git a/.clinerules/02-standards.md b/.clinerules/02-standards.md new file mode 100644 index 0000000..8f18b7b --- /dev/null +++ b/.clinerules/02-standards.md @@ -0,0 +1,36 @@ +--- +paths: + - "web/src/**" + - "web/tests/**" +--- +# Code Standards (TypeScript / Vite) + +## Architecture (feature-based, see README "Architecture") + +- Business logic lives in **pure cores** (`features/*/*.core.ts`, `shared/`) with zero DOM dependencies — push decisions into pure modules and keep DOM wiring thin. +- **UI layers** (`*.ui.ts`) query the DOM, bind events, and render from the pure core. **Adapters** (`*.storage.ts`, `shared/audio/beep.ts`) isolate side-effecting platform APIs; make platform dependencies injectable (e.g. optional `AudioContext` parameter) so they can be mocked in tests. +- The composition root (`src/app/app.ts`) is the **only** place that knows page element IDs; `main.ts` only calls `bootstrap()`. +- Consume features through their barrel (`@/features/clock`, `@/features/timer`), never by reaching into internal modules. Path alias `@/` → `src/`. + +## Structure + +- `src/features//` — one directory per feature with `index.ts` as its public surface; `src/shared/` — cross-feature primitives (time, audio, dom); `src/styles/` — one CSS file per concern imported via `main.css`; `src/public/` — copied verbatim to the build root. +- Tests mirror `src/` one-to-one under `web/tests/unit/` (`features/clock/clock.core.test.ts` ↔ `features/clock/clock.core.ts`). + +## Hard Constraints + +- The edge CSP has **no `unsafe-inline`** (ADR 0002): every page — including error pages — ships its CSS as a separate file linked via ``, never inline `