Skip to content
Merged
31 changes: 31 additions & 0 deletions .clinerules/01-project.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 36 additions & 0 deletions .clinerules/02-standards.md
Original file line number Diff line number Diff line change
@@ -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/<feature>/` — 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 `<link>`, never inline `<style>` blocks or style attributes.
- Site-wide security headers belong in `nginx/nginx.conf` only. The app container is a plain static file server that knows nothing about the outside world.
- `sitemap.xml` and `robots.txt` are **generated** at build time from `VITE_SITE_URL` (see `vite.config.ts`) — never hand-add them to `src/public/`. All absolute URLs derive from that single env value; there is no second place to edit.
- Both containers listen on **8080**; host port mappings must target 8080.

## Testing

- Vitest + jsdom. Unit-test pure logic directly (timer state machine, clock tick, time formatting); DOM-facing code is testable with jsdom without a browser.
- New logic goes in a pure module with tests in the mirrored `web/tests/unit/` path — a behavior change without a matching test is incomplete.
- Prefer tests that document behavior over tests that re-query the DOM for every assertion.

## Style

- Prettier is authoritative: **tabs**, single quotes, semicolons, print width 80, trailing commas — run `npm run format` if lint-staged or `format:check` flags style.
- ESLint rules are strict: `consistent-type-imports` (use `import type`), no unused vars (prefix intentional ones with `_`), `no-console` (only `error`/`warn` allowed).
40 changes: 40 additions & 0 deletions .clinerules/03-memory-bank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Memory Bank

I am Cline. My memory resets completely between sessions, so I rely entirely on my Memory Bank to understand this project and continue work effectively. At the start of EVERY task, read ALL memory bank files listed below — this is not optional.

## Memory Bank Structure

All files live in `memory-bank/` and build on each other:

1. `projectbrief.md` — foundation document: core requirements and goals. Source of truth for project scope. Created at project start if missing.
2. `productContext.md` — why the project exists, the problems it solves, how it should work.
3. `activeContext.md` — current work focus, recent changes, next steps, active decisions. Updated most frequently.
4. `systemPatterns.md` — system architecture, key technical decisions, design patterns, component relationships.
5. `techContext.md` — technologies, development setup, constraints, dependencies.
6. `progress.md` — what works, what is left, current status, known issues.

Additional files may be added under `memory-bank/` for complex features, integrations, API documentation, or deployment procedures.

## Workflow

- **"initialize memory bank"** — create the structure (starting from `projectbrief.md`) by analyzing the codebase.
- **"follow your custom instructions"** — read all memory bank files and continue where the last session left off.
- **"update memory bank"** — review ALL files and update them. Trigger after significant changes or milestones.

## Update Triggers

1. Discovering a new project pattern
2. After implementing significant changes
3. On explicit "update memory bank" request (review ALL files)
4. When context needs clarification to proceed

## Maintenance

The memory bank is a cache, not a journal. On every update:

- Prune `activeContext.md` aggressively — completed items are deleted, not archived. It must stay readable in seconds.
- Graduate stable, lasting knowledge into `systemPatterns.md` / `techContext.md`; session noise dies in `activeContext.md`.
- Treat edits to `projectbrief.md` as exceptional — it changes rarely.
- Delete entries that no longer hold; stale context is worse than no context.

The Memory Bank is the only link to previous work. Maintain it with precision — effectiveness depends entirely on its accuracy.
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Simple Clock

A clean, dark-themed digital clock with a built-in countdown timer. Built with TypeScript and Vite.
A clean, dark-themed digital clock with a countdown timer and alarms. Built with TypeScript and Vite.

## Repository Layout

Expand Down Expand Up @@ -44,11 +44,16 @@ web/ # Application workspace
│ │ │ ├── clock.ui.ts # Clock DOM wiring — initClock()
│ │ │ ├── clock-registry.ts # Multi-clock scaffold (not wired up yet)
│ │ │ └── index.ts # Feature public surface
│ │ └── timer/
│ │ ├── timer.core.ts # Pure state machine (no DOM)
│ │ ├── timer.presets.ts # Preset helpers (pure)
│ │ ├── timer.storage.ts # localStorage adapter
│ │ ├── timer.ui.ts # Timer DOM wiring — initTimer()
│ │ ├── timer/
│ │ │ ├── timer.core.ts # Pure state machine (no DOM)
│ │ │ ├── timer.presets.ts # Preset helpers (pure)
│ │ │ ├── timer.storage.ts # localStorage adapter
│ │ │ ├── timer.ui.ts # Timer DOM wiring — initTimer()
│ │ │ └── index.ts # Feature public surface
│ │ └── alarm/
│ │ ├── alarm.core.ts # Pure alarm state machine (no DOM)
│ │ ├── alarm.storage.ts # localStorage adapter
│ │ ├── alarm.ui.ts # Alarm DOM wiring — initAlarm()
│ │ └── index.ts # Feature public surface
│ ├── shared/
│ │ ├── time/format.ts # Time formatting (pure functions)
Expand All @@ -65,10 +70,12 @@ web/ # Application workspace
│ ├── base.css # Reset / body
│ ├── clock.css # Clock styles
│ ├── timer.css # Timer styles
│ ├── alarm.css # Alarm styles
│ ├── animations.css # Keyframes
│ └── responsive.css # Media queries
├── tests/
│ └── unit/ # Mirrors src/ one-to-one
│ ├── features/alarm/alarm.core.test.ts
│ ├── features/clock/clock.core.test.ts
│ ├── features/timer/timer.core.test.ts
│ └── shared/time/format.test.ts
Expand Down Expand Up @@ -115,6 +122,8 @@ Tests cover:
- Time formatting (12h, AM/PM, midnight, noon, duration)
- Clock tick logic (title updates, timezone display, dev marker, timer-active guard)
- Timer state machine (start/pause/reset/tick transitions, input validation, presets)
- Alarm state machine (create/enable/delete, due-tick window, snooze, one-shot firing,
persistence parsing, input validation)

## Container stacks

Expand Down
97 changes: 97 additions & 0 deletions docs/stories/US-001-multiple-alarms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# User Story: Multiple Alarms

**Story ID:** US-001
**Epic:** Alarms
**Status:** QA (QA assessment passed; code review approved — pending merge)

---

## 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. 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-<timestamp>-<rand>`). 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.

---

## 5. Definition of Done Checklist

- [x] Code implemented strictly according to ACs.

- [x] Unit/Integration tests written and passing (135 total, incl. 40 alarm tests
plus adversarial reload/snooze/boundary checks run during QA assessment).
- [x] QA assessment passed.
- [x] Code Review approved.
Loading
Loading