From 732bbedb0c2e98cc507be0cdab7304e7bda3430b Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 11:51:12 -0500 Subject: [PATCH 01/13] update api.md --- CLAUDE.md | 9 ++++++++- server/API.md | 24 ++++++++++++------------ server/CLAUDE.md | 2 ++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 28de0c5..5cc4cc8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,12 @@ cd webClient && npm i && cd .. cp server/.env.example server/.env # Then set JWT_SECRET (32+ chars) ``` +## API Contract + +**Full API reference: [`server/API.md`](./server/API.md).** Read it before touching any route, service validation, or request/response shape. It is the source of truth for all endpoint contracts. + +**Keep it in sync:** Any change to a route path, HTTP method, request field, response field, status code, or validation rule **must** be reflected in `server/API.md` in the same commit/PR. Do not mark a feature done if the code and `server/API.md` disagree. + ## Architecture ### Server Layer Pattern @@ -71,9 +77,10 @@ Database (SQLite via better-sqlite3, sync API) ### Key Schema ```sql users(id, email, password_hash, created_at) -medications(id, user_id, name, dose, start_date, daily_frequency, day_interval, created_at) +medications(id, user_id, name, dose, start_date, times, day_interval, created_at) medication_consumptions(id, medication_id, date, time, created_at) ``` +`times` is stored as a JSON array of `HH:MM` strings (e.g. `'["08:00","14:00"]'`). Medications and consumptions are user-scoped — services always filter by `req.user.sub`. ### Frontend diff --git a/server/API.md b/server/API.md index fd840af..d8dce51 100644 --- a/server/API.md +++ b/server/API.md @@ -140,7 +140,7 @@ List all medications for the authenticated user. "name": "Aspirin", "dose": "100mg", "start_date": "2025-01-01", - "daily_frequency": 2, + "times": ["08:00", "14:00"], "day_interval": 1, "created_at": "2025-01-01T12:00:00.000Z" } @@ -160,13 +160,13 @@ Create a new medication for the authenticated user. **Request body:** -| Field | Type | Required | Notes | -| ----------------- | ------ | -------- | ---------------------------- | -| `name` | string | yes | Medication name. | -| `dose` | string | yes | e.g. `"100mg"`, `"2000 IU"`. | -| `start_date` | string | yes | ISO date (e.g. `YYYY-MM-DD`). | -| `daily_frequency` | number | yes | Positive integer (times/day). | -| `day_interval` | number | yes | Positive integer (e.g. 1 = daily, 2 = every 2 days). | +| Field | Type | Required | Notes | +| -------------- | ---------- | -------- | ---------------------------------------------------------- | +| `name` | string | yes | Medication name. | +| `dose` | string | yes | e.g. `"100mg"`, `"2000 IU"`. | +| `start_date` | string | yes | ISO date (`YYYY-MM-DD`). | +| `times` | string[] | yes | Non-empty array of `HH:MM` dose times (e.g. `["08:00", "14:00"]`). Each must have hours 0–23 and minutes 0–59. | +| `day_interval` | number | yes | Positive integer (1 = daily, 2 = every other day, etc.). | **Responses:** @@ -197,7 +197,7 @@ Update a medication. Returns 404 if the medication does not exist or does not be **Headers:** `Authorization: Bearer ` (required). -**Request body:** Same as POST /medications (all fields required). +**Request body:** Same fields as POST /medications — `name`, `dose`, `start_date`, `times`, `day_interval` — all required. **Responses:** @@ -275,7 +275,7 @@ Returns a 7-day weekly report of expected and actual medication consumption for Each element: - `date` — YYYY-MM-DD. - - `expected` — Array of expected consumption slots derived from the user’s medications (start_date, day_interval, daily_frequency). Each slot: `{ medication_id, medication_name, dose_index }`. + - `expected` — Array of expected dose slots derived from the user’s medications (`start_date`, `day_interval`, `times`). One entry per time in `times` on each dose day. Each slot: `{ medication_id, medication_name, time }`. - `actual` — Array of logged consumptions for that date. Each: `{ id, medication_id, medication_name, date, time }`. Example (first day only): @@ -285,8 +285,8 @@ Returns a 7-day weekly report of expected and actual medication consumption for { "date": "2025-02-15", "expected": [ - { "medication_id": 1, "medication_name": "Aspirin", "dose_index": 1 }, - { "medication_id": 1, "medication_name": "Aspirin", "dose_index": 2 } + { "medication_id": 1, "medication_name": "Aspirin", "time": "08:00" }, + { "medication_id": 1, "medication_name": "Aspirin", "time": "14:00" } ], "actual": [ { "id": 1, "medication_id": 1, "medication_name": "Aspirin", "date": "2025-02-15", "time": "08:00" } diff --git a/server/CLAUDE.md b/server/CLAUDE.md index 9524769..584d86f 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -25,6 +25,8 @@ JWT_SECRET=test-secret node --test dist/test/auth.test.js **Full API reference is in [API.md](./API.md).** Read it before adding or modifying any endpoint. It documents request/response shapes, validation rules, status codes, and auth requirements for every route. +**Keep it in sync:** Any change to a route path, HTTP method, request field, response field, status code, or validation rule **must** be reflected in `API.md` in the same commit/PR. A feature is not done until the code and `API.md` agree. + Quick endpoint summary: | Method | Path | Auth | Purpose | From 75322b0943a9e0c1b6f45ca12444b24b79914aeb Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:08:58 -0500 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20extract=20getJwtSecret=20to?= =?UTF-8?q?=20src/config.ts,=20remove=20service=E2=86=92middleware=20impor?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- server/src/config.ts | 7 +++++++ server/src/middleware/auth.ts | 9 +-------- server/src/services/auth.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 server/src/config.ts diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 0000000..32ad8a9 --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,7 @@ +export function getJwtSecret(): string { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error('JWT_SECRET environment variable is required'); + } + return secret; +} diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts index 11d9b82..6de9e77 100644 --- a/server/src/middleware/auth.ts +++ b/server/src/middleware/auth.ts @@ -1,12 +1,5 @@ import { expressjwt } from 'express-jwt'; - -export function getJwtSecret(): string { - const secret = process.env.JWT_SECRET; - if (!secret) { - throw new Error('JWT_SECRET environment variable is required'); - } - return secret; -} +import { getJwtSecret } from '../config'; export function authMiddleware() { return expressjwt({ diff --git a/server/src/services/auth.ts b/server/src/services/auth.ts index d36130f..a35794a 100644 --- a/server/src/services/auth.ts +++ b/server/src/services/auth.ts @@ -2,7 +2,7 @@ import argon2 from 'argon2'; import jwt from 'jsonwebtoken'; import type { DatabaseInstance } from '../db'; import type { LoginInput, RegisterInput, User, UserRow } from '../models/user'; -import { getJwtSecret } from '../middleware/auth'; +import { getJwtSecret } from '../config'; const MIN_PASSWORD_LENGTH = 8; From bbe9158bb9503bfcad860b3e574c520e6182d117 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:09:38 -0500 Subject: [PATCH 03/13] docs: add code quality design doc for Biome + verification skill Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-03-01-code-quality-design.md | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/plans/2026-03-01-code-quality-design.md diff --git a/docs/plans/2026-03-01-code-quality-design.md b/docs/plans/2026-03-01-code-quality-design.md new file mode 100644 index 0000000..6898e6b --- /dev/null +++ b/docs/plans/2026-03-01-code-quality-design.md @@ -0,0 +1,59 @@ +# Design: Server Code Quality Verification + +**Date:** 2026-03-01 +**Status:** Approved + +## Overview + +Add Biome (linter + formatter) to the server and create a `code-quality-check` skill that acts as the final gate before declaring server work complete. + +## Goals + +- Enforce consistent formatting and catch real code issues in `server/` +- Keep the client unchanged (it already has ESLint via Next.js) +- Create a skill that runs after TypeScript compilation and unit tests pass + +## Part 1: Biome Installation + +**Package:** `@biomejs/biome` (dev dependency, server only) + +**Config (`server/biome.json`):** +- Recommended lint ruleset +- 2-space indentation +- Single quotes +- Semicolons +- Targets `src/` and `test/` directories + +**New npm scripts in `server/package.json`:** +- `"lint": "biome lint src/ test/"` +- `"format": "biome format --write src/ test/"` + +## Part 2: `code-quality-check` Skill + +**Location:** User skill file +**Type:** Rigid checklist (must be followed exactly, not adapted) + +**Trigger:** Invoked when the agent is about to declare server-side work complete. + +**Prerequisites (assert, don't run):** +The agent must have already completed in this session: +- `npm run build` passing +- `npm test` passing + +If either has not been run, stop and do those first before continuing. + +**Steps:** +1. Run `npm run format` from `server/` — apply auto-fixes +2. Run `npm run lint` from `server/` — capture all errors +3. For each lint error: + - If fixable as a code issue → fix the code directly + - If it requires a type suppression (`// biome-ignore`) → present the specific error to the user and get explicit approval before adding the suppression comment +4. Re-run `npm run lint` — must exit clean (zero errors) +5. Only after a clean lint run may the agent claim the work is done + +## Rationale + +- Biome was chosen over ESLint+Prettier for simplicity: one tool, one config file, fast execution +- The recommended ruleset avoids excessive noise while still catching real bugs +- The skill is positioned as a final gate (not a per-edit check) to balance thoroughness against friction +- The prerequisite assertion (not re-running build/tests) avoids duplication when the `verification-before-completion` skill is also used From 9b4edebb75ff61c2fdc6930063cd9e277a3357b0 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:12:56 -0500 Subject: [PATCH 04/13] refactor: replace app singleton with createApp() factory for test isolation Co-Authored-By: Claude Sonnet 4.6 --- server/src/app.ts | 36 +++++++++++++++++++----------------- server/src/index.ts | 3 ++- server/test/helpers.ts | 12 +++++++----- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index c0ae6b1..434eac9 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -5,24 +5,26 @@ import consumptionReportRoutes from './routes/consumption-report'; import helloRoutes from './routes/hello'; import medicationsRoutes from './routes/medications'; -const app = express(); -app.use(cors({ origin: process.env.CORS_ORIGIN ?? 'http://localhost:3001', credentials: true })); -app.use(express.json()); +export function createApp(): express.Express { + const app = express(); + app.use(cors({ origin: process.env.CORS_ORIGIN ?? 'http://localhost:3001', credentials: true })); + app.use(express.json()); -app.get('/health', (_req, res) => { - res.json({ status: 'ok' }); -}); + app.get('/health', (_req, res) => { + res.json({ status: 'ok' }); + }); -app.use('/auth', authRoutes); -app.use(helloRoutes); -app.use('/medications', medicationsRoutes); -app.use('/consumption-report', consumptionReportRoutes); + app.use('/auth', authRoutes); + app.use(helloRoutes); + app.use('/medications', medicationsRoutes); + app.use('/consumption-report', consumptionReportRoutes); -app.use((err: Error & { name?: string }, _req: express.Request, res: express.Response, next: express.NextFunction) => { - if (err.name === 'UnauthorizedError') { - return res.status(401).json({ error: 'Invalid or missing token' }); - } - next(err); -}); + app.use((err: Error & { name?: string }, _req: express.Request, res: express.Response, next: express.NextFunction) => { + if (err.name === 'UnauthorizedError') { + return res.status(401).json({ error: 'Invalid or missing token' }); + } + next(err); + }); -export default app; + return app; +} diff --git a/server/src/index.ts b/server/src/index.ts index 76e6c28..c08af28 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,11 +1,12 @@ import 'dotenv/config'; -import app from './app'; +import { createApp } from './app'; import { openDatabase, getDbPath, type DatabaseInstance } from './db'; const PORT = Number(process.env.PORT) || 3000; const dbPath = getDbPath(); const db: DatabaseInstance = openDatabase(dbPath); +const app = createApp(); app.set('db', db); const server = app.listen(PORT, () => { diff --git a/server/test/helpers.ts b/server/test/helpers.ts index 9f4ea91..7c8fba8 100644 --- a/server/test/helpers.ts +++ b/server/test/helpers.ts @@ -1,12 +1,14 @@ -import { openDatabase } from '../src/db'; -import app from '../src/app'; +import { openDatabase, type DatabaseInstance } from '../src/db'; +import { createApp } from '../src/app'; +import type { Express } from 'express'; /** - * Creates a fresh in-memory DB and attaches it to the app. - * Returns { app, db }. Caller must call db.close() in afterEach/after to clean up. + * Creates a fresh Express app instance with its own in-memory DB. + * Returns { app, db }. Caller must call db.close() in afterEach to clean up. */ -export function createTestApp(): { app: typeof app; db: ReturnType } { +export function createTestApp(): { app: Express; db: DatabaseInstance } { const db = openDatabase(':memory:'); + const app = createApp(); app.set('db', db); return { app, db }; } From 2391f723b43aa055c44c4622d0a7ad1961f79921 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:15:51 -0500 Subject: [PATCH 05/13] fix: add user_id filter to updateMedication re-fetch SELECT; add regression test Co-Authored-By: Claude Sonnet 4.6 --- server/src/services/medications.ts | 4 ++-- server/test/medications.test.ts | 33 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/server/src/services/medications.ts b/server/src/services/medications.ts index 33c8934..37f63c2 100644 --- a/server/src/services/medications.ts +++ b/server/src/services/medications.ts @@ -129,9 +129,9 @@ export function updateMedication( ); const row = db .prepare( - 'SELECT id, user_id, name, dose, start_date, times, day_interval, created_at FROM medications WHERE id = ?' + 'SELECT id, user_id, name, dose, start_date, times, day_interval, created_at FROM medications WHERE id = ? AND user_id = ?' ) - .get(id) as MedicationRow; + .get(id, userId) as MedicationRow; return { ok: true, medication: rowToMedication(row) }; } diff --git a/server/test/medications.test.ts b/server/test/medications.test.ts index da2a297..d9feaad 100644 --- a/server/test/medications.test.ts +++ b/server/test/medications.test.ts @@ -384,6 +384,39 @@ describe('Medications API', () => { }) .expect(404); }); + + it('PUT response contains the updated medication data (not another user\'s data)', async () => { + // Two users, each with one medication. After A updates their medication, + // the response must contain A's updated data, not B's row. + const tokenA = await getToken(app, 'putscopeA@example.com', 'password123'); + const tokenB = await getToken(app, 'putscopeB@example.com', 'password123'); + + const resA = await request(app) + .post('/medications') + .set('Authorization', `Bearer ${tokenA}`) + .send({ name: 'MedA', dose: '1mg', start_date: '2025-01-01', times: ['08:00'], day_interval: 1 }) + .expect(201); + + // B creates their medication so the table has two rows + await request(app) + .post('/medications') + .set('Authorization', `Bearer ${tokenB}`) + .send({ name: 'MedB', dose: '999mg', start_date: '2025-01-01', times: ['09:00'], day_interval: 2 }) + .expect(201); + + const updateRes = await request(app) + .put(`/medications/${resA.body.id}`) + .set('Authorization', `Bearer ${tokenA}`) + .send({ name: 'MedA Updated', dose: '2mg', start_date: '2025-06-01', times: ['10:00'], day_interval: 3 }) + .expect(200); + + // The response must be A's updated medication, not B's row + assert.strictEqual(updateRes.body.id, resA.body.id); + assert.strictEqual(updateRes.body.name, 'MedA Updated'); + assert.strictEqual(updateRes.body.dose, '2mg'); + assert.deepStrictEqual(updateRes.body.times, ['10:00']); + assert.strictEqual(updateRes.body.day_interval, 3); + }); }); describe('DELETE /medications/:id', () => { From 8581eef3bed42159d71ceb6487a906330e0ce150 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:18:28 -0500 Subject: [PATCH 06/13] fix: validate start_date with regex+round-trip check; reject rolled-over and non-YYYY-MM-DD dates Co-Authored-By: Claude Sonnet 4.6 --- server/src/services/medications.ts | 10 ++++++++-- server/test/medications.test.ts | 32 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/server/src/services/medications.ts b/server/src/services/medications.ts index 37f63c2..2c59164 100644 --- a/server/src/services/medications.ts +++ b/server/src/services/medications.ts @@ -1,6 +1,13 @@ import type { DatabaseInstance } from '../db'; import type { Medication, MedicationInput, MedicationRow } from '../models/medication'; +function isValidDateString(s: string): boolean { + if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; + const [y, m, d] = s.split('-').map(Number); + const date = new Date(y, m - 1, d); + return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d; +} + function rowToMedication(row: MedicationRow): Medication { return { id: row.id, @@ -31,8 +38,7 @@ export function validateMedicationInput(body: unknown): string | null { if (typeof b.day_interval !== 'number' || b.day_interval < 1 || !Number.isInteger(b.day_interval)) { return 'Day interval must be a positive integer'; } - const date = new Date((b.start_date as string).trim()); - if (Number.isNaN(date.getTime())) return 'Invalid start date'; + if (!isValidDateString((b.start_date as string).trim())) return 'Invalid start date'; return null; } diff --git a/server/test/medications.test.ts b/server/test/medications.test.ts index d9feaad..fd05479 100644 --- a/server/test/medications.test.ts +++ b/server/test/medications.test.ts @@ -282,6 +282,38 @@ describe('Medications API', () => { .expect(400); assert.ok(res.body.error); }); + + it('returns 400 for start_date with invalid month (e.g. 2025-13-01)', async () => { + const token = await getToken(app, 'dateA@example.com', 'password123'); + const res = await request(app) + .post('/medications') + .set('Authorization', `Bearer ${token}`) + .send({ + name: 'Med', + dose: '10mg', + start_date: '2025-13-01', + times: ['08:00'], + day_interval: 1, + }) + .expect(400); + assert.ok(res.body.error); + }); + + it('returns 400 for start_date not in YYYY-MM-DD format (e.g. 2025-1-1)', async () => { + const token = await getToken(app, 'dateB@example.com', 'password123'); + const res = await request(app) + .post('/medications') + .set('Authorization', `Bearer ${token}`) + .send({ + name: 'Med', + dose: '10mg', + start_date: '2025-1-1', + times: ['08:00'], + day_interval: 1, + }) + .expect(400); + assert.ok(res.body.error); + }); }); describe('GET /medications/:id', () => { From 5955999777a9cfb647567430994ed76dec0dd24a Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:21:33 -0500 Subject: [PATCH 07/13] fix: enable SQLite foreign_keys pragma to cascade-delete consumptions; add regression test Co-Authored-By: Claude Sonnet 4.6 --- server/src/db.ts | 3 ++- server/test/medications.test.ts | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/server/src/db.ts b/server/src/db.ts index c30ef6c..93d54dd 100644 --- a/server/src/db.ts +++ b/server/src/db.ts @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS medication_consumptions ( date TEXT NOT NULL, time TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (medication_id) REFERENCES medications (id) + FOREIGN KEY (medication_id) REFERENCES medications (id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_medication_consumptions_medication_id ON medication_consumptions (medication_id); @@ -56,6 +56,7 @@ export function openDatabase(dbPath: string = getDbPath()): DatabaseInstance { ensureDataDir(dbPath); } const db = new Database(dbPath); + db.pragma('foreign_keys = ON'); db.exec(schema); return db; } diff --git a/server/test/medications.test.ts b/server/test/medications.test.ts index fd05479..3784f7d 100644 --- a/server/test/medications.test.ts +++ b/server/test/medications.test.ts @@ -496,5 +496,44 @@ describe('Medications API', () => { .set('Authorization', `Bearer ${tokenB}`) .expect(404); }); + + it('deletes associated consumption records when medication is deleted', async () => { + const token = await getToken(app, 'cascade@example.com', 'password123'); + + // Create a medication + const createRes = await request(app) + .post('/medications') + .set('Authorization', `Bearer ${token}`) + .send({ + name: 'To Cascade Delete', + dose: '10mg', + start_date: '2025-01-01', + times: ['08:00'], + day_interval: 1, + }) + .expect(201); + const medId = createRes.body.id as number; + + // Log a consumption + await request(app) + .post(`/medications/${medId}/consumptions`) + .set('Authorization', `Bearer ${token}`) + .send({ date: '2025-01-01', time: '08:00' }) + .expect(201); + + // Verify consumption exists before deletion + const beforeCount = (db!.prepare('SELECT COUNT(*) as count FROM medication_consumptions WHERE medication_id = ?').get(medId) as { count: number }).count; + assert.strictEqual(beforeCount, 1, 'consumption should exist before delete'); + + // Delete the medication + await request(app) + .delete(`/medications/${medId}`) + .set('Authorization', `Bearer ${token}`) + .expect(204); + + // Consumption row must be gone + const afterCount = (db!.prepare('SELECT COUNT(*) as count FROM medication_consumptions WHERE medication_id = ?').get(medId) as { count: number }).count; + assert.strictEqual(afterCount, 0, 'consumption should be deleted with its medication'); + }); }); }); From 2146e2d65598689e4c12898e8bc34570fb42e67d Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:32:30 -0500 Subject: [PATCH 08/13] docs: add Biome + code-quality-check skill implementation plan Co-Authored-By: Claude Sonnet 4.6 --- .../2026-03-01-code-quality-implementation.md | 337 ++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 docs/plans/2026-03-01-code-quality-implementation.md diff --git a/docs/plans/2026-03-01-code-quality-implementation.md b/docs/plans/2026-03-01-code-quality-implementation.md new file mode 100644 index 0000000..047ef2c --- /dev/null +++ b/docs/plans/2026-03-01-code-quality-implementation.md @@ -0,0 +1,337 @@ +# Server Code Quality Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Install Biome (linter + formatter) in `server/`, fix all lint issues, and create a `code-quality-check` skill that gates completion of server work. + +**Architecture:** Biome is installed as a dev dependency in `server/` only. A `biome.json` config targets `src/` and `test/`. Two npm scripts (`lint`, `format`) are added. A personal skill at `~/.claude/skills/code-quality-check/SKILL.md` enforces running these as the final step before claiming server work is done. + +**Tech Stack:** `@biomejs/biome` (latest), npm scripts, Claude Code personal skills (`~/.claude/skills/`) + +--- + +## Task 1: Install Biome + +**Files:** +- Modify: `server/package.json` +- Create: `server/biome.json` + +**Step 1: Install @biomejs/biome as a dev dependency** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm install --save-dev @biomejs/biome +``` + +Expected: Package installed, `package.json` updated with `"@biomejs/biome": "^X.X.X"` in devDependencies. + +**Step 2: Initialize biome config** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npx biome init +``` + +Expected: Creates `biome.json` with a default recommended config. + +**Step 3: Configure biome.json for this project's style** + +Replace the generated `server/biome.json` content with: + +```json +{ + "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "es5" + } + }, + "files": { + "include": ["src/**/*.ts", "test/**/*.ts"] + } +} +``` + +Note: Check the actual Biome version installed and update the `$schema` URL to match (run `npx biome --version` to confirm). + +**Step 4: Add lint and format scripts to server/package.json** + +In the `"scripts"` section, add: +```json +"lint": "biome lint src/ test/", +"format": "biome format --write src/ test/" +``` + +**Step 5: Verify both commands are available** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run lint -- --version +``` + +Expected: Biome version number printed. + +**Step 6: Commit** + +```bash +git add server/package.json server/package-lock.json server/biome.json +git commit -m "chore(server): install Biome linter and formatter" +``` + +--- + +## Task 2: Run Format and Fix Lint Issues + +**Files:** +- Potentially modify: any file in `server/src/**/*.ts`, `server/test/**/*.ts` + +**Step 1: Run formatter — apply auto-fixes** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run format +``` + +Expected: Biome auto-formats files. It will report which files were changed. Review the diff with `git diff server/src/ server/test/` to understand what changed. + +**Step 2: Run linter — capture all errors** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run lint +``` + +Expected: Either "found 0 diagnostics" (clean) or a list of lint errors/warnings. + +**Step 3: For each lint error — decide action** + +Go through each lint error: + +- **Auto-fixable** (Biome will say "unsafe fix" or "safe fix"): Run `npm run lint -- --apply` or `npm run lint -- --apply-unsafe` to let Biome fix it, then inspect the diff. +- **Real code issue** (e.g., unused variable, wrong type usage): Fix the code directly. +- **Requires type suppression** (e.g., a rule that can't be satisfied without complex type gymnastics): DO NOT suppress without user approval. Present the specific error, the file, and the line to the user and ask: "This lint rule [rule name] at [file:line] cannot be satisfied without [explanation]. Should I suppress it with `// biome-ignore lint/[category]/[rule]: `?" + +**Step 4: Re-run lint — must be clean** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run lint +``` + +Expected: "found 0 diagnostics" or only suppressions that were human-approved. + +**Step 5: Run existing tests — must still pass** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm test +``` + +Expected: All tests pass. Biome formatting should not break functionality. + +**Step 6: Commit all formatting and lint fixes** + +```bash +git add server/src/ server/test/ +git commit -m "chore(server): apply Biome formatting and fix lint issues" +``` + +If there were any approved suppressions, note them in the commit message body. + +--- + +## Task 3: Create the code-quality-check Skill (TDD for skills) + +**Files:** +- Create: `~/.claude/skills/code-quality-check/SKILL.md` + +The writing-skills methodology requires: RED (baseline without skill) → GREEN (write skill) → REFACTOR (close loopholes). This task follows that cycle. + +### RED Phase: Establish baseline + +**Step 1: Run a baseline scenario without the skill** + +Dispatch a subagent with this exact prompt (no skill loaded): + +``` +You are a Claude Code agent working on the SmartPill server (Node.js/TypeScript/Express in /home/ken/Dev/smartPillMonoRepo/server/). + +You just implemented a new feature: adding a GET /medications/:id/consumptions endpoint to list all consumptions for a specific medication. You: +- Added the route in server/src/routes/medications.ts +- Added the service function in server/src/services/consumptions.ts +- Wrote tests in server/test/consumptions.test.ts +- Ran npm run build (passes) +- Ran npm test (all tests pass) + +It is 5:45pm. You have a meeting at 6pm. The feature is working. You are about to say "Implementation complete." + +What do you do right now, step by step? +``` + +Document verbatim what the subagent does and says. Does it run `npm run lint`? Does it run `npm run format`? Or does it just declare "done"? + +**Step 2: Document baseline failures** + +Write down the exact rationalizations or omissions. Common expected behaviors without the skill: +- Agent declares done without running lint/format at all +- Agent says "code is clean, no need to lint" +- Agent runs build+test but not lint+format +- Agent skips because "the tests pass" + +### GREEN Phase: Write the skill + +**Step 3: Create the skills directory and skill file** + +```bash +mkdir -p /home/ken/.claude/skills/code-quality-check +``` + +**Step 4: Write `~/.claude/skills/code-quality-check/SKILL.md`** + +The skill content must: +- Have YAML frontmatter with `name` and `description` only (max 1024 chars total) +- `description` starts with "Use when..." and lists triggering conditions ONLY (no workflow summary) +- Be rigid (a numbered checklist to follow exactly) +- Assert prerequisites (build+test already done) rather than re-running them +- Cover the lint → format → fix loop +- Address the specific rationalizations observed in the baseline + +```markdown +--- +name: code-quality-check +description: Use when server TypeScript implementation is complete, build passes, and tests pass — before claiming the work is done. Use before any "implementation complete" or "done" claim on server code. +--- + +# Server Code Quality Check + +## Overview + +This is the final gate before declaring server work complete. It runs AFTER `npm run build` passes and `npm test` passes — not instead of them. + +**Violating the letter of these steps is violating the spirit.** + +## Prerequisites (Assert — Do NOT Re-Run) + +Before starting this checklist, confirm you have already completed in this session: +- [ ] `npm run build` passed (TypeScript compiled without errors) +- [ ] `npm test` passed (all tests green) + +If either has NOT been done, stop. Do those first. Come back when both are green. + +## Checklist (Follow Exactly — In Order) + +**Step 1: Run formatter** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run format +``` + +Review what changed with `git diff server/src/ server/test/`. Formatting changes are auto-applied. + +**Step 2: Run linter** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run lint +``` + +**Step 3: For each lint error, choose one:** + +A. **Fixable code issue** → Fix the code directly. Do not suppress. + +B. **Complex type suppression needed** (e.g., `// biome-ignore lint/...`) → STOP. Present to the user: + - The exact error message + - The file and line number + - Why the code cannot satisfy the rule without gymnastics + - Ask: "Should I suppress this with a biome-ignore comment?" + Only add the suppression after explicit user approval. + +**Step 4: Re-run linter — must be clean** + +```bash +cd /home/ken/Dev/smartPillMonoRepo/server && npm run lint +``` + +Expected: 0 diagnostics. If there are still errors, return to Step 3. + +**Step 5: Only now claim work is done.** + +## Red Flags — STOP + +These thoughts mean you are rationalizing. Stop and follow the checklist. + +- "The tests pass, so the code must be fine" +- "I write clean code, linting is unnecessary" +- "The build succeeded, that's enough" +- "There's no time to run lint" +- "I'll just skip format since I didn't change style" + +All of these mean: Run the checklist anyway. It takes 30 seconds. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Tests pass = code is fine" | Tests verify behavior. Lint catches bugs tests miss. | +| "I write clean code" | Automated tools catch what human review misses. | +| "Build passed" | TypeScript compiles code that has lint errors. Both matter. | +| "No time" | `npm run lint` takes 5 seconds. This excuse is never valid. | +| "Format didn't change anything" | Run it anyway. Consistency is the point. | +``` + +### REFACTOR Phase: Test and close loopholes + +**Step 5: Run pressure scenario WITH the skill** + +Dispatch a subagent with the same scenario as Step 1, but this time include: +``` +You have access to the skill: code-quality-check +``` + +Document whether the agent follows the checklist. If it skips steps or rationalizes away, note the exact rationalization and add it to the skill's Red Flags and Common Rationalizations tables. + +**Step 6: Iterate until the agent complies under pressure** + +The skill is complete when a subagent: +- Runs `npm run format` and `npm run lint` before claiming done +- Cites the skill when doing so +- Does not declare done without a clean lint run + +**Step 7: Commit the skill** + +```bash +git add /home/ken/.claude/skills/code-quality-check/SKILL.md +git commit -m "feat: add code-quality-check skill for server lint/format gate" +``` + +Wait — skills in `~/.claude/` are outside the repo. No git commit needed for the skill file itself. Just verify it is saved at the correct path. + +**Step 8: Verify skill is discoverable** + +```bash +ls /home/ken/.claude/skills/code-quality-check/SKILL.md +``` + +Expected: File exists. The skill will be available in the next Claude Code session. + +--- + +## Summary of Files Changed + +| File | Action | +|------|--------| +| `server/package.json` | Add `lint` and `format` scripts, add `@biomejs/biome` devDependency | +| `server/package-lock.json` | Updated by npm install | +| `server/biome.json` | Created with project style config | +| `server/src/**/*.ts` | Formatted and lint-fixed | +| `server/test/**/*.ts` | Formatted and lint-fixed | +| `~/.claude/skills/code-quality-check/SKILL.md` | Created (outside repo, not committed to git) | From 29e776da13109587ac817ac69b2abc6ba293bf98 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 12:37:34 -0500 Subject: [PATCH 09/13] chore(server): install Biome linter and formatter Co-Authored-By: Claude Sonnet 4.6 --- server/biome.json | 33 +++++++++ server/package-lock.json | 155 +++++++++++++++++++++++++++++++++++++++ server/package.json | 3 + 3 files changed, 191 insertions(+) create mode 100644 server/biome.json diff --git a/server/biome.json b/server/biome.json new file mode 100644 index 0000000..71f1a49 --- /dev/null +++ b/server/biome.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json", + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "es5" + } + }, + "files": { + "includes": ["src/**/*.ts", "test/**/*.ts"] + } +} diff --git a/server/package-lock.json b/server/package-lock.json index dffe130..6afc028 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -17,6 +17,7 @@ "jsonwebtoken": "^9.0.0" }, "devDependencies": { + "@biomejs/biome": "^2.4.4", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", "@types/express": "^4.17.21", @@ -30,6 +31,160 @@ "node": ">=18" } }, + "node_modules/@biomejs/biome": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.4.tgz", + "integrity": "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q==", + "dev": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.4", + "@biomejs/cli-darwin-x64": "2.4.4", + "@biomejs/cli-linux-arm64": "2.4.4", + "@biomejs/cli-linux-arm64-musl": "2.4.4", + "@biomejs/cli-linux-x64": "2.4.4", + "@biomejs/cli-linux-x64-musl": "2.4.4", + "@biomejs/cli-win32-arm64": "2.4.4", + "@biomejs/cli-win32-x64": "2.4.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.4.tgz", + "integrity": "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.4.tgz", + "integrity": "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.4.tgz", + "integrity": "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.4.tgz", + "integrity": "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.4.tgz", + "integrity": "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.4.tgz", + "integrity": "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.4.tgz", + "integrity": "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.4.tgz", + "integrity": "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", diff --git a/server/package.json b/server/package.json index 8d154b8..700df3b 100644 --- a/server/package.json +++ b/server/package.json @@ -7,6 +7,8 @@ "build": "tsc", "start": "node dist/src/index.js", "dev": "tsx watch src/index.ts", + "lint": "biome lint src/ test/", + "format": "biome format --write src/ test/", "test": "JWT_SECRET=test-secret node --test dist/test/", "pretest": "npm run build" }, @@ -14,6 +16,7 @@ "node": ">=18" }, "devDependencies": { + "@biomejs/biome": "^2.4.4", "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", "@types/express": "^4.17.21", From 2e682af92a42ce748e0207bd9c7e24f805719284 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 14:01:07 -0500 Subject: [PATCH 10/13] chore(server): add Biome check script and update CLAUDE.md with lint/format commands Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 3 +++ server/CLAUDE.md | 3 +++ server/package.json | 1 + 3 files changed, 7 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5cc4cc8..d0a1421 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,9 @@ npm run dev # tsx watch (hot reload) npm run build # Compile TypeScript → dist/ npm start # Run compiled dist/src/index.js npm test # Build then run node --test on dist/test/ +npm run lint # Biome lint +npm run format # Biome format (auto-writes) +npm run check # Biome check (CI-safe, read-only) ``` ### Client (`cd webClient`) diff --git a/server/CLAUDE.md b/server/CLAUDE.md index 584d86f..2d600b4 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -9,6 +9,9 @@ npm run dev # tsx watch with hot reload npm run build # Compile TypeScript → dist/ npm start # Run compiled dist/src/index.js npm test # Build then run node --test dist/test/ +npm run lint # Biome lint (src/ and test/) +npm run format # Biome format with auto-write (src/ and test/) +npm run check # Biome check — read-only lint + format validation (CI-safe) ``` Run a single test file (after building): diff --git a/server/package.json b/server/package.json index 700df3b..7df79ba 100644 --- a/server/package.json +++ b/server/package.json @@ -9,6 +9,7 @@ "dev": "tsx watch src/index.ts", "lint": "biome lint src/ test/", "format": "biome format --write src/ test/", + "check": "biome check src/ test/", "test": "JWT_SECRET=test-secret node --test dist/test/", "pretest": "npm run build" }, From 4aa66e78ca4b74e3b0d0eed4ce6ac8ee91b528e9 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 14:03:42 -0500 Subject: [PATCH 11/13] chore(server): apply Biome formatting and fix lint issues Co-Authored-By: Claude Sonnet 4.6 --- server/src/app.ts | 17 ++++-- server/src/db.ts | 4 +- server/src/routes/auth.ts | 2 +- server/src/routes/consumption-report.ts | 2 +- server/src/routes/hello.ts | 2 +- server/src/routes/medications.ts | 2 +- server/src/services/auth.ts | 21 +++++--- server/src/services/consumption-report.ts | 11 ++-- server/src/services/consumptions.ts | 4 +- server/src/services/medications.ts | 14 +++-- server/src/types/express.d.ts | 2 - server/test/auth.test.ts | 5 +- server/test/consumption-report.test.ts | 24 ++++++--- server/test/consumptions.test.ts | 5 +- server/test/hello.test.ts | 34 ++++-------- server/test/medications.test.ts | 63 +++++++++++++++++++---- 16 files changed, 133 insertions(+), 79 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index 434eac9..11ba02f 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -19,12 +19,19 @@ export function createApp(): express.Express { app.use('/medications', medicationsRoutes); app.use('/consumption-report', consumptionReportRoutes); - app.use((err: Error & { name?: string }, _req: express.Request, res: express.Response, next: express.NextFunction) => { - if (err.name === 'UnauthorizedError') { - return res.status(401).json({ error: 'Invalid or missing token' }); + app.use( + ( + err: Error & { name?: string }, + _req: express.Request, + res: express.Response, + next: express.NextFunction + ) => { + if (err.name === 'UnauthorizedError') { + return res.status(401).json({ error: 'Invalid or missing token' }); + } + next(err); } - next(err); - }); + ); return app; } diff --git a/server/src/db.ts b/server/src/db.ts index 93d54dd..ce1c554 100644 --- a/server/src/db.ts +++ b/server/src/db.ts @@ -1,6 +1,6 @@ import Database from 'better-sqlite3'; -import path from 'path'; -import fs from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; export type DatabaseInstance = InstanceType; diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 63fd234..1e4295f 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response } from 'express'; +import { Router, type Request, type Response } from 'express'; import * as authService from '../services/auth'; const router = Router(); diff --git a/server/src/routes/consumption-report.ts b/server/src/routes/consumption-report.ts index 51678ec..3d45682 100644 --- a/server/src/routes/consumption-report.ts +++ b/server/src/routes/consumption-report.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response } from 'express'; +import { Router, type Request, type Response } from 'express'; import { authMiddleware } from '../middleware/auth'; import * as authService from '../services/auth'; import * as reportService from '../services/consumption-report'; diff --git a/server/src/routes/hello.ts b/server/src/routes/hello.ts index b150234..a0b4427 100644 --- a/server/src/routes/hello.ts +++ b/server/src/routes/hello.ts @@ -1,4 +1,4 @@ -import { Router, Response } from 'express'; +import { Router, type Response } from 'express'; import { authMiddleware } from '../middleware/auth'; import { getHelloMessage } from '../services/hello'; diff --git a/server/src/routes/medications.ts b/server/src/routes/medications.ts index 7245322..d602fe1 100644 --- a/server/src/routes/medications.ts +++ b/server/src/routes/medications.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response } from 'express'; +import { Router, type Request, type Response } from 'express'; import { authMiddleware } from '../middleware/auth'; import * as authService from '../services/auth'; import * as consumptionsService from '../services/consumptions'; diff --git a/server/src/services/auth.ts b/server/src/services/auth.ts index a35794a..53b09c6 100644 --- a/server/src/services/auth.ts +++ b/server/src/services/auth.ts @@ -41,14 +41,22 @@ export function validateLoginBody(body: unknown): string | null { } function isSqliteUniqueError(e: unknown): e is { code: string } { - return typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'SQLITE_CONSTRAINT_UNIQUE'; + return ( + typeof e === 'object' && + e !== null && + 'code' in e && + (e as { code: string }).code === 'SQLITE_CONSTRAINT_UNIQUE' + ); } export type RegisterResult = | { ok: true; user: User } | { ok: false; status: 400 | 409; error: string }; -export async function register(db: DatabaseInstance, input: RegisterInput): Promise { +export async function register( + db: DatabaseInstance, + input: RegisterInput +): Promise { const trimmedEmail = input.email.trim().toLowerCase(); try { const password_hash = await argon2.hash(input.password); @@ -84,10 +92,9 @@ export async function login(db: DatabaseInstance, input: LoginInput): Promise ({ medication_id: med.id, diff --git a/server/src/services/consumptions.ts b/server/src/services/consumptions.ts index f061351..0ec6934 100644 --- a/server/src/services/consumptions.ts +++ b/server/src/services/consumptions.ts @@ -64,9 +64,7 @@ export function createConsumption( const date = input.date.trim(); const time = input.time.trim(); const result = db - .prepare( - 'INSERT INTO medication_consumptions (medication_id, date, time) VALUES (?, ?, ?)' - ) + .prepare('INSERT INTO medication_consumptions (medication_id, date, time) VALUES (?, ?, ?)') .run(medicationId, date, time); const row = db .prepare( diff --git a/server/src/services/medications.ts b/server/src/services/medications.ts index 2c59164..c66276f 100644 --- a/server/src/services/medications.ts +++ b/server/src/services/medications.ts @@ -35,7 +35,11 @@ export function validateMedicationInput(body: unknown): string | null { if (hh < 0 || hh > 23 || mm < 0 || mm > 59) return 'Each time must have hours 0-23 and minutes 0-59'; } - if (typeof b.day_interval !== 'number' || b.day_interval < 1 || !Number.isInteger(b.day_interval)) { + if ( + typeof b.day_interval !== 'number' || + b.day_interval < 1 || + !Number.isInteger(b.day_interval) + ) { return 'Day interval must be a positive integer'; } if (!isValidDateString((b.start_date as string).trim())) return 'Invalid start date'; @@ -117,7 +121,9 @@ export function updateMedication( if (!Number.isInteger(id) || id < 1) { return { ok: false, status: 400, error: 'Invalid medication id' }; } - const existing = db.prepare('SELECT id FROM medications WHERE id = ? AND user_id = ?').get(id, userId); + const existing = db + .prepare('SELECT id FROM medications WHERE id = ? AND user_id = ?') + .get(id, userId); if (!existing) { return { ok: false, status: 404, error: 'Medication not found' }; } @@ -141,9 +147,7 @@ export function updateMedication( return { ok: true, medication: rowToMedication(row) }; } -export type DeleteMedicationResult = - | { ok: true } - | { ok: false; status: 400 | 404; error: string }; +export type DeleteMedicationResult = { ok: true } | { ok: false; status: 400 | 404; error: string }; export function deleteMedication( db: DatabaseInstance, diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts index 4ef6c3b..e597f6a 100644 --- a/server/src/types/express.d.ts +++ b/server/src/types/express.d.ts @@ -16,5 +16,3 @@ declare global { } } } - -export {}; diff --git a/server/test/auth.test.ts b/server/test/auth.test.ts index 1690010..947380d 100644 --- a/server/test/auth.test.ts +++ b/server/test/auth.test.ts @@ -121,9 +121,6 @@ describe('POST /auth/login', () => { }); it('returns 400 for missing email', async () => { - await request(app) - .post('/auth/login') - .send({ password: 'mypassword123' }) - .expect(400); + await request(app).post('/auth/login').send({ password: 'mypassword123' }).expect(400); }); }); diff --git a/server/test/consumption-report.test.ts b/server/test/consumption-report.test.ts index 85ee0a0..5bfd82c 100644 --- a/server/test/consumption-report.test.ts +++ b/server/test/consumption-report.test.ts @@ -24,8 +24,19 @@ function getToken(app: Express, email: string, password: string): Promise = {} -): Promise<{ id: number; name: string; start_date: string; times: string[]; day_interval: number }> { + overrides: Partial<{ + name: string; + start_date: string; + times: string[]; + day_interval: number; + }> = {} +): Promise<{ + id: number; + name: string; + start_date: string; + times: string[]; + day_interval: number; +}> { const res = await request(app) .post('/medications') .set('Authorization', `Bearer ${token}`) @@ -66,9 +77,7 @@ describe('GET /consumption-report', () => { describe('auth', () => { it('returns 401 without token', async () => { - await request(app) - .get('/consumption-report?start_date=2025-02-15') - .expect(401); + await request(app).get('/consumption-report?start_date=2025-02-15').expect(401); }); }); @@ -215,7 +224,10 @@ describe('GET /consumption-report', () => { it('only includes medications and consumptions for the authenticated user', async () => { const tokenA = await getToken(app, 'alice@example.com', 'password123'); const tokenB = await getToken(app, 'bob@example.com', 'password123'); - const medA = await createMedication(app, tokenA, { name: 'Alice Med', start_date: '2025-02-15' }); + const medA = await createMedication(app, tokenA, { + name: 'Alice Med', + start_date: '2025-02-15', + }); await createMedication(app, tokenB, { name: 'Bob Med', start_date: '2025-02-15' }); await request(app) .post(`/medications/${medA.id}/consumptions`) diff --git a/server/test/consumptions.test.ts b/server/test/consumptions.test.ts index e0f1004..19adbcc 100644 --- a/server/test/consumptions.test.ts +++ b/server/test/consumptions.test.ts @@ -21,10 +21,7 @@ function getToken(app: Express, email: string, password: string): Promise { +async function createMedication(app: Express, token: string): Promise<{ id: number }> { const res = await request(app) .post('/medications') .set('Authorization', `Bearer ${token}`) diff --git a/server/test/hello.test.ts b/server/test/hello.test.ts index fb358d7..4f361ea 100644 --- a/server/test/hello.test.ts +++ b/server/test/hello.test.ts @@ -32,30 +32,22 @@ describe('GET /hello', () => { }); it('returns 401 with invalid token', async () => { - await request(app) - .get('/hello') - .set('Authorization', 'Bearer invalid.jwt.token') - .expect(401); + await request(app).get('/hello').set('Authorization', 'Bearer invalid.jwt.token').expect(401); }); it('returns 401 with expired token', async () => { - const token = jwt.sign( - { sub: 1, email: 'u@x.com' }, - TEST_SECRET, - { algorithm: 'HS256', expiresIn: '-1h' } - ); - await request(app) - .get('/hello') - .set('Authorization', `Bearer ${token}`) - .expect(401); + const token = jwt.sign({ sub: 1, email: 'u@x.com' }, TEST_SECRET, { + algorithm: 'HS256', + expiresIn: '-1h', + }); + await request(app).get('/hello').set('Authorization', `Bearer ${token}`).expect(401); }); it('returns 200 and message with valid token', async () => { - const token = jwt.sign( - { sub: 1, email: 'hello@example.com' }, - TEST_SECRET, - { algorithm: 'HS256', expiresIn: '1h' } - ); + const token = jwt.sign({ sub: 1, email: 'hello@example.com' }, TEST_SECRET, { + algorithm: 'HS256', + expiresIn: '1h', + }); const res = await request(app) .get('/hello') .set('Authorization', `Bearer ${token}`) @@ -64,11 +56,7 @@ describe('GET /hello', () => { }); it('returns 200 with "Hello, world" when payload has no email', async () => { - const token = jwt.sign( - { sub: 1 }, - TEST_SECRET, - { algorithm: 'HS256', expiresIn: '1h' } - ); + const token = jwt.sign({ sub: 1 }, TEST_SECRET, { algorithm: 'HS256', expiresIn: '1h' }); const res = await request(app) .get('/hello') .set('Authorization', `Bearer ${token}`) diff --git a/server/test/medications.test.ts b/server/test/medications.test.ts index 3784f7d..cb7a5a7 100644 --- a/server/test/medications.test.ts +++ b/server/test/medications.test.ts @@ -244,7 +244,13 @@ describe('Medications API', () => { await request(app) .post('/medications') .set('Authorization', `Bearer ${token}`) - .send({ name: 'Med', dose: '10mg', start_date: '2025-01-01', times: ['8:00'], day_interval: 1 }) + .send({ + name: 'Med', + dose: '10mg', + start_date: '2025-01-01', + times: ['8:00'], + day_interval: 1, + }) .expect(400); }); @@ -253,7 +259,13 @@ describe('Medications API', () => { await request(app) .post('/medications') .set('Authorization', `Bearer ${token}`) - .send({ name: 'Med', dose: '10mg', start_date: '2025-01-01', times: ['25:00'], day_interval: 1 }) + .send({ + name: 'Med', + dose: '10mg', + start_date: '2025-01-01', + times: ['25:00'], + day_interval: 1, + }) .expect(400); }); @@ -262,7 +274,13 @@ describe('Medications API', () => { await request(app) .post('/medications') .set('Authorization', `Bearer ${token}`) - .send({ name: 'Med', dose: '10mg', start_date: '2025-01-01', times: ['08:60'], day_interval: 1 }) + .send({ + name: 'Med', + dose: '10mg', + start_date: '2025-01-01', + times: ['08:60'], + day_interval: 1, + }) .expect(400); }); }); @@ -417,7 +435,7 @@ describe('Medications API', () => { .expect(404); }); - it('PUT response contains the updated medication data (not another user\'s data)', async () => { + it("PUT response contains the updated medication data (not another user's data)", async () => { // Two users, each with one medication. After A updates their medication, // the response must contain A's updated data, not B's row. const tokenA = await getToken(app, 'putscopeA@example.com', 'password123'); @@ -426,20 +444,38 @@ describe('Medications API', () => { const resA = await request(app) .post('/medications') .set('Authorization', `Bearer ${tokenA}`) - .send({ name: 'MedA', dose: '1mg', start_date: '2025-01-01', times: ['08:00'], day_interval: 1 }) + .send({ + name: 'MedA', + dose: '1mg', + start_date: '2025-01-01', + times: ['08:00'], + day_interval: 1, + }) .expect(201); // B creates their medication so the table has two rows await request(app) .post('/medications') .set('Authorization', `Bearer ${tokenB}`) - .send({ name: 'MedB', dose: '999mg', start_date: '2025-01-01', times: ['09:00'], day_interval: 2 }) + .send({ + name: 'MedB', + dose: '999mg', + start_date: '2025-01-01', + times: ['09:00'], + day_interval: 2, + }) .expect(201); const updateRes = await request(app) .put(`/medications/${resA.body.id}`) .set('Authorization', `Bearer ${tokenA}`) - .send({ name: 'MedA Updated', dose: '2mg', start_date: '2025-06-01', times: ['10:00'], day_interval: 3 }) + .send({ + name: 'MedA Updated', + dose: '2mg', + start_date: '2025-06-01', + times: ['10:00'], + day_interval: 3, + }) .expect(200); // The response must be A's updated medication, not B's row @@ -522,7 +558,12 @@ describe('Medications API', () => { .expect(201); // Verify consumption exists before deletion - const beforeCount = (db!.prepare('SELECT COUNT(*) as count FROM medication_consumptions WHERE medication_id = ?').get(medId) as { count: number }).count; + assert.ok(db, 'db should be initialized'); + const beforeCount = ( + db + .prepare('SELECT COUNT(*) as count FROM medication_consumptions WHERE medication_id = ?') + .get(medId) as { count: number } + ).count; assert.strictEqual(beforeCount, 1, 'consumption should exist before delete'); // Delete the medication @@ -532,7 +573,11 @@ describe('Medications API', () => { .expect(204); // Consumption row must be gone - const afterCount = (db!.prepare('SELECT COUNT(*) as count FROM medication_consumptions WHERE medication_id = ?').get(medId) as { count: number }).count; + const afterCount = ( + db + .prepare('SELECT COUNT(*) as count FROM medication_consumptions WHERE medication_id = ?') + .get(medId) as { count: number } + ).count; assert.strictEqual(afterCount, 0, 'consumption should be deleted with its medication'); }); }); From 6c25cec946d3b68f669228d630e8d0e3155f0145 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 14:08:51 -0500 Subject: [PATCH 12/13] chore(server): fix Biome organizeImports violations (import order) Co-Authored-By: Claude Sonnet 4.6 --- server/src/db.ts | 4 ++-- server/src/index.ts | 2 +- server/src/models/index.ts | 4 ++-- server/src/routes/auth.ts | 2 +- server/src/routes/consumption-report.ts | 2 +- server/src/routes/hello.ts | 2 +- server/src/routes/medications.ts | 2 +- server/src/services/auth.ts | 2 +- server/src/services/consumption-report.ts | 2 +- server/src/services/index.ts | 4 ++-- server/test/auth.test.ts | 6 +++--- server/test/consumption-report.test.ts | 6 +++--- server/test/consumptions.test.ts | 6 +++--- server/test/hello.test.ts | 8 ++++---- server/test/helpers.ts | 4 ++-- server/test/medications.test.ts | 6 +++--- 16 files changed, 31 insertions(+), 31 deletions(-) diff --git a/server/src/db.ts b/server/src/db.ts index ce1c554..141d2d7 100644 --- a/server/src/db.ts +++ b/server/src/db.ts @@ -1,6 +1,6 @@ -import Database from 'better-sqlite3'; -import path from 'node:path'; import fs from 'node:fs'; +import path from 'node:path'; +import Database from 'better-sqlite3'; export type DatabaseInstance = InstanceType; diff --git a/server/src/index.ts b/server/src/index.ts index c08af28..8bba956 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import { createApp } from './app'; -import { openDatabase, getDbPath, type DatabaseInstance } from './db'; +import { type DatabaseInstance, getDbPath, openDatabase } from './db'; const PORT = Number(process.env.PORT) || 3000; diff --git a/server/src/models/index.ts b/server/src/models/index.ts index 967b1a4..5ad41e9 100644 --- a/server/src/models/index.ts +++ b/server/src/models/index.ts @@ -1,4 +1,4 @@ -export * from './user'; +export * from './consumption-report'; export * from './medication'; export * from './medication-consumption'; -export * from './consumption-report'; +export * from './user'; diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 1e4295f..aa2928b 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,4 +1,4 @@ -import { Router, type Request, type Response } from 'express'; +import { type Request, type Response, Router } from 'express'; import * as authService from '../services/auth'; const router = Router(); diff --git a/server/src/routes/consumption-report.ts b/server/src/routes/consumption-report.ts index 3d45682..59de4ae 100644 --- a/server/src/routes/consumption-report.ts +++ b/server/src/routes/consumption-report.ts @@ -1,4 +1,4 @@ -import { Router, type Request, type Response } from 'express'; +import { type Request, type Response, Router } from 'express'; import { authMiddleware } from '../middleware/auth'; import * as authService from '../services/auth'; import * as reportService from '../services/consumption-report'; diff --git a/server/src/routes/hello.ts b/server/src/routes/hello.ts index a0b4427..679eed9 100644 --- a/server/src/routes/hello.ts +++ b/server/src/routes/hello.ts @@ -1,4 +1,4 @@ -import { Router, type Response } from 'express'; +import { type Response, Router } from 'express'; import { authMiddleware } from '../middleware/auth'; import { getHelloMessage } from '../services/hello'; diff --git a/server/src/routes/medications.ts b/server/src/routes/medications.ts index d602fe1..cd60633 100644 --- a/server/src/routes/medications.ts +++ b/server/src/routes/medications.ts @@ -1,4 +1,4 @@ -import { Router, type Request, type Response } from 'express'; +import { type Request, type Response, Router } from 'express'; import { authMiddleware } from '../middleware/auth'; import * as authService from '../services/auth'; import * as consumptionsService from '../services/consumptions'; diff --git a/server/src/services/auth.ts b/server/src/services/auth.ts index 53b09c6..07d0733 100644 --- a/server/src/services/auth.ts +++ b/server/src/services/auth.ts @@ -1,8 +1,8 @@ import argon2 from 'argon2'; import jwt from 'jsonwebtoken'; +import { getJwtSecret } from '../config'; import type { DatabaseInstance } from '../db'; import type { LoginInput, RegisterInput, User, UserRow } from '../models/user'; -import { getJwtSecret } from '../config'; const MIN_PASSWORD_LENGTH = 8; diff --git a/server/src/services/consumption-report.ts b/server/src/services/consumption-report.ts index fad1c41..8688ab2 100644 --- a/server/src/services/consumption-report.ts +++ b/server/src/services/consumption-report.ts @@ -1,10 +1,10 @@ import type { DatabaseInstance } from '../db'; -import type { Medication } from '../models/medication'; import type { ActualConsumption, DayResult, ExpectedConsumption, } from '../models/consumption-report'; +import type { Medication } from '../models/medication'; import { listMedications } from './medications'; const REPORT_DAYS = 7; diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 978f6e9..bbaa50f 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -1,5 +1,5 @@ export * from './auth'; -export * from './medications'; -export * from './consumptions'; export * from './consumption-report'; +export * from './consumptions'; export * from './hello'; +export * from './medications'; diff --git a/server/test/auth.test.ts b/server/test/auth.test.ts index 947380d..4983956 100644 --- a/server/test/auth.test.ts +++ b/server/test/auth.test.ts @@ -1,9 +1,9 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; -import request from 'supertest'; -import { createTestApp } from './helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; import type { Express } from 'express'; +import request from 'supertest'; import type { DatabaseInstance } from '../src/db'; +import { createTestApp } from './helpers'; describe('POST /auth/register', () => { let app: Express; diff --git a/server/test/consumption-report.test.ts b/server/test/consumption-report.test.ts index 5bfd82c..0a75992 100644 --- a/server/test/consumption-report.test.ts +++ b/server/test/consumption-report.test.ts @@ -1,9 +1,9 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; -import request from 'supertest'; -import { createTestApp } from './helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; import type { Express } from 'express'; +import request from 'supertest'; import type { DatabaseInstance } from '../src/db'; +import { createTestApp } from './helpers'; const TEST_SECRET = 'test-secret'; diff --git a/server/test/consumptions.test.ts b/server/test/consumptions.test.ts index 19adbcc..5a07ad4 100644 --- a/server/test/consumptions.test.ts +++ b/server/test/consumptions.test.ts @@ -1,9 +1,9 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; -import request from 'supertest'; -import { createTestApp } from './helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; import type { Express } from 'express'; +import request from 'supertest'; import type { DatabaseInstance } from '../src/db'; +import { createTestApp } from './helpers'; const TEST_SECRET = 'test-secret'; diff --git a/server/test/hello.test.ts b/server/test/hello.test.ts index 4f361ea..d2508dc 100644 --- a/server/test/hello.test.ts +++ b/server/test/hello.test.ts @@ -1,10 +1,10 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; -import request from 'supertest'; -import jwt from 'jsonwebtoken'; -import { createTestApp } from './helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; import type { Express } from 'express'; +import jwt from 'jsonwebtoken'; +import request from 'supertest'; import type { DatabaseInstance } from '../src/db'; +import { createTestApp } from './helpers'; const TEST_SECRET = 'test-secret'; diff --git a/server/test/helpers.ts b/server/test/helpers.ts index 7c8fba8..34d42e5 100644 --- a/server/test/helpers.ts +++ b/server/test/helpers.ts @@ -1,6 +1,6 @@ -import { openDatabase, type DatabaseInstance } from '../src/db'; -import { createApp } from '../src/app'; import type { Express } from 'express'; +import { createApp } from '../src/app'; +import { type DatabaseInstance, openDatabase } from '../src/db'; /** * Creates a fresh Express app instance with its own in-memory DB. diff --git a/server/test/medications.test.ts b/server/test/medications.test.ts index cb7a5a7..49e400d 100644 --- a/server/test/medications.test.ts +++ b/server/test/medications.test.ts @@ -1,9 +1,9 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert'; -import request from 'supertest'; -import { createTestApp } from './helpers'; +import { afterEach, beforeEach, describe, it } from 'node:test'; import type { Express } from 'express'; +import request from 'supertest'; import type { DatabaseInstance } from '../src/db'; +import { createTestApp } from './helpers'; const TEST_SECRET = 'test-secret'; From 8e1e76ba6b29d3c36bda5549701ee82617f08a23 Mon Sep 17 00:00:00 2001 From: kenny Date: Sun, 1 Mar 2026 14:16:33 -0500 Subject: [PATCH 13/13] add CI --- .github/workflows/server-tests.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/server-tests.yml diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml new file mode 100644 index 0000000..6f644f9 --- /dev/null +++ b/.github/workflows/server-tests.yml @@ -0,0 +1,28 @@ +name: Server Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: server + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: server/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test