653 lines
45 KiB
Markdown
653 lines
45 KiB
Markdown
# MyKindlebox — Implementation Plan
|
||
|
||
**Version:** 1.1
|
||
**Last Updated:** 2026-06-20
|
||
**Audience:** AI developers
|
||
**Scope:** Base application only — core herd and litter management, no advanced features
|
||
|
||
Reference documents: `app-design-document.md`, `tech-stack.md`
|
||
|
||
---
|
||
|
||
## Conventions
|
||
|
||
- Each step has a single, concrete objective.
|
||
- Each step ends with a **Validation** block — a specific check that confirms the step is complete and correct before moving on.
|
||
- Do not proceed to the next step until the current step's validation passes.
|
||
- No placeholders or stubs — every step should leave the codebase in a working, testable state.
|
||
|
||
---
|
||
|
||
## Deferred: Authentication
|
||
|
||
The tech stack specifies single-user session token auth. Auth is **intentionally out of scope for this base plan**. All `/api/*` routes are unprotected in this phase. A future plan addendum will cover the login screen, token issuance, and route protection middleware. Do not add auth stubs or placeholders — implement it fully when that phase is added.
|
||
|
||
---
|
||
|
||
## Phase 1 — Project Scaffolding
|
||
|
||
### Step 1.1 — Initialize the monorepo structure
|
||
|
||
Create a root project folder named `mykindlebox`. Inside it, create two subdirectories: `server` for the backend and `client` for the frontend. Add a root-level `README.md` with the project name and one-line description.
|
||
|
||
**Validation:** The folder structure `mykindlebox/server` and `mykindlebox/client` exists. The README is present at the root.
|
||
|
||
---
|
||
|
||
### Step 1.2 — Initialize the backend project
|
||
|
||
Inside `server`, initialize a Node.js project. Install Express as a dependency. Create a main entry file (`index.js` or `index.ts`) that starts an HTTP server on port 3001 and responds to `GET /health` with a JSON body of `{ "status": "ok" }`.
|
||
|
||
**Validation:** Running the server and sending a request to `http://localhost:3001/health` returns HTTP 200 with `{ "status": "ok" }`.
|
||
|
||
---
|
||
|
||
### Step 1.3 — Initialize the frontend project
|
||
|
||
Inside `client`, scaffold a new React + Vite project. Install Tailwind CSS and configure it with a `tailwind.config` file. Install React Router v7. The default page should render the text "MyKindlebox" in the center of the screen.
|
||
|
||
**Validation:** Running the dev server and opening the browser shows a page with the text "MyKindlebox". No console errors.
|
||
|
||
---
|
||
|
||
### Step 1.4 — Configure Tailwind design tokens
|
||
|
||
In the Tailwind config, extend the theme with the custom color tokens from the design document. Define all ten color values as named tokens matching the CSS variable names (e.g., `surface`, `surface-raised`, `surface-sunken`, `border`, `text-primary`, `text-secondary`, `accent`, `accent-hover`, `accent-warm`, `danger`).
|
||
|
||
**Validation:** Apply a background of `bg-surface` and text of `text-text-primary` to the root element. The page background should render as `#F7F3EC` and text as `#1C1A16`. Inspect in browser devtools to confirm the computed colors.
|
||
|
||
---
|
||
|
||
### Step 1.5 — Load custom fonts
|
||
|
||
Add DM Sans and Inter via Google Fonts (or self-hosted). Configure Tailwind's font family extension so that `font-display` maps to DM Sans and `font-body` maps to Inter. Apply `font-body` to the root `<body>` element.
|
||
|
||
**Validation:** In the browser, inspect the computed font of any text element on the page. It should show Inter. Temporarily apply `font-display` to a heading and confirm it shows DM Sans.
|
||
|
||
---
|
||
|
||
### Step 1.6 — Set up Docker Compose
|
||
|
||
Create a `docker-compose.yml` at the project root. Define two services: `server` (Node.js backend on port 3001) and `client` (Vite dev server or Nginx static on port 5173). Mount a named volume for the SQLite database file under `server/data/`.
|
||
|
||
**Validation:** Running `docker compose up` starts both services without errors. The health endpoint at `http://localhost:3001/health` and the frontend at `http://localhost:5173` are both reachable from the host machine.
|
||
|
||
---
|
||
|
||
## Phase 2 — Database
|
||
|
||
### Step 2.1 — Install and configure Drizzle ORM
|
||
|
||
Inside `server`, install Drizzle ORM and the `better-sqlite3` driver. Create a `db` directory inside `server`. Add a `db/client.ts` file that opens a SQLite connection to `server/data/mykindlebox.db` and exports the database instance.
|
||
|
||
**Validation:** Starting the server creates the file `server/data/mykindlebox.db` if it does not already exist. No errors on startup.
|
||
|
||
---
|
||
|
||
### Step 2.2 — Define the Rabbit schema
|
||
|
||
In `server/db/schema.ts`, define the `rabbits` table with all fields from the data model in the design document: `id`, `name`, `farm`, `sex`, `dob`, `weight_lbs`, `ear_number`, `location`, `status`, `color_name`, `photo_path`, `registration_number`, `gc_number`, `sire_id`, `dam_id`, the seven locus fields (`locus_a` through `locus_v`), `created_at`, and `updated_at`.
|
||
|
||
- `sex` must be constrained to the values: `buck`, `doe`, `unknown`.
|
||
- `status` must be constrained to: `active`, `for_sale`, `on_hold`, `sold`, `deceased`, `other`.
|
||
- `sire_id` and `dam_id` are self-referencing foreign keys to `rabbits.id`, nullable.
|
||
- Add twelve freeform text columns for ancestors not in the database, covering all three ancestor generations:
|
||
- Generation 1 (parents): `sire_name_text`, `dam_name_text`
|
||
- Generation 2 (grandparents): `paternal_grandsire_name_text`, `paternal_granddam_name_text`, `maternal_grandsire_name_text`, `maternal_granddam_name_text`
|
||
- Generation 3 (great-grandparents): `pp_grandsire_name_text`, `pp_granddam_name_text`, `pm_grandsire_name_text`, `pm_granddam_name_text`, `mp_grandsire_name_text`, `mp_granddam_name_text`, `mm_grandsire_name_text`, `mm_granddam_name_text`
|
||
All nullable text. The following rule governs when these columns are written and read:
|
||
|
||
**Chain-authority rule:** A `_name_text` column for a given ancestor slot is written *only* when the immediate parent node in that slot's chain has no FK (i.e., the chain is broken at the parent). Once a node resolves through a FK to a real DB record, all deeper generations in that branch come exclusively from that ancestor's own `sire_id`/`dam_id`/`_name_text` fields — the subject's own flat columns for those deeper slots are never written and never read. They are dead columns in an unbroken chain.
|
||
|
||
Concretely:
|
||
- `sire_name_text` is written only when `sire_id` is null.
|
||
- `paternal_grandsire_name_text` is written only when `sire_id` is null (chain broken at Sire). If `sire_id` is set, the Sire's Sire always comes from `Sire.sire_id` or `Sire.sire_name_text` — never from the subject's `paternal_grandsire_name_text`.
|
||
- The same rule applies recursively at every generation and every branch.
|
||
- `created_at` and `updated_at` default to the current timestamp.
|
||
|
||
**Validation:** Running the Drizzle migration (or `db.run` equivalent) creates a `rabbits` table in the SQLite file with the correct columns. Open the database with a SQLite browser and confirm all columns and constraints are present, including all fourteen `_name_text` columns (two parents, four grandparents, eight great-grandparents).
|
||
|
||
---
|
||
|
||
### Step 2.3 — Define the Litter schema
|
||
|
||
In the same `schema.ts`, define the `litters` table with fields: `id`, `sire_id` (FK → rabbits, nullable), `dam_id` (FK → rabbits, nullable), `kindle_date`, `kit_count`, `notes`, and `created_at`.
|
||
|
||
**Validation:** After migration, the `litters` table exists in the database with the correct columns. Confirm the foreign key references to the `rabbits` table are present.
|
||
|
||
---
|
||
|
||
### Step 2.4 — Define the Settings schema
|
||
|
||
Define a `settings` table with fields: `key` (text, primary key) and `value` (text). This is a simple key-value store for app-wide configuration such as rabbitry name and weight display preference.
|
||
|
||
Seed it with two default rows on first run: `rabbitry_name` with a value of `"My Rabbitry"` and `weight_unit` with a value of `"decimal_lbs"`.
|
||
|
||
**Validation:** After startup, the `settings` table exists and contains exactly two rows with the keys `rabbitry_name` and `weight_unit`.
|
||
|
||
---
|
||
|
||
### Step 2.5 — Run and validate all migrations on startup
|
||
|
||
Configure the server so that on startup it runs all pending migrations automatically. Migrations should be idempotent — running them twice must not produce errors or duplicate data.
|
||
|
||
**Validation:** Stop the server, delete the database file, and restart. The database is recreated with all three tables and the two settings seed rows. Stop and restart again without deleting — no errors, no duplicate rows.
|
||
|
||
---
|
||
|
||
## Phase 3 — Backend API
|
||
|
||
### Step 3.1 — Add a global error handler and request logger
|
||
|
||
Add middleware to the Express app that logs every incoming request (method and path) to the console. Add a global error-handling middleware that catches unhandled errors and returns a JSON response with HTTP 500 and a body of `{ "error": "Internal server error" }`.
|
||
|
||
**Validation:** Make a request to a non-existent route (e.g., `GET /nonexistent`). The server logs the request and returns HTTP 404. Trigger a deliberate error in a test route and confirm HTTP 500 is returned with the correct JSON body.
|
||
|
||
---
|
||
|
||
### Step 3.2 — Rabbits API — GET all rabbits
|
||
|
||
Create a `GET /api/rabbits` endpoint. It must return an array of all rabbit records. Support the following optional query parameters:
|
||
|
||
- `sex` — filter by `buck`, `doe`, `unknown`, or the special value `kits` (returns rabbits of any sex whose `dob` is within the last 56 days)
|
||
- `status` — filter by any valid status value
|
||
- `search` — case-insensitive partial match on the `name` field
|
||
- `sort` — one of `name`, `dob`, `weight_lbs`, `created_at` (default: `name`)
|
||
|
||
**Validation:** With an empty database, the endpoint returns `[]`. After inserting two test rabbits directly into the DB (one buck, one doe), confirm that `GET /api/rabbits` returns both, `GET /api/rabbits?sex=buck` returns only the buck, and `GET /api/rabbits?search=<partial name>` filters correctly.
|
||
|
||
---
|
||
|
||
### Step 3.3 — Rabbits API — GET single rabbit
|
||
|
||
Create a `GET /api/rabbits/:id` endpoint. It must return the full rabbit record for the given ID. If no rabbit with that ID exists, return HTTP 404 with `{ "error": "Rabbit not found" }`.
|
||
|
||
**Validation:** Requesting a valid ID returns the correct rabbit record with all fields. Requesting ID `99999` (nonexistent) returns HTTP 404 with the error message.
|
||
|
||
---
|
||
|
||
### Step 3.4 — Rabbits API — POST create rabbit
|
||
|
||
Create a `POST /api/rabbits` endpoint. It must accept a JSON body, validate that `name` is present and non-empty, and insert the record. Return the newly created rabbit record with its assigned `id` and HTTP 201.
|
||
|
||
Reject requests missing `name` with HTTP 400 and `{ "error": "Name is required" }`.
|
||
|
||
**Validation:** `POST /api/rabbits` with a valid body including at minimum `{ "name": "Test Rabbit", "sex": "buck" }` returns HTTP 201 and the new record with an `id`. A subsequent `GET /api/rabbits/:id` with that ID returns the same record. Posting without a name returns HTTP 400.
|
||
|
||
---
|
||
|
||
### Step 3.5 — Rabbits API — PATCH update rabbit
|
||
|
||
Create a `PATCH /api/rabbits/:id` endpoint. It must accept a partial JSON body and update only the provided fields. Return the full updated rabbit record. Return HTTP 404 if the rabbit does not exist.
|
||
|
||
**Validation:** Create a rabbit via POST. Send a PATCH with `{ "weight_lbs": 3.5 }`. Confirm the returned record has `weight_lbs: 3.5` and all other fields are unchanged. Send a PATCH to a nonexistent ID and confirm HTTP 404.
|
||
|
||
---
|
||
|
||
### Step 3.6 — Rabbits API — DELETE rabbit
|
||
|
||
Create a `DELETE /api/rabbits/:id` endpoint. Return HTTP 204 on success. Return HTTP 404 if the rabbit does not exist. Before deleting, set `sire_id` and `dam_id` to `null` on any rabbit that references the deleted rabbit, to preserve referential integrity.
|
||
|
||
**Validation:** Create two rabbits where one is the sire of the other. Delete the sire. Confirm the child rabbit's `sire_id` is now `null`. Confirm the deleted rabbit returns 404 on a subsequent GET.
|
||
|
||
---
|
||
|
||
### Step 3.7 — Rabbits API — GET ancestry
|
||
|
||
Create a `GET /api/rabbits/:id/ancestors` endpoint. It must return a nested object representing the rabbit's full 4-generation pedigree (root + 3 ancestor generations). Each node should include the rabbit's `id`, `name`, `farm`, `sex`, `color_name`, and their own `sire_id` / `dam_id`. Unknown ancestors (no FK and no freeform text) should be represented as `null`.
|
||
|
||
The endpoint must resolve each node using the chain-authority rule defined in Step 2.2:
|
||
- If a node's parent resolved via FK, read the node's data from that parent DB record's own `sire_id`/`dam_id`/`_name_text` fields. Do not read from the subject's flat columns for that slot.
|
||
- If a node's parent had no FK (chain broken), read the node's data from the subject's corresponding `_name_text` column.
|
||
- Never mix sources within a branch: once the chain is broken, all remaining nodes in that branch come from the subject's flat columns; once the chain is intact, all nodes come from recursive ancestor records.
|
||
|
||
**Validation:** Create a set of 15 linked rabbit records (root → 2 parents → 4 grandparents → 8 great-grandparents). Call the ancestors endpoint on the root rabbit and confirm the returned tree has the correct nested structure with all 14 ancestors populated at the correct positions.
|
||
|
||
Then test the broken-chain case: create a root rabbit with `sire_id` null and `sire_name_text = "Bob's Buck"` and `paternal_grandsire_name_text = "Old Bob"`. Call the endpoint and confirm the Sire node returns the freeform text "Bob's Buck" with no `id`, and the Sire's Sire node returns "Old Bob" from the subject's flat column.
|
||
|
||
Then test the unbroken-chain case: create a root rabbit with a real `sire_id`. Set a value in the subject's `paternal_grandsire_name_text` column directly in the DB (simulating stale or erroneous data). Call the endpoint and confirm the Sire's Sire node comes from `Sire.sire_id` or `Sire.sire_name_text` — not from the subject's flat column. The subject's flat column must be ignored.
|
||
|
||
---
|
||
|
||
### Step 3.8 — Rabbits API — GET rabbit search (autocomplete)
|
||
|
||
Create a `GET /api/rabbits/search?q=<query>` endpoint specifically for the pedigree tree autocomplete. It must return only `id`, `name`, `farm`, and `sex` for up to 10 matching rabbits. Match on any portion of the `name` field, case-insensitive.
|
||
|
||
Also support an optional `sex` query parameter (`buck`, `doe`, or `unknown`) to restrict results to a single sex. This is required by the Add Litter form, which must restrict the Sire field to bucks and the Dam field to does.
|
||
|
||
**Validation:** With several rabbits in the database, querying `?q=jim` returns only rabbits whose names contain "jim" (case-insensitive). Querying `?q=&sex=buck` returns only bucks. Querying `?q=&sex=doe` returns only does. The response contains no more than 10 results and only the four specified fields.
|
||
|
||
---
|
||
|
||
### Step 3.9 — Litters API — CRUD endpoints
|
||
|
||
Create the following endpoints for litters:
|
||
|
||
- `GET /api/litters` — return all litters, sorted by `kindle_date` descending. Include the sire and dam's `name` and `id` in each result (joined, not just IDs).
|
||
- `GET /api/litters/:id` — return a single litter. Include sire and dam details. Include all Rabbit records whose `sire_id` and `dam_id` match the litter's parents and whose `dob` matches the `kindle_date`.
|
||
- `POST /api/litters` — create a litter. Require `kindle_date`. Return HTTP 201 with the new record.
|
||
- `PATCH /api/litters/:id` — update a litter's fields. Return the updated record.
|
||
- `DELETE /api/litters/:id` — delete a litter. Return HTTP 204.
|
||
|
||
**Validation:** Create a litter via POST. Confirm it appears in GET all. Add a rabbit with matching `sire_id`, `dam_id`, and `dob` — confirm it appears in the GET single litter's kit list. Delete the litter and confirm it returns 404.
|
||
|
||
---
|
||
|
||
### Step 3.10 — Settings API
|
||
|
||
Create two endpoints:
|
||
|
||
- `GET /api/settings` — return all settings as a flat JSON object (e.g., `{ "rabbitry_name": "My Rabbitry", "weight_unit": "decimal_lbs" }`).
|
||
- `PATCH /api/settings` — accept a partial JSON object and update the provided keys. Only allow known keys (`rabbitry_name`, `weight_unit`). Reject unknown keys with HTTP 400.
|
||
|
||
**Validation:** `GET /api/settings` returns the two seeded defaults. `PATCH /api/settings` with `{ "rabbitry_name": "Belowland Rabbitry" }` updates the value. A subsequent GET reflects the change. Sending an unknown key returns HTTP 400.
|
||
|
||
---
|
||
|
||
### Step 3.11 — Dashboard stats endpoint
|
||
|
||
Create a `GET /api/stats` endpoint that returns a single JSON object with the following counts derived from the `rabbits` table:
|
||
|
||
- `herd` — count of all rabbits with status `active`
|
||
- `bucks` — count of active rabbits with sex `buck`
|
||
- `does` — count of active rabbits with sex `doe`
|
||
- `kits` — count of rabbits with status `active` whose `dob` is within the last 56 days (8 weeks). Use this age-window logic consistently; do not use the litter-join method.
|
||
- `for_sale` — count with status `for_sale`
|
||
- `on_hold` — count with status `on_hold`
|
||
- `sold` — count with status `sold`
|
||
- `archived` — count with status `other` or `deceased`. The dashboard stat card for this value must be labeled **"Archived"**, not "Other", to distinguish it from the sex-based "Other" filter tab on the Herd page.
|
||
|
||
**Validation:** Insert rabbits with known statuses and DOBs. Call `GET /api/stats` and confirm each count matches the expected value. Insert a rabbit with a `dob` of today — confirm it appears in `kits`. Insert one with a `dob` of 57 days ago — confirm it does not. Confirm the `archived` key is present and correctly counts `other` + `deceased` status rabbits.
|
||
|
||
---
|
||
|
||
### Step 3.12 — Photo upload endpoint
|
||
|
||
Create a `POST /api/rabbits/:id/photo` endpoint that accepts a `multipart/form-data` request containing a single image file in a field named `photo`. The endpoint must:
|
||
|
||
- Validate that the uploaded file is an image (accept `image/jpeg`, `image/png`, `image/webp` only). Reject other types with HTTP 400.
|
||
- Save the file to a designated uploads directory on disk (e.g., `server/data/uploads/<rabbit-id>.<ext>`).
|
||
- Update the rabbit's `photo_path` field in the database to the saved file path.
|
||
- Return the updated rabbit record with HTTP 200.
|
||
|
||
The `POST /api/rabbits` and `PATCH /api/rabbits/:id` endpoints remain JSON-only. Photo upload is always a separate step after the rabbit record exists.
|
||
|
||
Serve the uploads directory as static files at `/uploads/*` so the frontend can display photos via a standard `<img src>` URL.
|
||
|
||
**Validation:** Create a rabbit via `POST /api/rabbits`. Then send a `POST /api/rabbits/:id/photo` request with a valid JPEG file. Confirm the file is saved to disk and the rabbit's `photo_path` is updated in the database. Request the photo URL directly in the browser — it should display the image. Send a non-image file (e.g., a `.txt` file) and confirm HTTP 400 is returned.
|
||
|
||
---
|
||
|
||
## Phase 4 — Frontend Foundation
|
||
|
||
### Step 4.1 — Set up React Router with route definitions
|
||
|
||
Configure React Router v7 in the client app. Define routes for all paths in the IA:
|
||
|
||
- `/` → Dashboard page component
|
||
- `/herd` → Herd page component
|
||
- `/herd/add` → Add Rabbit page component
|
||
- `/herd/:id` → Rabbit Detail page component
|
||
- `/herd/:id/edit` → Edit Rabbit page component
|
||
- `/litters` → Litters page component
|
||
- `/litters/add` → Add Litter page component
|
||
- `/litters/:id` → Litter Detail page component
|
||
- `/genetics` → Genetics Calculator page component
|
||
- `/settings` → Settings page component
|
||
|
||
Each route should render a placeholder component that shows the route name as a heading (e.g., "Dashboard", "Herd", etc.).
|
||
|
||
**Validation:** Navigating to each URL in the browser renders the corresponding placeholder heading with no errors. Navigating to `/nonexistent` should show a "Not Found" message (add a catch-all route).
|
||
|
||
---
|
||
|
||
### Step 4.2 — Build the app shell layout
|
||
|
||
Create a persistent layout component that wraps all pages. It must include:
|
||
|
||
- A left sidebar (240px wide) on screens wider than 768px containing: the app name "MyKindlebox" at the top, and navigation links for Dashboard, Herd, Litters, Genetics, and Settings.
|
||
- A bottom navigation bar on screens 768px and below with five items: Dashboard, Herd, Litters, Add (links to `/herd/add`), and Settings.
|
||
- A main content area to the right of the sidebar (or below the top on mobile) where the page component renders.
|
||
|
||
The active route should be visually highlighted in the navigation.
|
||
|
||
**Validation:** On a desktop-width browser, the sidebar is visible and the bottom nav is hidden. On a mobile-width browser (or using devtools), the sidebar is hidden and the bottom nav is visible. Clicking each nav item navigates to the correct route and highlights the active item.
|
||
|
||
---
|
||
|
||
### Step 4.3 — Set up React Query
|
||
|
||
Install and configure React Query. Wrap the app in a `QueryClientProvider`. Create a shared `api.ts` utility file in the client that exports typed fetch functions for each API endpoint defined in Phase 3. All fetch functions should target `http://localhost:3001/api`.
|
||
|
||
**Validation:** In the Dashboard placeholder component, call `GET /api/stats` using a React Query `useQuery` hook. Confirm the raw response data is logged to the console without errors. The network tab in devtools should show a successful request to the backend.
|
||
|
||
---
|
||
|
||
### Step 4.4 — Build shared UI components
|
||
|
||
Build the following small, reusable components. These will be used throughout the app. Each should accept the appropriate props and apply the correct design tokens from the Tailwind config.
|
||
|
||
- `Button` — variants: `primary` (accent background), `secondary` (border only), `danger` (danger background). Sizes: `sm`, `md`.
|
||
- `Badge` — displays a status label. Color-coded by status value: `active` (accent), `for_sale` (accent-warm), `on_hold` (text-secondary), `sold` (border), `deceased` (danger).
|
||
- `Card` — a white (`surface-raised`) container with a border and subtle shadow.
|
||
- `PageHeader` — a heading with an optional subtitle and optional right-side action slot.
|
||
- `EmptyState` — a centered message with a title, description, and optional action button. Used when lists are empty.
|
||
|
||
**Validation:** Create a temporary `/dev` route that renders all five components with representative props. Visually confirm colors, sizes, and layout match the design tokens. Remove the dev route after validation.
|
||
|
||
---
|
||
|
||
## Phase 5 — Dashboard Page
|
||
|
||
### Step 5.1 — Build the stats bar
|
||
|
||
On the Dashboard page, fetch data from `GET /api/stats`. Render eight stat cards in a 4-column grid (2-column on mobile) using the `Card` component. Each card shows the label and count. Each card is a clickable link to the corresponding filtered herd view.
|
||
|
||
Show a loading skeleton (grey placeholder boxes) while the data is fetching. Show an error message if the request fails.
|
||
|
||
**Validation:** With rabbits of various statuses in the database, the dashboard stats cards display the correct counts. Clicking a stat card navigates to `/herd` (with the appropriate filter). The loading skeleton is visible briefly before data loads on a slow connection (simulate with devtools throttling).
|
||
|
||
---
|
||
|
||
### Step 5.2 — Build the quick actions section
|
||
|
||
Below the stats bar, render three action buttons: "Add Rabbit" (links to `/herd/add`), "Record Litter" (links to `/litters/add`), and "View Herd" (links to `/herd`). Use the `Button` component with the `primary` variant for "Add Rabbit" and `secondary` for the others.
|
||
|
||
**Validation:** All three buttons are visible and navigate to the correct routes when clicked.
|
||
|
||
---
|
||
|
||
### Step 5.3 — Build the recent litters widget
|
||
|
||
Below the quick actions, fetch the three most recent litters from `GET /api/litters` and display them as a small list. Each entry shows the sire × dam names, kindle date (formatted as "Month DD, YYYY"), and kit count. Each entry links to its litter detail page. Show the `EmptyState` component if no litters exist yet.
|
||
|
||
**Validation:** With litters in the database, the three most recent appear in the widget with correct names, dates, and kit counts. With no litters, the empty state renders. Each entry links to the correct `/litters/:id` route.
|
||
|
||
---
|
||
|
||
## Phase 6 — Herd Page
|
||
|
||
### Step 6.1 — Build the rabbit card component
|
||
|
||
Create a `RabbitCard` component. It accepts a rabbit record as a prop and renders: a placeholder breed silhouette image (SVG or static asset), the rabbit's name, color name and sex on the same line separated by a bullet, age (calculated from DOB as years/months) and weight on the same line, and litter/kit counts (these can be placeholder zeros until the API supports them). The entire card links to `/herd/:id`. Apply the `Card` component as the wrapper.
|
||
|
||
**Validation:** Render the card with a hardcoded rabbit prop. Confirm all fields display correctly. Confirm the card links to the correct route. A rabbit with no DOB should show "Age unknown" rather than crashing.
|
||
|
||
---
|
||
|
||
### Step 6.2 — Build the herd filter bar
|
||
|
||
Above the rabbit grid on the Herd page, build a filter bar with:
|
||
|
||
- A segmented tab control for sex: All / Bucks / Does / Kits / Other
|
||
- A sort dropdown: Name / Age / Recent / Weight
|
||
- A text search input with a small search icon
|
||
- A view toggle: grid icon and list icon
|
||
|
||
> **Clarification on filter axes:** "Kits" filters by `dob` within the last 56 days (age window, same as the dashboard stat). "Other" filters by `sex = unknown` — this is a sex-axis filter, not a status filter. This is intentionally different from the dashboard's "Archived" card, which filters by `status`. Do not conflate them.
|
||
|
||
Selecting any control should update the URL query parameters (e.g., `?sex=buck&sort=name&q=jim&view=list`) and persist state on page reload.
|
||
|
||
**Validation:** Selecting "Bucks" updates the URL to include `?sex=buck`. Typing in the search box updates `?q=<value>` after a short debounce (no update on every keystroke). Toggling the view switch updates `?view=list` or `?view=grid`. Refreshing the page with those query params restores the filter state.
|
||
|
||
---
|
||
|
||
### Step 6.3 — Build the herd grid view
|
||
|
||
Fetch rabbits from `GET /api/rabbits` using the active filter and sort state from the URL params. Render results as a responsive grid: 2 columns on mobile, 3 on tablet, 4 on desktop. Use the `RabbitCard` component for each rabbit. Show the `EmptyState` component when no results match the current filter. Show loading skeletons (matching the card size) while fetching.
|
||
|
||
**Validation:** With 10+ rabbits in the database, all appear in the grid. Applying the "Bucks" filter shows only bucks. Using the search input filters by name in real time. The grid is 2 columns on a 375px viewport and 4 columns on a 1280px viewport.
|
||
|
||
---
|
||
|
||
### Step 6.4 — Build the herd list view
|
||
|
||
When `?view=list` is active, render a compact table instead of the card grid. Columns: Name, Sex, Color, Age, Weight, Status. Each row links to the rabbit detail page. Zebra-stripe rows using the `surface-sunken` token for alternating rows.
|
||
|
||
**Validation:** Toggling to list view renders the table. All columns are populated correctly. Clicking a row navigates to `/herd/:id`. The same filters (sex, search, sort) applied in grid view also apply in list view.
|
||
|
||
---
|
||
|
||
## Phase 7 — Add / Edit Rabbit Flow
|
||
|
||
### Step 7.1 — Build the pedigree tree input component
|
||
|
||
Create a `PedigreeTree` component. It renders a 4-generation pedigree layout (root + 3 ancestor generations) with 15 nodes: the subject (new rabbit, non-editable label), 2 parents, 4 grandparents, and 8 great-grandparents. The layout follows standard ARBA pedigree orientation — root on the left, each generation branching right.
|
||
|
||
Each ancestor node is an interactive input. When focused, it shows a dropdown with a search box. Typing in the search box calls `GET /api/rabbits/search?q=<query>` and displays matching results. Selecting a result fills that node with the rabbit's name and stores the rabbit's ID. The node also shows two non-search options: "Enter manually" (accepts freeform text, saved to the subject's corresponding `_name_text` column with no FK) and "Unknown" (clears both the FK and the text field for that slot).
|
||
|
||
**Chain-authority rule in the UI:** This rule governs which nodes are editable and where their data is sourced.
|
||
|
||
- When a node is resolved via FK (a real DB record is selected), all child nodes in that branch are automatically populated from that ancestor's own record and become **read-only / locked**. The user cannot enter freeform text into a child node whose parent resolved via FK. Locked nodes display the ancestor's data in a distinct locked style (e.g., greyed out with a lock icon) and are not interactive.
|
||
- When a node has no FK (empty, freeform, or Unknown), its child nodes are **editable**. The user may search for a DB record, enter freeform text, or mark as Unknown. Values entered here are saved to the subject's flat `_name_text` columns.
|
||
- This means "Enter manually" and "Unknown" options are only available on a node whose immediate parent has no FK. They must not appear on locked nodes.
|
||
|
||
When a DB-record ancestor is selected, cascade-populate its branch as deep as that ancestor's own records go. If a grandparent is a DB record but has no sire or dam recorded, the great-grandparent nodes in that branch remain editable (chain is broken at the grandparent level). If a grandparent has freeform text for its own parent, display that text in the locked node in muted style.
|
||
|
||
**Validation:** Render the component in isolation. Typing in the Sire node triggers a search request and shows results. Selecting a DB rabbit as Sire fills the node, auto-populates grandparent nodes from that rabbit's own record, and locks those grandparent nodes — they must not be interactive. Confirm the locked grandparent nodes do not show the search dropdown when clicked.
|
||
|
||
Clear the Sire node (set to Unknown). Confirm the grandparent nodes in the sire branch become editable again. Type a freeform name into the Sire's Sire node and confirm it saves to the subject's `paternal_grandsire_name_text` column with no FK.
|
||
|
||
Select "Unknown" on a parent node and confirm its child nodes are editable. Select "Enter manually" on a parent node and confirm its child nodes are also editable (chain is broken).
|
||
|
||
---
|
||
|
||
### Step 7.2 — Build the rabbit details form
|
||
|
||
Create a `RabbitDetailsForm` component. It renders all fields from the design document's Step 2:
|
||
|
||
- Name (text input, required, marked with an asterisk)
|
||
- Farm / Prefix (text input, pre-filled from the `rabbitry_name` settings value)
|
||
- Sex (three-button segmented control: Buck / Doe / Unknown)
|
||
- Date of Birth (date input)
|
||
- Weight (number input plus a unit toggle: "decimal lbs" or "lbs + oz", driven by the `weight_unit` setting)
|
||
- Ear Number (text input)
|
||
- Location (text input)
|
||
- Status (select dropdown with all six valid values, defaulting to "active")
|
||
- Color Name (text input)
|
||
- Photo (file input, accepts image files only — `image/jpeg`, `image/png`, `image/webp` — shows a preview thumbnail after selection). Note: the photo is uploaded separately via `POST /api/rabbits/:id/photo` after the rabbit record is created. On the Add Rabbit page, the photo field is uploaded as a second request immediately after the initial `POST /api/rabbits` succeeds. On the Edit Rabbit page, a new photo selection replaces the existing one via the same endpoint.
|
||
|
||
**Validation:** Render the form in isolation with no initial values. The Farm field is pre-filled with the settings value. The weight unit toggle switches between a single decimal input and two inputs (lbs + oz). Submitting the form without a Name shows a validation error on the Name field without submitting. All other fields are optional.
|
||
|
||
---
|
||
|
||
### Step 7.3 — Build the genetics section
|
||
|
||
Create a `GeneticsForm` component. It renders a collapsible section labeled "Genetics (optional)". When expanded, it shows pill-group toggles for each of the seven loci (A, B, C, D, E, En, V) with the correct allele options from the design document.
|
||
|
||
Below the loci selectors, render a read-only text field labeled "Generated color string" that displays a human-readable color description assembled from the selected loci (e.g., selecting `aa`, `BB`, `CC`, `DD`, `ee` produces "Self Black Non-Extension").
|
||
|
||
The section defaults to collapsed when adding a new rabbit. It defaults to expanded when editing a rabbit that already has any locus data set.
|
||
|
||
**Validation:** On the Add Rabbit page, the genetics section is collapsed by default and expands when clicked. Selecting loci updates the generated color string. On the Edit page for a rabbit with locus data, the section is expanded by default and shows the correct existing selections.
|
||
|
||
---
|
||
|
||
### Step 7.4 — Build the review step
|
||
|
||
Create a `ReviewStep` component. It displays a read-only summary card of all entered data: the pedigree tree (ancestors listed by role), all detail fields with their values, and the selected genetics loci. Fields with no value should show a dash rather than being omitted.
|
||
|
||
At the bottom, render a "Save Rabbit" button (`primary` variant) and a "Back" button (`secondary` variant) that returns to the details step.
|
||
|
||
**Validation:** Populate all fields in the form steps and navigate to the review step. All entered values appear correctly. Empty optional fields show a dash. The "Back" button returns to the details form without losing any entered data.
|
||
|
||
---
|
||
|
||
### Step 7.5 — Assemble the Add Rabbit multi-step flow
|
||
|
||
On the `/herd/add` page, compose the three steps (Pedigree Tree → Rabbit Details + Genetics → Review) into a linear wizard. Show a step indicator at the top (Step 1 of 3, Step 2 of 3, Step 3 of 3).
|
||
|
||
A "Next" button advances to the next step. The "Next" button on Step 1 is always enabled (pedigree is optional). The "Next" button on Step 2 is disabled until the Name field has a value. On Step 3, clicking "Save Rabbit" submits a `POST /api/rabbits` request with all collected data, then redirects to the new rabbit's detail page at `/herd/:id`.
|
||
|
||
**Validation:** Complete the full flow with valid data. Confirm the POST request is sent with all fields and the correct ancestor IDs. Confirm the redirect lands on the new rabbit's detail page. Attempt to advance past Step 2 without a Name — confirm the button is disabled and/or shows a validation error.
|
||
|
||
---
|
||
|
||
### Step 7.6 — Build the Edit Rabbit page
|
||
|
||
On `/herd/:id/edit`, fetch the existing rabbit record and its ancestors. Pre-populate all form fields and the pedigree tree with the existing data. The flow is a single page (not a wizard) with all three sections visible: pedigree tree at the top, details form in the middle, genetics section (expanded) below.
|
||
|
||
Submitting sends a `PATCH /api/rabbits/:id` request with only the changed fields. On success, redirect to the rabbit's detail page.
|
||
|
||
**Validation:** Open the edit page for a rabbit with full data. All fields are pre-filled correctly. Change the weight and save. Confirm the detail page shows the updated weight and all other fields are unchanged. Confirm only the changed field appears in the PATCH request body (check network tab).
|
||
|
||
---
|
||
|
||
## Phase 8 — Rabbit Detail Page
|
||
|
||
### Step 8.1 — Build the rabbit detail left column
|
||
|
||
On `/herd/:id`, fetch the rabbit record. Render the left column with:
|
||
|
||
- Photo (if `photo_path` is set, display the image; otherwise show the breed silhouette placeholder)
|
||
- Name as a Display-size heading
|
||
- Farm name as a secondary subheading (omit if null)
|
||
- Status badge using the `Badge` component
|
||
- A stats grid: Sex, DOB (formatted as "Month DD, YYYY"), Age (calculated), Weight, Ear Number, Location
|
||
- Registration Number and GC Number (only rendered if non-null)
|
||
- Color Name, followed by loci pills for any locus fields that are set
|
||
|
||
**Validation:** View the detail page for a rabbit with all fields populated. Confirm every field renders. View a rabbit with minimal data (name and sex only) — confirm no crashes and null fields are omitted gracefully. The status badge shows the correct color for each status value.
|
||
|
||
---
|
||
|
||
### Step 8.2 — Build the read-only pedigree tree
|
||
|
||
In the right column of the rabbit detail page, fetch from `GET /api/rabbits/:id/ancestors`. Render the full 4-generation pedigree tree (root + 3 ancestor generations, 15 nodes) in the same visual layout as the entry tree, but read-only. Each ancestor node shows the rabbit's name and farm. Nodes with a database record are clickable links to that rabbit's detail page. Nodes with a freeform text value (stored in a `_name_text` column) display that text in a muted style with no link. Nodes where both the FK and text field are null display "Unknown" in a muted style.
|
||
|
||
**Validation:** On a rabbit with a full 4-generation tree in the database, all 14 ancestor nodes are populated and link to the correct detail pages. On a rabbit with unknown parents, those nodes display "Unknown" in muted text with no link. A node backed by freeform text displays the text in muted style with no link. Clicking a linked ancestor node navigates correctly.
|
||
|
||
---
|
||
|
||
### Step 8.3 — Build the litter history section
|
||
|
||
Below the pedigree tree in the right column, query `GET /api/litters` filtered by the current rabbit appearing as sire or dam. Display matching litters in a compact table: partner name, kindle date, and kit count. Each row links to the litter detail page. Show nothing (no section heading) if the rabbit has no litter history.
|
||
|
||
**Validation:** For a doe with 3 recorded litters, the litter history table shows all 3 with correct dates and kit counts. For a rabbit with no litters, the section is absent.
|
||
|
||
---
|
||
|
||
### Step 8.4 — Build the detail page action bar
|
||
|
||
At the top of the detail page, render an action bar with:
|
||
|
||
- "Edit" button — links to `/herd/:id/edit`
|
||
- "Add Litter" button — links to `/litters/add` with the rabbit's ID pre-filled as a query param (as sire or dam based on sex)
|
||
- "Mark For Sale" button — sends `PATCH /api/rabbits/:id` with `{ "status": "for_sale" }` and refreshes the page. If the rabbit's status is already `for_sale`, show "Mark Active" instead, which sets status back to `active`.
|
||
- "Archive" button — shows a confirmation prompt before sending `PATCH /api/rabbits/:id` with `{ "status": "deceased" }`. Uses the `danger` style.
|
||
|
||
**Validation:** Clicking "Edit" navigates correctly. Clicking "Mark For Sale" updates the status badge on the page without a full reload. Clicking "Archive" shows a confirmation prompt; cancelling does nothing; confirming updates the status.
|
||
|
||
---
|
||
|
||
## Phase 9 — Litters
|
||
|
||
### Step 9.1 — Build the litter card component
|
||
|
||
Create a `LitterCard` component. It renders: sire name × dam name (with "×" as a separator), kindle date formatted as "Month DD, YYYY", kit count (e.g., "3 kits"), and two buttons: "View Kits" linking to `/litters/:id` and "Edit" linking to the litter's edit state. Use the `Card` component as the wrapper.
|
||
|
||
**Validation:** Render the card with a hardcoded litter prop. Confirm all fields display correctly. A litter with no sire should show "Unknown × [dam name]". Both buttons link to the correct routes.
|
||
|
||
---
|
||
|
||
### Step 9.2 — Build the Litters list page
|
||
|
||
On `/litters`, fetch all litters from `GET /api/litters`. Render them as a vertical list of `LitterCard` components, newest first. Add a "Record Litter" button in the page header action slot. Show the `EmptyState` component if no litters exist.
|
||
|
||
**Validation:** With litters in the database, they appear newest first. The "Record Litter" button links to `/litters/add`. With no litters, the empty state is shown.
|
||
|
||
---
|
||
|
||
### Step 9.3 — Build the Add Litter form
|
||
|
||
On `/litters/add`, render a form with the following fields:
|
||
|
||
- Sire (autocomplete search using `GET /api/rabbits/search?q=&sex=buck`, restricted to bucks only — or "Unknown")
|
||
- Dam (autocomplete search using `GET /api/rabbits/search?q=&sex=doe`, restricted to does only — or "Unknown")
|
||
- Kindle Date (date input, required)
|
||
- Kit Count (number input, minimum 0)
|
||
- Notes (textarea, optional)
|
||
|
||
If the page is loaded with a `?sire=<id>` or `?dam=<id>` query param (coming from a rabbit's detail page), pre-fill the corresponding field.
|
||
|
||
Submitting sends `POST /api/litters`. On success, redirect to the new litter's detail page.
|
||
|
||
**Validation:** Submit the form with a sire, dam, and kindle date. Confirm the POST is sent and the redirect lands on the new litter's detail page. Load the page with `?dam=<id>` and confirm the dam field is pre-filled. Submit without a kindle date and confirm a validation error appears.
|
||
|
||
---
|
||
|
||
### Step 9.4 — Build the Litter detail page
|
||
|
||
On `/litters/:id`, fetch the litter from `GET /api/litters/:id`. Render:
|
||
|
||
- Page header: "Sire × Dam" as the title, kindle date as subtitle
|
||
- Sire and Dam name links (to their respective rabbit detail pages)
|
||
- Kindle date, and an expected wean date calculated as 8 weeks after kindle date, formatted as "Month DD, YYYY"
|
||
- Kit table: Name, Sex, Color, Ear Number, Status badge, Weight. Each row's name links to the kit's rabbit detail page.
|
||
- "Add Kit" button that links to `/herd/add` with `?sire=<id>&dam=<id>&dob=<kindle_date>` pre-filled
|
||
|
||
> **Note (v1):** The litter detail page derives its kit list by matching Rabbit records where `sire_id` and `dam_id` match the litter's parents and `dob` equals the litter's `kindle_date` exactly. This is intentional for v1. To prevent mismatches, the Add Rabbit form must default the DOB field to the litter's kindle date when launched from the "Add Kit" button — the user should not have to type it. A date-range tolerance is deferred to a future release.
|
||
|
||
**Validation:** View a litter with 3 kits. All kit rows appear with correct data and link to the correct rabbit detail pages. The expected wean date is exactly 56 days after the kindle date. The "Add Kit" button pre-fills the Add Rabbit form with the correct sire, dam, and DOB. Manually clear the DOB field on the Add Rabbit form before saving — confirm that kit does not appear in the litter's kit list (exact match enforcement).
|
||
|
||
---
|
||
|
||
## Phase 10 — Settings Page
|
||
|
||
### Step 10.1 — Build the Settings form
|
||
|
||
On `/settings`, fetch settings from `GET /api/settings`. Render a form with:
|
||
|
||
- Rabbitry Name (text input, pre-filled from `rabbitry_name`)
|
||
- Weight Display (segmented control: "Decimal lbs" / "lbs + oz", driven by `weight_unit`)
|
||
|
||
A "Save Settings" button sends `PATCH /api/settings` with the changed values and shows a success toast notification on completion.
|
||
|
||
**Validation:** Load the settings page and confirm the fields are pre-filled with the current values. Change the rabbitry name and save. Reload the page and confirm the new name persists. Switch the weight unit and confirm it persists across reload.
|
||
|
||
---
|
||
|
||
### Step 10.2 — Build the database export
|
||
|
||
Add a "Export Database" button on the settings page. Clicking it sends a `GET /api/export` request. The backend should return the raw SQLite database file as a binary download with the filename `mykindlebox-backup-<YYYY-MM-DD>.db` and the content type `application/octet-stream`.
|
||
|
||
**Validation:** Clicking the export button triggers a file download in the browser. The downloaded file can be opened with a SQLite browser (e.g., DB Browser for SQLite) and contains the correct tables and data.
|
||
|
||
---
|
||
|
||
## Phase 11 — Polish and PWA
|
||
|
||
### Step 11.1 — Add a PWA manifest
|
||
|
||
Add a `manifest.json` to the client's public directory. Include: `name` ("MyKindlebox"), `short_name` ("Kindlebox"), `start_url` ("/"), `display` ("standalone"), `background_color` ("#F7F3EC"), `theme_color` ("#4A6741"), and at least one icon (192×192 PNG).
|
||
|
||
**Validation:** In Chrome devtools, open the Application tab → Manifest. All fields appear correctly with no warnings. On a mobile device (or devtools mobile emulation), the browser offers an "Add to Home Screen" prompt.
|
||
|
||
---
|
||
|
||
### Step 11.2 — Add a service worker for offline shell caching
|
||
|
||
Register a service worker that caches the app shell (HTML, JS, CSS bundles, and the static silhouette image). When the device is offline, the app should load and display cached pages rather than a browser error. API requests that fail offline should show an inline "You're offline" notice rather than a crash.
|
||
|
||
**Validation:** Load the app in Chrome. Go to devtools → Application → Service Workers and confirm the service worker is registered. Enable "Offline" mode in devtools Network tab. Reload the page — the app shell loads. Navigate to `/herd` — the page renders (even if the rabbit list shows the offline notice).
|
||
|
||
---
|
||
|
||
### Step 11.3 — Add reduced motion support
|
||
|
||
Wrap all CSS transitions and animations in a `@media (prefers-reduced-motion: reduce)` media query so they are disabled when the user has this OS-level preference enabled.
|
||
|
||
**Validation:** In devtools, enable "Emulate CSS prefers-reduced-motion: reduce" in the Rendering panel. Confirm that any hover transitions and loading skeleton animations are absent.
|
||
|
||
---
|
||
|
||
### Step 11.4 — Keyboard navigation audit
|
||
|
||
Verify that all interactive elements — buttons, links, form inputs, autocomplete dropdowns, segmented controls — are reachable and operable via keyboard alone (Tab to focus, Enter/Space to activate). All focused elements must have a visible focus ring using the accent color.
|
||
|
||
**Validation:** Starting from the top of any page, tab through all interactive elements. Every element receives a visible focus ring. No interactive element is skipped. The pedigree tree autocomplete dropdown can be opened, navigated with arrow keys, and confirmed with Enter, entirely without a mouse.
|
||
|
||
---
|
||
|
||
*End of base implementation plan. Advanced features (genetics calculator, show tracking, health log, exports, public listing) are documented separately.*
|