MyKindlebox/memory-bank/implementation-plan.md

45 KiB
Raw Blame History

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.