Compare commits

25 Commits
main2 ... main

Author SHA1 Message Date
761387388d Add .gitea/workflows/docker-build.yml
All checks were successful
Build and Push Docker Image / build (push) Successful in 30s
2026-03-29 14:22:45 -05:00
4394286d0b Delete .gitea/workflows/docker-publish.yml 2026-03-29 14:15:19 -05:00
jason
b8633863b0 fix: add pagination to unbounded GET endpoints
All list endpoints now accept ?page and ?limit (default 50, max 200) and
return { data, total, page, limit } instead of a bare array, preventing
memory and performance failures at scale.

- GET /api/dogs: adds pagination, server-side search (?search) and sex
  filter (?sex), and a stats aggregate (total/males/females) for the
  Dashboard to avoid counting from the array
- GET /api/litters: adds pagination; also fixes N+1 query by fetching
  all puppies for the current page in a single query instead of one per
  litter
- DogList: moves search/sex filtering server-side with 300ms debounce;
  adds Prev/Next pagination controls
- LitterList: uses paginated response; adds Prev/Next pagination controls
- Dashboard: reads counts from stats/total fields instead of array length
- LitterDetail, LitterForm: switch dogs fetch to /api/dogs/all (complete
  list, no pagination, for sire/dam dropdowns)
- DogForm: updates litters fetch to use paginated response shape

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 16:40:28 -05:00
jason
fa7a336588 docs 2026-03-12 11:26:48 -05:00
c3696ba015 docker 2026-03-12 07:43:30 -05:00
c483096c63 qs 2026-03-12 07:40:50 -05:00
e4e3b44fcf Delete RELEASE_NOTES_v0.4.0.md 2026-03-12 07:38:32 -05:00
78e15d08af Delete CLEANUP_NOTES.md 2026-03-12 07:38:27 -05:00
454665b9cb Delete TEST.md 2026-03-12 07:38:04 -05:00
d8557fcfca database 2026-03-12 07:37:20 -05:00
5f68ca0e8b readmes 2026-03-12 07:35:15 -05:00
42bab14ac3 reverse pedigree 2026-03-12 07:27:41 -05:00
5ca594fdc7 external dogs 2026-03-12 07:21:44 -05:00
13185a5281 Roadmap 2,3,4 2026-03-11 23:48:35 -05:00
17b008a674 Merge pull request 'stroke fix' (#55) from pedigree-update into master
Reviewed-on: #55
2026-03-11 15:49:55 -05:00
jason
9b3210a81e stroke fix 2026-03-11 15:49:46 -05:00
81357e87ae Merge pull request 'halo effect' (#54) from pedigree-update into master
Reviewed-on: #54
2026-03-11 15:41:57 -05:00
jason
8abd5e2db6 halo effect 2026-03-11 15:41:30 -05:00
a63617d9c0 Merge pull request 'remove shadow' (#53) from pedigree-update into master
Reviewed-on: #53
2026-03-11 15:37:49 -05:00
jason
7195aaecfc remove shadow 2026-03-11 15:37:38 -05:00
34bf29d8bf Merge pull request 'text update' (#52) from pedigree-update into master
Reviewed-on: #52
2026-03-11 15:33:49 -05:00
jason
4f3074b1f4 text update 2026-03-11 15:33:23 -05:00
3c7ba1775f Merge pull request 'ped changes' (#51) from pedigree-update into master
Reviewed-on: #51
2026-03-11 15:27:42 -05:00
jason
0a0a5d232c ped changes 2026-03-11 15:26:35 -05:00
jason
58b53c981e feat: Add pedigree routes for COI calculation, direct relation checks, and ancestral/descendant trees. 2026-03-11 14:48:59 -05:00
36 changed files with 1265 additions and 2635 deletions

View File

@@ -0,0 +1,25 @@
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: registry.alwisp.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and Push
run: |
docker build -t registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:latest .
docker push registry.alwisp.com/${{ gitea.repository_owner }}/${{ gitea.repository }}:latest

View File

@@ -1,47 +0,0 @@
name: Build & Publish Docker Image
on:
push:
branches:
- master
tags:
- 'v*'
workflow_dispatch: {}
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.alwisp.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata (tags & labels)
id: meta
uses: docker/metadata-action@v5
with:
images: git.alwisp.com/jason/breedr
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-,format=short
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

2
API.md
View File

@@ -1,4 +1,4 @@
# BREEDR API Documentation (v0.6.1)
# BREEDR API Documentation (v0.8.0)
Base URL: `/api`

View File

@@ -1,115 +0,0 @@
# Documentation Cleanup Notes
## Files to Delete (Outdated)
These documentation files are now outdated and should be deleted manually:
### DATABASE_MIGRATIONS.md
- **Reason:** We no longer use migrations - clean init only
- **Replacement:** DATABASE.md has current schema documentation
- **Action:** Delete this file
### DEPLOY_NOW.md
- **Reason:** Deployment info is outdated, superseded by README
- **Replacement:** README.md has up-to-date deployment instructions
- **Action:** Review and delete if redundant
### FEATURE_IMPLEMENTATION.md
- **Reason:** Old implementation notes, likely stale
- **Replacement:** ROADMAP.md has current feature status
- **Action:** Review content, delete if redundant
### FRONTEND_FIX_REQUIRED.md
- **Reason:** Specific bug fix notes, likely resolved
- **Replacement:** Issues are tracked in ROADMAP
- **Action:** Check if fixed, then delete
### IMPLEMENTATION_PLAN.md
- **Reason:** Planning document, likely outdated
- **Replacement:** ROADMAP.md is the living document
- **Action:** Review and delete if redundant
### SPRINT1_PEDIGREE_COMPLETE.md
- **Reason:** Sprint-specific notes, now historical
- **Replacement:** ROADMAP.md shows current progress
- **Action:** Archive or delete
### migrate-now.sh
- **Reason:** Shell script for old migration system
- **Replacement:** Not needed - init.js handles everything
- **Action:** Delete this file
## Files to Keep (Current)
### DATABASE.md ✓
- Complete schema documentation
- Explains clean design (no migrations)
- Reference for developers
### README.md ✓
- Main project documentation
- Installation and setup
- Current features
- Recently updated
### ROADMAP.md ✓
- Development progress tracking
- Feature planning
- Version history
- Recently updated
### INSTALL.md ✓
- Detailed installation instructions
- May need review for accuracy
### QUICKSTART.md ✓
- Quick setup guide
- May need review for accuracy
## Manual Cleanup Required
Gitea API doesn't support file deletion via MCP in some cases. To clean up:
```bash
# Pull the branch
git checkout docs/clean-schema-and-roadmap-update
# Delete outdated files
git rm DATABASE_MIGRATIONS.md
git rm DEPLOY_NOW.md
git rm FEATURE_IMPLEMENTATION.md
git rm FRONTEND_FIX_REQUIRED.md
git rm IMPLEMENTATION_PLAN.md
git rm SPRINT1_PEDIGREE_COMPLETE.md
git rm migrate-now.sh
# Commit and push
git commit -m "Clean: Remove outdated documentation files"
git push origin docs/clean-schema-and-roadmap-update
```
## Post-Cleanup Review
After cleanup, review these files for accuracy:
1. **INSTALL.md** - Verify installation steps are current
2. **QUICKSTART.md** - Ensure quick start is up-to-date
3. **docs/ folder** - Review any documentation in docs/ directory
## Summary
**Keep:**
- DATABASE.md (new, comprehensive)
- README.md (updated)
- ROADMAP.md (updated)
- INSTALL.md (needs review)
- QUICKSTART.md (needs review)
**Delete:**
- DATABASE_MIGRATIONS.md
- DEPLOY_NOW.md
- FEATURE_IMPLEMENTATION.md
- FRONTEND_FIX_REQUIRED.md
- IMPLEMENTATION_PLAN.md
- SPRINT1_PEDIGREE_COMPLETE.md
- migrate-now.sh

View File

@@ -1,222 +0,0 @@
# BREEDR Database Schema
## Overview
This document describes the clean database schema for BREEDR. **NO migrations** - fresh installs create the correct schema automatically.
## Schema Design
### Core Principle: Parents Table Approach
The `dogs` table **does NOT have sire/dam columns**. Parent relationships are stored in the separate `parents` table. This design:
- Keeps the schema clean and normalized
- Allows flexible parent relationships
- Supports future extensions (multiple sires, surrogates, etc.)
## Tables
### dogs
Core registry for all dogs.
```sql
CREATE TABLE dogs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
registration_number TEXT UNIQUE,
breed TEXT NOT NULL,
sex TEXT NOT NULL CHECK(sex IN ('male', 'female')),
birth_date DATE,
color TEXT,
microchip TEXT,
photo_urls TEXT, -- JSON array
notes TEXT,
litter_id INTEGER, -- Links to litters table
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (litter_id) REFERENCES litters(id) ON DELETE SET NULL
);
```
**Important:** NO `sire_id` or `dam_id` columns!
### parents
Stores sire/dam relationships.
```sql
CREATE TABLE parents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL, -- The puppy
parent_id INTEGER NOT NULL, -- The parent
parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')),
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES dogs(id) ON DELETE CASCADE,
UNIQUE(dog_id, parent_type) -- One sire, one dam per dog
);
```
### litters
Breeding records and litter tracking.
```sql
CREATE TABLE litters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sire_id INTEGER NOT NULL,
dam_id INTEGER NOT NULL,
breeding_date DATE NOT NULL,
whelping_date DATE,
puppy_count INTEGER DEFAULT 0,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sire_id) REFERENCES dogs(id) ON DELETE CASCADE,
FOREIGN KEY (dam_id) REFERENCES dogs(id) ON DELETE CASCADE
);
```
### health_records
Health tests, vaccinations, exams, treatments.
```sql
CREATE TABLE health_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
record_type TEXT NOT NULL CHECK(record_type IN ('test', 'vaccination', 'exam', 'treatment', 'certification')),
test_name TEXT,
test_date DATE NOT NULL,
result TEXT,
document_url TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE
);
```
### heat_cycles
Female heat cycle tracking for breeding timing.
```sql
CREATE TABLE heat_cycles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
start_date DATE NOT NULL,
end_date DATE,
progesterone_peak_date DATE,
breeding_date DATE,
breeding_successful INTEGER DEFAULT 0,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE
);
```
### traits
Genetic trait tracking and inheritance.
```sql
CREATE TABLE traits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
trait_category TEXT NOT NULL,
trait_name TEXT NOT NULL,
trait_value TEXT NOT NULL,
inherited_from INTEGER, -- Parent dog ID
notes TEXT,
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
FOREIGN KEY (inherited_from) REFERENCES dogs(id) ON DELETE SET NULL
);
```
## API Usage
### Creating a Dog with Parents
```javascript
POST /api/dogs
{
"name": "Puppy Name",
"breed": "Breed Name",
"sex": "male",
"sire_id": 5, // Parent male dog ID
"dam_id": 8, // Parent female dog ID
"litter_id": 2 // Optional: link to litter
}
```
The API route automatically:
1. Inserts the dog into `dogs` table (without sire/dam columns)
2. Creates entries in `parents` table linking to sire and dam
### Querying Parents
```sql
-- Get a dog's parents
SELECT p.parent_type, d.*
FROM parents p
JOIN dogs d ON p.parent_id = d.id
WHERE p.dog_id = ?;
-- Get a dog's offspring
SELECT d.*
FROM dogs d
JOIN parents p ON d.id = p.dog_id
WHERE p.parent_id = ?;
```
## Fresh Install
For a fresh install:
1. **Delete the old database** (if upgrading):
```bash
rm data/breedr.db
```
2. **Start the server** - it will create the correct schema automatically:
```bash
npm run dev
```
3. **Verify the schema**:
```bash
sqlite3 data/breedr.db ".schema dogs"
```
You should see `litter_id` but **NO** `sire_id` or `dam_id` columns.
## Troubleshooting
### "no such column: weight" or "no such column: sire_id"
**Solution:** Your database has an old schema. Delete it and let the app recreate it:
```bash
rm data/breedr.db
npm run dev
```
### Parent relationships not saving
Check server logs. You should see:
```
✓ Dog inserted with ID: 123
Adding sire relationship: dog 123 -> sire 5
✓ Sire relationship added
Adding dam relationship: dog 123 -> dam 8
✓ Dam relationship added
```
If relationships aren't being created, check that `sire_id` and `dam_id` are being sent in the API request.
## Database Files
- `server/db/init.js` - Creates clean schema, no migrations
- `server/routes/dogs.js` - Handles parent relationships via `parents` table
- `server/index.js` - Initializes database on startup
**NO MIGRATIONS!** The init file is the source of truth.

View File

@@ -4,115 +4,98 @@ This document provides technical details and guidelines for developing and maint
## Tech Stack Overview
- **Monorepo Structure**:
- `server/`: Express.js backend.
- `client/`: React/Vite frontend.
- `data/`: SQLite database storage.
- `uploads/`: Uploaded images and documents.
- `static/`: Static assets for the application.
### Backend
- **Node.js & Express**: Core API server.
- **better-sqlite3**: High-performance SQLite driver.
- **Multer**: Multi-part form data handling for photo uploads.
- **Bcrypt & JWT**: (Planned) Authentication and security.
- **Backend**: Node.js, Express, better-sqlite3, multer, bcrypt, jsonwebtoken.
- **Frontend**: React 18, Vite, React Router 6, Axios, Lucide React, D3 (for pedigree trees).
- **Database**: SQLite (managed with better-sqlite3).
### Frontend
- **React 18 & Vite**: Modern reactive UI with fast HMR.
- **React Router 6**: Client-side navigation.
- **Lucide React**: Consistent iconography.
- **React-D3-Tree & D3.js**: Dynamic pedigree visualization.
- **Axios**: Promised-based HTTP client for API communication.
## Getting Started
### Prerequisites
- Node.js (v18+ recommended)
- npm
### Installation
1. Clone the repository.
2. Install root dependencies:
```bash
npm install
```
3. Install client dependencies:
```bash
cd client && npm install
```
### Development Commands
Run the following from the project root:
- **Run both client and server**: `npm run dev`
- **Run server only**: `npm run server`
- **Run client only**: `npm run client`
- **Initialize Database**: `npm run db:init`
- **Build for production**: `npm run build`
---
## Database Architecture
### Data Storage
The database is a single SQLite file located at `data/breedr.db`. This directory is automatically created on startup if it doesn't exist.
### Initialization & Schema
- **Initialization**: `server/db/init.js` defines the initial schema and creates tables if they don't exist.
- **Migrations**: `server/db/migrations.js` handles schema updates. Migrations run automatically on server startup.
### SQLite Implementation
The database is a single file located at `data/breedr.db`. This directory is automatically created on startup.
### "Parents Table" Approach
Instead of storing parent IDs directly in the `dogs` table (which was the old approach), relationships are managed in a dedicated `parents` table:
Parent relationships are managed in a dedicated `parents` table rather than columns in the `dogs` table.
- ** dog_id**: The child dog.
- ** parent_id**: The parent dog.
- ** parent_type**: 'sire' or 'dam'.
```sql
CREATE TABLE parents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
parent_id INTEGER NOT NULL,
parent_type TEXT NOT NULL CHECK(parent_type IN ('sire', 'dam')),
FOREIGN KEY (dog_id) REFERENCES dogs(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES dogs(id) ON DELETE CASCADE,
UNIQUE(dog_id, parent_type)
);
```
**Benefits**: Supports recursive lookups, avoids `ALTER TABLE` complexity for lineage changes, and allows historical mapping of ancestors without full profiles.
**Key Benefits**:
- Avoids complex `ALTER TABLE` operations when changing pedigree logic.
- Cleanly separates dog attributes from lineage relationships.
- Supports indexing for fast recursive pedigree lookups.
### Safe Migrations
BREEDR use a migration-free synchronization approach:
1. `server/db/init.js` defines the latest table structures.
2. Safe `ALTER TABLE` guards inject missing columns on startup.
3. This ensures data persistence across updates without manual migration scripts.
### Key Tables
- **`dogs`**: Core dog data (name, breed, sex, microchip, etc.).
- **`parents`**: Lineage relationships (Sire/Dam).
- **`litters`**: Groups of dogs from a single breeding.
- **`breeding_records`**: Planned or completed breeding events.
- **`health_records`**: OFA results, vaccinations, and other health tests.
- **`genetic_tests`**: DNA panel results.
- **`settings`**: Kennel-wide configurations.
- `dogs`: Registry for kennel and external dogs.
- `parents`: Ancestry relationships.
- `litters`: Produced breeding groups.
- `health_records`: OFA clearances and vet records.
- `genetic_tests`: DNA panel results.
- `settings`: Kennel-wide configuration (single row).
## Backend Development
---
### API Routes
Routes are modularized in `server/routes/`:
- `/api/dogs`: Dog management.
- `/api/litters`: Litter management.
- `/api/health`: Health record management.
- `/api/genetics`: Genetic testing management.
- `/api/pedigree`: Pedigree tree generation.
- `/api/breeding`: Breeding records.
- `/api/settings`: Application settings.
## Frontend Documentation
### File Uploads
Images and documents are stored in `uploads/`. The `multer` middleware handles file processing. File paths are stored in the database as relative URLs (e.g., `/uploads/image.jpg`).
### Project Structure
```text
client/src/
├── components/ # Reusable UI (PedigreeTree, DogForm, Cards)
├── hooks/ # Custom hooks (useSettings)
├── pages/ # Route-level components
├── App.jsx # Routing & Layout
└── index.css # Global styles & Design System
```
## Frontend Development
### Design System & Styling
The UI follows a modern dark-theme aesthetic using **CSS Variables** defined in `index.css`:
- `--primary`: Brand color (Warm Amber/Blue).
- `--bg-primary`: Deep Slate background.
- Glassmorphism effects via `backdrop-filter`.
- Responsive grid layouts (`.grid-2`, `.grid-3`).
### State Management
- **Settings**: Managed globally via `SettingsProvider` in `client/src/hooks/useSettings.jsx`.
- **Component State**: Local `useState` and `useEffect` are preferred for feature-specific data.
### Key Components
- **PedigreeTree**: horizontal, D3-powered tree with zoom/pan.
- **DogForm**: Dual-mode (Kennel/External) dog entry with parent selection.
### Styling
- CSS Variables are used for theming.
- The UI uses a modern, clean design with Lucide icons.
---
### Pedigree Trees
The pedigree tree visualization is powered by `react-d3-tree` and D3.js. Logic for building the tree structure is located in `server/routes/pedigree.js` and visualized in the `PedigreeTree` component.
## API & Backend Development
## Environment Variables
### Route Modules (`server/routes/`)
- `/api/dogs`: Dog registry and photo uploads.
- `/api/litters`: Litter management and puppy linking.
- `/api/pedigree`: Recursive ancestry/descendant tree generation.
- `/api/breeding`: Heat cycle tracking and whelping projections.
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | `3000` |
| `NODE_ENV` | Environment mode | `development` |
| `DATA_DIR` | Path to DB storage | `../data` |
| `UPLOAD_PATH` | Path to uploads | `../uploads` |
| `STATIC_PATH` | Path to static assets | `../static` |
| `DB_PATH` | Full path to .db file | `../data/breedr.db` |
| `DB_PATH` | Path to .db file | `../data/breedr.db` |
| `UPLOAD_PATH`| Path to photo storage| `../uploads` |
---
## Technical History & Design Logs
For deeper technical dives into specific features, refer to the `docs/` directory:
- [UI Redesign & Color System](docs/UI_REDESIGN.md)
- [Compact Card Layout Design](docs/COMPACT_CARDS.md)
- [Microchip Field Unique Constraint Fix](docs/MICROCHIP_FIX.md)
---
*Last Updated: March 12, 2026*

View File

@@ -37,14 +37,14 @@ RUN npm install --omit=dev
# Copy server code
COPY server/ ./server/
# Copy static assets (branding, etc.) to ensure default logo is present
COPY static/ ./static/
# Copy built frontend from previous stage
COPY --from=frontend-builder /app/client/dist ./client/dist
# Create necessary directories (including static for branding assets)
RUN mkdir -p /app/data /app/uploads /app/static
# Initialize database schema on build
RUN node server/db/init.js || true
# Create data and uploads directories
RUN mkdir -p /app/data /app/uploads
# Set environment variables
ENV NODE_ENV=production

View File

@@ -1,105 +0,0 @@
# Frontend Guide
This document provides an overview of the frontend architecture, technologies, and patterns used in the BREEDR application.
## Tech Stack
- **Framework**: [React](https://reactjs.org/) (bootstrapped with [Vite](https://vitejs.dev/))
- **Routing**: [react-router-dom](https://reactrouter.com/)
- **Icons**: [lucide-react](https://lucide.dev/)
- **Data Fetching**: [axios](https://axios-http.com/)
- **Visualizations**: [react-d3-tree](https://github.com/bkrem/react-d3-tree) (for pedigree rendering)
- **Styling**: Standard CSS with CSS Variables (Custom properties)
## Project Structure
```text
client/src/
├── components/ # Reusable UI components
├── hooks/ # Custom hooks and context providers
├── pages/ # Page-level components (routes)
├── utils/ # Helper functions and utilities
├── App.jsx # Root component with routing
├── App.css # Layout-specific styles
├── index.css # Global styles and CSS variables
└── main.jsx # Application entry point
```
## Routing and Layout
The application uses `react-router-dom` for navigation. The primary layout and routes are defined in `client/src/App.jsx`.
- **Navbar**: Contains links to Dashboard, Dogs, Litters, Breeding, Pairing, and Settings.
- **Main Content**: Renders the matched route element within a `.main-content` container.
### Key Routes
- `/`: Dashboard
- `/dogs`: Dog List
- `/dogs/:id`: Dog Detail
- `/pedigree/:id`: Pedigree View
- `/litters`: Litter List
- `/breeding`: Breeding Calendar
- `/pairing`: Pairing Simulator
- `/settings`: Settings Page
## State Management
### Settings Context (`useSettings`)
Global application settings (like kennel name) are managed via a React Context.
- **Provider**: `SettingsProvider` in `client/src/hooks/useSettings.jsx`.
- **Usage**:
```javascript
import { useSettings } from '../hooks/useSettings';
const { settings, saveSettings, loading } = useSettings();
```
### Local State
Most page-specific data is managed using standard `useState` and `useEffect` hooks, fetching data via `axios`.
## Styling Conventions
The application follows a dark-theme aesthetic using CSS variables for consistency.
### CSS Variables (`client/src/index.css`)
Key variables include:
- `--primary`: Main brand color (warm amber/copper).
- `--bg-primary`: Primary background.
- `--text-primary`: Primary text color.
- `--border`: Standard border color.
- `--radius`: Default border radius.
### Reusable UI Classes
- `.btn`, `.btn-primary`, `.btn-secondary`: Standard button styles.
- `.card`: Container for grouped content.
- `.grid`, `.grid-2`, `.grid-3`: Responsive grid layouts.
- `.modal-overlay`, `.modal-content`: Standard modal structure.
## Key Components
### PedigreeTree (`client/src/components/PedigreeTree.jsx`)
Uses `react-d3-tree` to render a horizontal, step-based pedigree.
- **Props**: `dogId`, `pedigreeData`, `coi`.
- **Features**: Custom node rendering (differentiating by sex and champion status), zoom/pan controls, and COI display.
### DogForm (`client/src/components/DogForm.jsx`)
Handles both creation and editing of dog records.
- **Logic**: Manages internal/external dog states, parent selection (manual or linked to litter), and champion status.
- **Validation**: Basic required field validation; errors are displayed at the top of the form.
## Data Fetching Patterns
Standard `axios` requests are used:
```javascript
const fetchData = async () => {
try {
const res = await axios.get('/api/endpoint');
setData(res.data);
} catch (err) {
setError(err.response?.data?.error || 'Error message');
}
};
```
## Icons
Use `lucide-react` for all icons to ensure consistency across the UI.

View File

@@ -1,340 +0,0 @@
# BREEDR Quick Start Guide
## Litter Management & Pedigree Visualization
## Installation
### 1. Pull the Feature Branch
```bash
git checkout feature/litter-management-and-pedigree
```
### 2. Run Database Migration
```bash
node server/db/migrate_litter_id.js
```
You should see:
```
Running litter_id migration...
✓ Added litter_id column to dogs table
✓ Created index on litter_id
Migration completed successfully!
```
### 3. Install Dependencies
```bash
cd client
npm install
cd ..
```
### 4. Start the Application
```bash
npm run dev
```
The server will start on `http://localhost:3000` and the client on `http://localhost:5173`
---
## Feature 1: Litter Management
### Creating Your First Litter
1. **Navigate to Litters**
- Click "Litters" in the navigation menu
- Click "Add New Litter" button
2. **Fill in Litter Details**
- **Sire (Father)**: Select from dropdown of male dogs
- **Dam (Mother)**: Select from dropdown of female dogs
- **Breeding Date**: Date of breeding (required)
- **Whelping Date**: Expected/actual birth date (optional)
- **Expected Puppy Count**: Estimated number of puppies
- **Notes**: Any additional breeding information
3. **Save the Litter**
- Click "Create Litter"
- Litter appears in the list with format: "Sire x Dam - Date"
### Adding Puppies to a Litter
#### Method 1: Link to Existing Litter (Recommended)
1. **Click "Add New Dog"**
2. **Enter Puppy Details**
- Name (required)
- Breed (required)
- Sex (required)
- Birth Date
- Color
- Microchip
3. **Select Parent Method**
- Choose "Link to Litter" radio button
- Select the litter from dropdown
- Parents are automatically filled!
4. **Save**
- Click "Add Dog"
- Puppy is now linked to the litter
- Parent relationships are automatically created
#### Method 2: Manual Parent Selection
1. **Click "Add New Dog"**
2. **Enter Puppy Details**
3. **Select Parent Method**
- Choose "Manual Parent Selection" radio button
- Select Sire from male dogs dropdown
- Select Dam from female dogs dropdown
4. **Save**
- Puppy is created with selected parents
- No litter association
### Viewing Litter Details
1. **Click on a Litter** in the list
2. **See Litter Information:**
- Sire and Dam details
- Breeding and whelping dates
- List of all puppies in the litter
- Actual puppy count vs expected
### Editing a Litter
1. Click "Edit" on the litter
2. Update breeding/whelping dates
3. Modify notes
4. **Note:** Cannot change sire/dam after creation
---
## Feature 2: Interactive Pedigree Tree
### Viewing a Pedigree
1. **From Dog List:**
- Click on any dog
- Click "View Pedigree" button
2. **Pedigree Opens in Modal**
- Shows dog's ancestry tree
- 5 generations displayed
- Color-coded by sex:
- Blue nodes = Males ♂
- Pink nodes = Females ♀
### Navigating the Tree
#### Zoom Controls
- **Zoom In**: Click "+" button or mouse wheel up
- **Zoom Out**: Click "-" button or mouse wheel down
- **Reset View**: Click reset button to center tree
#### Panning
- **Click and Drag**: Move the tree around
- **Mouse Wheel**: Zoom in/out
#### Node Information
Each node displays:
- Dog name (large text)
- Registration number
- Birth year
- Sex symbol (♂ or ♀)
### Reading the Tree
```
Great-Great-Grandpa ♂
Great-Grandpa ♂
Great-Great-Grandma ♀
Grandpa ♂
Great-Great-Grandpa ♂
Great-Grandma ♀
Great-Great-Grandma ♀
Sire ♂
Great-Great-Grandpa ♂
Great-Grandpa ♂
Great-Great-Grandma ♀
Grandma ♀
Great-Great-Grandpa ♂
Great-Grandma ♀
Great-Great-Grandma ♀
Dog Name
Dam ♀
[... similar structure for dam's side]
```
---
## Common Workflows
### Workflow 1: Breeding a Litter
1. ✓ Select breeding pair (sire and dam)
2. ✓ Create litter record with breeding date
3. ✓ Track whelping date when puppies are born
4. ✓ Add each puppy:
- Link to the litter
- Enter individual details
- Assign registration numbers
5. ✓ View pedigree of any puppy to see full ancestry
### Workflow 2: Recording Historical Dogs
1. ✓ Add foundation dogs (no parents)
2. ✓ Add their offspring using manual parent selection
3. ✓ Continue building the family tree
4. ✓ View pedigrees to verify relationships
### Workflow 3: Planning a Breeding
1. ✓ View pedigrees of potential sire and dam
2. ✓ Check for common ancestors
3. ✓ Use trial pairing tool (coming soon)
4. ✓ Create litter when breeding occurs
---
## Tips & Best Practices
### For Litter Management
**Do:**
- Create the litter record BEFORE adding puppies
- Enter accurate breeding dates for record keeping
- Use meaningful notes (progesterone timing, heat cycle info)
- Link puppies to litters for automatic parent relationships
**Don't:**
- Don't change sire/dam after litter creation (create new litter instead)
- Don't forget to update whelping date when puppies arrive
- Avoid mixing litter-linked and manually-parented puppies
### For Pedigree Viewing
**Do:**
- Zoom out to see the full tree at once
- Use drag to focus on specific branches
- Click nodes to see additional details
- Reset view if you get lost
**Don't:**
- Don't try to edit from pedigree view (use dog edit form)
- Avoid excessive zooming (can make nodes too small)
### Data Entry Tips
1. **Registration Numbers**: Enter consistently (e.g., "AKC-12345")
2. **Microchips**: Use full 15-digit number
3. **Birth Dates**: Critical for age calculations and sorting
4. **Breed Names**: Keep consistent spelling and capitalization
5. **Colors**: Use standard color terminology for your breed
---
## Troubleshooting
### "No such column: sire" Error
**Problem:** Getting this error when adding a dog
**Solution:**
1. Make sure you ran the migration:
```bash
node server/db/migrate_litter_id.js
```
2. Restart the server
3. Try again
### Pedigree Tree Not Loading
**Problem:** Pedigree modal shows "Loading..." forever
**Possible Causes:**
- Dog has no parents recorded
- Network issue
- Server not running
**Solution:**
1. Check browser console for errors
2. Verify server is running
3. Ensure dog has at least one parent recorded
### Parents Not Auto-Populating
**Problem:** Selected a litter but parents didn't fill in
**Solution:**
1. Refresh the page
2. Make sure litter has valid sire and dam
3. Try selecting the litter again
### Can't See All Generations
**Problem:** Pedigree tree only shows 2-3 generations
**This is normal if:**
- Older generations don't have parents recorded
- Foundation dogs have no ancestry
- You need to add more historical data
---
## Keyboard Shortcuts
*Coming in future release*
- `Ctrl/Cmd + N` - New Dog
- `Ctrl/Cmd + L` - New Litter
- `Ctrl/Cmd + P` - View Pedigree
- `Esc` - Close Modal
---
## Next Features Coming Soon
🔜 **Trial Pairing Simulator**
- Calculate COI before breeding
- See common ancestors
- Risk assessment
🔜 **Heat Cycle Tracking**
- Track progesterone levels
- Breeding date recommendations
- Calendar view
🔜 **PDF Pedigree Export**
- Print-ready pedigrees
- Custom formatting
- Multiple generations
---
## Getting Help
- **Documentation:** [FEATURE_IMPLEMENTATION.md](./FEATURE_IMPLEMENTATION.md)
- **Roadmap:** [ROADMAP.md](./ROADMAP.md)
- **Installation:** [INSTALL.md](./INSTALL.md)
- **README:** [README.md](./README.md)
---
## Video Tutorials
*Coming soon - check back for video walkthroughs of these features!*
1. Creating Your First Litter
2. Adding Puppies to a Litter
3. Navigating Pedigree Trees
4. Advanced Breeding Records
---
## Congratulations!
You're now ready to use BREEDR's litter management and pedigree visualization features. Start by creating a litter or viewing a pedigree tree!
**Happy Breeding! 🐶**

391
README.md
View File

@@ -2,365 +2,86 @@
A reactive, interactive dog breeding genealogy mapping system for professional kennel management.
## ✅ Current Features
---
### Core Functionality
- **✅ Dog Registry** - Complete CRUD operations with comprehensive profiles
- **✅ Photo Management** - Multiple photos per dog with upload/delete capabilities
- **✅ Parent Relationships** - Clean database design using `parents` table (no sire/dam columns in dogs table)
- **✅ Litter Management** - Track breeding records, link puppies to litters
- **✅ Interactive Pedigree Visualization** - Multi-generational family trees with zoom/pan
- **✅ Modern UI** - Sleek, dark-themed interface with compact info cards
- **✅ Search & Filter** - Find dogs by name, breed, sex, and more
- **✅ Branded Navigation** - Custom logo (br-logo.png) with gold-to-rusty-red gradient title
- **✅ Trial Pairing Simulator** - COI calculator with common ancestors table and risk badge
- **✅ Heat Cycle Calendar** - Month grid calendar with cycle windows, breeding date suggestions, and **projected whelping identifiers**
- **✅ Champion Bloodline Tracking** - Mark dogs as titled champions; offspring display a Champion Bloodline badge
- **✅ Kennel Settings** - Configurable kennel name, tagline, address, AKC ID, breed, owner info
- **✅ UI Theme** - CSS custom property theming with `--champion-gold` and dark-mode variables
## 🌟 Recent Highlights (v0.8.0)
- **✅ Reverse Pedigree** — Toggle between ancestors and descendants view for full lineage tracking.
- **✅ External Dog Mapping** — Assign parents to external dogs, allowing for full genealogy of outside lines.
- **✅ Universal Parent Selection** — Select any dog (kennel or external) as a sire/dam from any profile.
### Database Architecture
- **✅ Clean Schema** - No migrations needed; fresh installs create correct structure; existing DBs auto-migrate via safe `ALTER TABLE` guards
- **✅ Normalized Design** - `parents` table for relationships (sire/dam)
- **✅ Litter Linking** - Dogs linked to litters via `litter_id`
- **✅ Health Records** - Medical history and genetic testing
- **✅ Heat Cycles** - Breeding cycle tracking
- **✅ Genetic Traits** - Inherited trait mapping
- **✅ Settings Table** - Single-row kennel configuration with all contact/identity fields
---
### Recently Added (March 10, 2026 — v0.6.1)
- **✅ COI Direct-Relation Fix** — `calculateCOI` now correctly computes inbreeding coefficient for parent×offspring pairings. Previously returned `0.00%` due to blanket exclusion of `sid` from `commonIds`; sire now correctly appears as a common ancestor in the dam's ancestry map when they are parent×offspring
- **✅ pedigree.js Route Fix** — `commonIds` filter changed from `id !== sid && id !== did``id !== did` only; preserves parent×offspring COI path while still preventing reflexive dam self-loop
- **Expected COI for parent×offspring pairing:** ~25.00% (Wright's path coefficient method)
### Previously Added (March 9, 2026 — v0.6.0)
- **✅ Champion Flag** — `is_champion INTEGER DEFAULT 0` on `dogs` table; safe `ALTER TABLE` migration guard for existing DBs
- **✅ Champion Toggle in DogForm** — amber-gold highlighted checkbox row with `Award` icon; marks dog as titled champion
- **✅ Champion ⭐ in Parent Dropdowns** — sire/dam selects append `⭐` to champion names for at-a-glance visibility
- **✅ Champion Bloodline Badge** — offspring of champion parents display a badge on dog cards and detail pages
- **✅ Kennel Settings API** — `GET/PUT /api/settings` with single-row column schema and ALLOWED_KEYS whitelist
- **✅ Settings Table Migration** — all kennel fields added with safe `ALTER TABLE` guards on existing DBs; default seed row auto-created
- **✅ SettingsProvider / useSettings** — React context hook renamed `useSettings.jsx` (was `.js`; contained JSX causing Vite build failure)
- **✅ `server/index.js` Fix** — `initDatabase()` called with no args to match updated `db/init.js`; removed duplicate `/api/health` route
- **✅ `settings.js` Route Fix** — rewrote from double-encoded base64 + old key/value schema to correct single-row column schema
### Previously Added (March 9, 2026 — v0.5.1)
- **✅ Projected Whelp Window on Calendar** - Indigo/purple day cells (days 5865 from breeding date) visible directly on the month grid
- **✅ Expected Whelp Day Marker** - Indigo dot on the exact expected whelp day (day 63) alongside the green breeding dot
- **✅ "[Name] due" Cell Label** - Baby 🍼 icon + dog name label inside the whelp day cell
- **✅ Active Cycle Card — Whelp Range** - "Whelp est. [date]" row with earliestlatest range shown on each active cycle card
- **✅ Jump-to-Whelp-Month Button** - One-click navigation to the whelp month when it differs from current view
- **✅ Live Whelp Preview in Modal** - Instant client-side earliest/expected/latest preview as soon as a breeding date is entered (no save required)
- **✅ Whelping Banner** - Full-width indigo banner listing dogs with projected whelps when no active heat cycles are visible
- **✅ Legend Entry** - "Projected Whelp" added to calendar legend
- **✅ Updated Page Subtitle** - Now reads: *"Track heat cycles, optimal breeding windows, and projected whelping dates"*
### Previously Added (March 9, 2026 — v0.5.0)
- **✅ Heat Cycle Calendar** - Full month grid with color-coded cycle windows (Proestrus / Optimal / Late Estrus / Diestrus)
- **✅ Start Cycle Modal** - Click any day or the header button to log a new heat cycle for a female
- **✅ Breeding Date Suggestions** - Phase windows with date ranges loaded from `GET /api/breeding/heat-cycles/:id/suggestions`
- **✅ Whelping Estimate** - Auto-calculates earliest/expected/latest whelping once a breeding date is logged
- **✅ Trial Pairing Simulator** - `/pairing` route with sire/dam dropdowns, COI%, risk badge, and common ancestors table
- **✅ Pairing Nav Link** - `FlaskConical` icon added to navbar
- **✅ New API Endpoints** - `GET /api/breeding/heat-cycles`, `GET /api/breeding/heat-cycles/:id/suggestions`
### Previously Added (March 9, 2026 — v0.4.x)
- **✅ Brand Logo** - Custom `br-logo.png` in navbar replacing generic icon
- **✅ Gradient Title** - Gold-to-rusty-red gradient on "BREEDR" brand text
- **✅ Static Asset Serving** - `/static` directory served by Express for branding assets
- **✅ Dev Proxy** - Vite dev server proxies `/static` to Express backend
- **✅ Route Fix** - `/static` and `/uploads` paths no longer fall through to React catch-all
- **✅ Logo Sizing** - Fixed brand logo to 1:1 aspect ratio square
## Technology Stack
- **Frontend**: React 18 with modern component design
- **Visualization**: React-D3-Tree for pedigree charts
- **Backend**: Node.js/Express API
- **Database**: SQLite (embedded, zero-config) with clean normalized schema + safe `ALTER TABLE` migration guards
- **Container**: Single Docker image with multi-stage build
- **Styling**: CSS custom properties with dark theme + `--champion-gold` + gradient branding
## Quick Start
### Docker Deployment (Recommended)
## 🚀 Quick Start
### 1. Docker Deployment (Recommended)
```bash
# Clone repository
git clone https://git.alwisp.com/jason/breedr.git
cd breedr
# Build Docker image
docker build -t breedr:latest .
# Run with docker-compose
docker-compose up -d
```
Access at: `http://localhost:3000`
### Upgrading an Existing Installation
The database now uses safe `ALTER TABLE` guards — **you do not need to delete your database to upgrade**. Just pull and rebuild:
```bash
docker-compose down
git pull origin master
docker-compose up -d --build
```
Access at: `http://localhost:3000`
New columns (`is_champion`, all `settings` kennel fields) are added automatically on first boot. Your existing dog data is preserved.
### Fresh Install Database Setup
For a **fresh install**, the database will automatically initialize with the correct schema and seed a default settings row.
## Database Schema
### Key Design Principles
1. **No sire/dam columns in `dogs` table** - Parent relationships stored in `parents` table
2. **Normalized structure** - Reduces redundancy, improves data integrity
3. **Litter linking** - Dogs reference litters via `litter_id` foreign key
4. **Safe migrations** - `ALTER TABLE ... ADD COLUMN` guards allow zero-downtime upgrades
### Core Tables
- **dogs** - Core dog registry; includes `is_champion`, `litter_id`, `photo_urls`
- **parents** - Sire/dam relationships (dog_id, parent_id, parent_type)
- **litters** - Breeding records with sire/dam references
- **health_records** - Medical and genetic testing
- **heat_cycles** - Breeding cycle tracking
- **traits** - Genetic trait mapping
- **settings** - Single-row kennel configuration (kennel_name, tagline, address, phone, email, website, akc_id, breed, owner_name)
**Full schema documentation:** [DATABASE.md](DATABASE.md)
## Environment Variables
- `NODE_ENV` - production/development (default: production)
- `PORT` - Server port (default: 3000)
- `DATA_DIR` - Data directory for SQLite file (default: /app/data)
- `UPLOAD_PATH` - Upload directory (default: /app/uploads)
- `STATIC_PATH` - Static assets directory (default: /app/static)
## Development
### Local Development Setup
### 2. Manual Development Setup
```bash
# Install dependencies
npm install
# Run development server (frontend + backend, nodemon auto-reload)
npm run dev
# Build for production
npm run build
```
> **Note:** The database initializes automatically on first boot. No manual migrations are required.
> **Note:** `npm run dev` uses nodemon for auto-reload on the server. `npm start` (production) does **not** watch for changes — restart is required after pulling updates.
---
### Project Structure
## 🐕 Managing Your Kennel
- **Adding Dogs**: Go to the **Dogs** page, click **Add New Dog**. You can mark dogs as **External** if they aren't in your kennel but are needed for pedigree mapping.
- **Champion Tracking**: Toggle the **Champion** status to title dogs. Offspring will automatically display the "Champion Bloodline" badge.
- **Photo Management**: Multiple high-quality photos per dog with a compact gallery view.
- **Litter Tracking**: Link puppies to breeding records automatically to track weight and health from birth.
## 🧬 Breeding & Genetics
- **Interactive Pedigree**: 5-generation trees with zoom/pan. Toggle the **Reverse Pedigree** switch to see descendant lineage.
- **Trial Pairing Simulator**: Calculate Wright's Inbreeding Coefficient (COI) instantly. Identifies common ancestors and providing risk badges (Low/Moderate/High).
- **Heat Cycles**: Track female cycles on the calendar. Includes **projected whelping alerts** (indigo windows) and expected due dates.
---
## 🛠️ Technology Stack
- **Frontend**: React 18, Vite, Lucide Icons
- **Visualization**: React-D3-Tree, D3.js
- **Backend**: Node.js, Express.js
- **Database**: SQLite (Zero-config, safe `ALTER TABLE` migrations)
- **Deployment**: Multi-stage Docker
---
## 📂 Project Structure
```
breedr/
├── client/ # React frontend
├── src/
├── pages/
├── BreedingCalendar.jsx # Heat cycle calendar + whelping identifiers
├── PairingSimulator.jsx # Trial pairing + COI
│ │ ├── SettingsPage.jsx # Kennel settings form
│ │ │ ├── Dashboard.jsx
│ │ │ ├── DogList.jsx
│ │ │ ├── DogDetail.jsx
│ │ │ ├── PedigreeView.jsx
│ │ │ └── LitterList.jsx
│ │ ├── components/
│ │ │ └── DogForm.jsx # Champion toggle + parent selects
│ │ ├── hooks/
│ │ │ └── useSettings.jsx # SettingsProvider + useSettings context
│ │ └── App.jsx
│ └── package.json
├── server/ # Node.js backend
│ ├── routes/
│ │ ├── dogs.js # is_champion in all queries
│ │ ├── settings.js # GET/PUT kennel settings (single-row schema)
│ │ ├── breeding.js # Heat cycles, whelping, suggestions
│ │ ├── pedigree.js # COI, trial pairing (v0.6.1 direct-relation fix)
│ │ ├── litters.js
│ │ └── health.js
│ ├── db/
│ │ └── init.js # Schema + ALTER TABLE migration guards
│ └── index.js
├── static/ # Branding assets (br-logo.png, etc.)
├── docs/ # Documentation
├── ROADMAP.md
├── DATABASE.md
├── Dockerfile
├── docker-compose.yml
└── README.md
├── client/ # React frontend (Pages: Pedigree, Pairing, Calendar, Settings)
├── server/ # Node.js backend (Routes: Dogs, Pedigree, Breeding, Settings)
├── static/ # Branded assets (logos, etc.)
├── data/ # SQLite database storage (mapped in Docker)
├── uploads/ # Dog photo storage (mapped in Docker)
└── docs/ # Technical documentation and design history
```
## API Endpoints
---
### Dogs
- `GET/POST /api/dogs` - Dog CRUD operations
- `GET /api/dogs/:id` - Get dog with parents (incl. is_champion), offspring, and health summary
- `PUT /api/dogs/:id` - Update dog (incl. is_champion)
- `POST /api/dogs/:id/photos` - Upload photos
## 🕒 Release Summary
### Settings
- `GET /api/settings` - Get kennel settings
- `PUT /api/settings` - Update kennel settings (partial update supported)
- **v0.8.0** (Mar 2026): Reverse Pedigree & External dog parentage.
- **v0.7.0** (In Progress): Health & Genetics (OFA clearances, DNA panels).
- **v0.6.1**: COI calculation fix for direct parent×offspring relations.
- **v0.6.0**: Champion status tracking & Kennel settings API.
### Pedigree & Genetics
- `GET /api/pedigree/:id` - Generate pedigree tree
- `POST /api/pedigree/trial-pairing` - COI + common ancestors + risk recommendation
- `GET /api/pedigree/relations/:sireId/:damId` - Direct relation detection (parent/grandparent check)
---
### Breeding & Heat Cycles
- `GET /api/breeding/heat-cycles` - All heat cycles
- `GET /api/breeding/heat-cycles/active` - Active cycles with dog info
- `GET /api/breeding/heat-cycles/dog/:dogId` - Cycles for a specific dog
- `GET /api/breeding/heat-cycles/:id/suggestions` - Breeding windows + whelping estimate
- `POST /api/breeding/heat-cycles` - Create new heat cycle
- `PUT /api/breeding/heat-cycles/:id` - Update cycle (log breeding date, etc.)
- `DELETE /api/breeding/heat-cycles/:id` - Delete cycle
- `GET /api/breeding/whelping-calculator` - Standalone whelping date calculator
## ❓ Troubleshooting
- **COI shows 0.00%?**: Ensure both parents are mapped and have shared ancestors.
- **Missing Columns?**: Restart the server; auto-init guards add columns automatically.
- **Logo not appearing?**: Place `br-logo.png` in the `static/` directory.
### Litters
- `GET/POST /api/litters` - Litter management
---
### Assets
- `GET /static/*` - Branding and static assets
- `GET /uploads/*` - Dog photos
## Troubleshooting
### COI shows 0.00% for parent×offspring pairings
Ensure you are on v0.6.1+ (merge PR #47). The fix removes a blanket `id !== sid` exclusion in `calculateCOI` that was silently zeroing the inbreeding coefficient when the sire is a direct ancestor of the dam. After merging, restart the server.
### Server crashes with `SyntaxError: Unexpected end of input` on `settings.js`
The settings route file may have been corrupted (double-encoded base64). Pull the latest code and rebuild.
### "no such column: kennel_name" or "no such column: is_champion"
Your database predates the `ALTER TABLE` migration guards. Pull the latest code and restart — columns are added automatically. No data loss.
### "no such column: weight" or "no such column: sire_id"
Your database has a very old schema. Delete and recreate:
```bash
cp data/breedr.db data/breedr.db.backup
rm data/breedr.db
docker-compose restart
```
### Logo not appearing in navbar
Ensure `br-logo.png` is placed in the `static/` directory at the project root. The file is served at `/static/br-logo.png`.
### Heat cycles not showing on calendar
Ensure dogs are registered with `sex: 'female'` before creating heat cycles. The API validates this and will return a 400 error for male dogs.
### Whelping window not appearing on calendar
A breeding date must be logged on the cycle for whelp window cells to appear. Use the Cycle Detail modal → "Log Breeding Date" field.
## Roadmap
### ✅ Completed
- [x] Docker containerization
- [x] SQLite database with clean schema + ALTER TABLE migration guards
- [x] Dog management (CRUD) with champion flag
- [x] Photo management
- [x] Interactive pedigree visualization
- [x] Litter management
- [x] Parent-child relationships via parents table
- [x] Modern UI redesign
- [x] Search and filtering
- [x] Custom brand logo + gradient title
- [x] Static asset serving
- [x] Trial Pairing Simulator (COI + common ancestors + risk badge)
- [x] Heat Cycle Calendar (month grid + windows + breeding suggestions + whelping estimate)
- [x] **Projected Whelping Calendar Identifier** (whelp window cells, due label, active card range, live modal preview, whelping banner)
- [x] **Champion Bloodline Tracking** (is_champion flag, DogForm toggle, offspring badge)
- [x] **Kennel Settings** (GET/PUT /api/settings, SettingsProvider, kennel name in navbar)
- [x] **COI Direct-Relation Fix** (parent×offspring now correctly yields ~25% COI — v0.6.1)
### 🔜 In Progress / Up Next
- [ ] Health Records System
- [ ] Genetic trait tracking
### 📋 Planned
- [ ] PDF pedigree generation
- [ ] Advanced search and filters
- [ ] Export capabilities
- [ ] Progesterone tracking (extended feature)
**Full roadmap:** [ROADMAP.md](ROADMAP.md)
## Recent Updates
### March 10, 2026 - COI Direct-Relation Bug Fix (v0.6.1)
- **Fixed:** `calculateCOI` in `server/routes/pedigree.js` — removed `id !== sid` from `commonIds` filter
- **Root cause:** `getAncestorMap` includes each dog at `gen 0`; the sire (`sid`) correctly appears in the dam's ancestor map at `gen 1` for parent×offspring pairings, but `id !== sid` was filtering it out and returning `0.00%`
- **Result:** Parent×offspring pairings now correctly return ~25.00% COI; all other pairings unaffected
- **PR:** [#47](https://git.alwisp.com/jason/breedr/pulls/47)
### March 9, 2026 - Champion Bloodline, Settings, Build Fixes (v0.6.0)
- **Added:** `is_champion` column to `dogs` table with safe `ALTER TABLE` migration guard
- **Added:** Champion toggle checkbox in DogForm with amber-gold highlight and `Award` icon
- **Added:** `⭐` suffix on champion sire/dam in parent dropdowns
- **Added:** Champion Bloodline badge on offspring cards/detail pages
- **Added:** `GET/PUT /api/settings` route — single-row column schema with `ALLOWED_KEYS` whitelist
- **Added:** Full kennel settings columns in `settings` table with migration guards
- **Added:** `SettingsProvider` / `useSettings` React context for kennel name in navbar
- **Fixed:** `useSettings.js``useSettings.jsx` (Vite build failure — JSX in `.js` file)
- **Fixed:** `server/index.js``initDatabase()` called with no args; removed duplicate `/api/health` route
- **Fixed:** `server/routes/settings.js` — rewrote from double-encoded base64 + old key/value schema
- **Fixed:** `DB_PATH` arg removed from `initDatabase()` call; `DATA_DIR` env var now controls directory
### March 9, 2026 - Projected Whelping Calendar Identifier (v0.5.1)
- **Added:** Indigo whelp window (days 5865) on calendar grid cells when a breeding date is logged
- **Added:** Indigo dot marker on exact expected whelp day (day 63)
- **Added:** `Baby` icon + "[Name] due" label inside whelp day cells
- **Added:** "Whelp est. [date]" row with earliestlatest range on active cycle cards
- **Added:** Jump-to-whelp-month button on active cycle cards
- **Added:** Live whelp preview in Cycle Detail modal (client-side, instant, no save required)
- **Added:** Full-width whelping banner when projected whelps exist but no active heat cycles are visible
- **Added:** "Projected Whelp" legend entry with Baby icon
- **Updated:** Page subtitle to include projected whelping dates
### March 9, 2026 - Heat Cycle Calendar & Trial Pairing Simulator (v0.5.0)
- **Added:** Full month grid heat cycle calendar with color-coded phase windows
- **Added:** Start Heat Cycle modal (click any day or header button)
- **Added:** Cycle Detail modal with breeding window breakdown and inline breeding date logging
- **Added:** Whelping estimate (earliest/expected/latest) auto-calculated from breeding date
- **Added:** Trial Pairing Simulator at `/pairing` with COI%, risk badge, common ancestors table
- **Added:** `GET /api/breeding/heat-cycles` and `GET /api/breeding/heat-cycles/:id/suggestions` endpoints
- **Moved:** Progesterone tracking to extended roadmap
### March 9, 2026 - Branding & Header Improvements (v0.4.1)
- **Added:** Custom `br-logo.png` brand logo in navbar
- **Added:** Gold-to-rusty-red gradient on "BREEDR" title text
- **Added:** `/static` directory for branding assets served by Express
- **Fixed:** Vite dev proxy for `/static` routes
- **Fixed:** `/static` and `/uploads` paths no longer fall through to React router
- **Fixed:** Brand logo sized as fixed 1:1 square for proper aspect ratio
## Documentation
- [DATABASE.md](DATABASE.md) - Complete schema documentation
- [ROADMAP.md](ROADMAP.md) - Development roadmap and features
- [INSTALL.md](INSTALL.md) - Detailed installation instructions
- [QUICKSTART.md](QUICKSTART.md) - Quick setup guide
## License
Private use only - All rights reserved
## Support
For issues or questions:
- Check documentation in `docs/` folder
- Review DATABASE.md for schema questions
- Check container logs: `docker logs breedr`
- Contact the system administrator
**Full Documentation**:
[Installation Guide](INSTALL.md) | [Development & Architecture](DEVELOPMENT.md) | [API Reference](API.md) | [Roadmap](ROADMAP.md)

View File

@@ -1,308 +0,0 @@
# BREEDR v0.4.0 Release Notes
**Release Date:** March 9, 2026
**Branch:** `docs/clean-schema-and-roadmap-update`
**Focus:** Clean Database Schema & Documentation Overhaul
---
## 🆕 What's New
### Clean Database Architecture
We've completely overhauled the database design for simplicity and correctness:
- **✅ NO MORE MIGRATIONS** - Fresh init creates correct schema automatically
- **✅ Removed weight/height columns** - Never implemented, now gone
- **✅ Added litter_id column** - Proper linking of puppies to litters
- **✅ Parents table approach** - NO sire/dam columns in dogs table
- **✅ Normalized relationships** - Sire/dam stored in separate parents table
### Why This Matters
The old schema had:
- Migration scripts trying to fix schema issues
- `sire_id` and `dam_id` columns causing "no such column" errors
- Complex migration logic that could fail
The new schema:
- ✅ Clean initialization - always correct
- ✅ Normalized design - proper relationships
- ✅ Simple maintenance - no migration tracking
- ✅ Better logging - see exactly what's happening
---
## 🛠️ Technical Changes
### Database
**Removed:**
- `dogs.weight` column (never implemented)
- `dogs.height` column (never implemented)
- `dogs.sire_id` column (moved to parents table)
- `dogs.dam_id` column (moved to parents table)
- `server/db/migrations.js` (no more migrations)
**Added:**
- `dogs.litter_id` column with foreign key to litters
- `parents` table for sire/dam relationships
- Clean `server/db/init.js` as single source of truth
### API Changes
**server/routes/dogs.js:**
- Fixed parent handling - properly uses parents table
- Added detailed logging for relationship creation
- Removed schema detection logic
- Cleaner error messages
**server/index.js:**
- Removed migrations import and execution
- Simplified startup - just calls initDatabase()
- Better console output with status indicators
### Documentation
**New Files:**
- `DATABASE.md` - Complete schema reference
- `CLEANUP_NOTES.md` - Lists outdated files to remove
- `RELEASE_NOTES_v0.4.0.md` - This file
**Updated Files:**
- `README.md` - Current features and setup instructions
- `ROADMAP.md` - Accurate progress tracking and version history
**Outdated Files (Manual Deletion Required):**
- `DATABASE_MIGRATIONS.md`
- `DEPLOY_NOW.md`
- `FEATURE_IMPLEMENTATION.md`
- `FRONTEND_FIX_REQUIRED.md`
- `IMPLEMENTATION_PLAN.md`
- `SPRINT1_PEDIGREE_COMPLETE.md`
- `migrate-now.sh`
See `CLEANUP_NOTES.md` for details.
---
## 🚀 Upgrade Instructions
### For Fresh Installs
No action needed! The database will initialize correctly:
```bash
git clone https://git.alwisp.com/jason/breedr.git
cd breedr
git checkout docs/clean-schema-and-roadmap-update
docker-compose up -d
```
### For Existing Installations
**Important:** This update requires starting with a fresh database.
1. **Backup your data:**
```bash
cp data/breedr.db data/breedr.db.backup
```
2. **Stop the application:**
```bash
docker-compose down
```
3. **Delete old database:**
```bash
rm data/breedr.db
```
4. **Pull latest code:**
```bash
git pull origin docs/clean-schema-and-roadmap-update
```
5. **Rebuild and restart:**
```bash
docker-compose up -d --build
```
6. **Verify database:**
```bash
docker exec -it breedr sqlite3 /app/data/breedr.db ".schema dogs"
```
You should see `litter_id` but **NO** `sire_id`, `dam_id`, `weight`, or `height` columns.
### Data Migration Notes
**Parent Relationships:**
- Cannot be automatically migrated due to schema change
- You'll need to re-enter sire/dam relationships for existing dogs
- Use the dog edit form or litter linking feature
**All Other Data:**
- Basic dog info (name, breed, sex, etc.) can be re-entered
- Photos will need to be re-uploaded
- Consider this a fresh start with a clean, correct schema
---
## 🐛 Bug Fixes
- ✅ **Fixed:** "no such column: sire" errors
- ✅ **Fixed:** "no such column: weight" errors
- ✅ **Fixed:** "no such column: height" errors
- ✅ **Fixed:** Parent relationships not saving properly
- ✅ **Fixed:** Schema detection failures on startup
- ✅ **Fixed:** Migration system complexity
---
## 📚 Documentation Updates
### DATABASE.md
Comprehensive database documentation including:
- Schema design principles
- All table structures with SQL
- API usage examples
- Query examples for relationships
- Fresh install instructions
- Troubleshooting guide
### README.md
Updated with:
- Current feature list
- Clean schema explanation
- Fresh install vs upgrade instructions
- Troubleshooting for common errors
- Links to documentation
### ROADMAP.md
Updated with:
- Phase 1-3 marked complete
- v0.4.0 release notes
- Current sprint focus recommendations
- Version history
---
## 🧐 Developer Notes
### New Development Workflow
**For database changes:**
1. Edit `server/db/init.js` only
2. Test with fresh database: `rm data/breedr.db && npm run dev`
3. Update `DATABASE.md` documentation
4. No migrations needed!
**For API changes involving parents:**
- Use `parents` table for sire/dam relationships
- Check `server/routes/dogs.js` for examples
- Log relationship creation for debugging
### Testing
Test these scenarios:
1. Fresh install - database created correctly
2. Add dog with sire/dam - parents table populated
3. Add dog via litter - litter_id set, parents auto-linked
4. View dog details - parents and offspring shown correctly
5. Pedigree view - multi-generation tree displays
---
## 📊 What's Next
### Recommended Next Features
1. **Trial Pairing Simulator** (4-6 hours)
- Uses existing COI calculator backend
- High value for breeding decisions
- Relatively quick to implement
2. **Health Records System** (6-8 hours)
- Important for breeding decisions
- Vaccination tracking
- Document management
3. **Heat Cycle Management** (6-8 hours)
- Natural extension of litter management
- Calendar functionality
- Breeding planning
See `ROADMAP.md` for full details.
---
## Support
**Documentation:**
- [DATABASE.md](DATABASE.md) - Schema reference
- [README.md](README.md) - Project overview
- [ROADMAP.md](ROADMAP.md) - Development plan
- [CLEANUP_NOTES.md](CLEANUP_NOTES.md) - File cleanup guide
**Common Issues:**
- "no such column" errors → Delete database and restart
- Parents not saving → Check server logs for relationship creation
- Schema looks wrong → Verify with `.schema dogs` command
**Logs:**
```bash
docker logs breedr
```
---
## 🎉 Credits
Clean schema design and implementation by the BREEDR development team.
Special thanks for thorough testing and validation of the new database architecture.
---
## 📝 Changelog Summary
### Added
- Clean database initialization system
- `dogs.litter_id` column
- `parents` table for relationships
- DATABASE.md documentation
- Detailed logging for debugging
- CLEANUP_NOTES.md
- RELEASE_NOTES_v0.4.0.md
### Changed
- Database init is now single source of truth
- Parent relationships use parents table
- README.md updated
- ROADMAP.md updated
- Simplified server startup
### Removed
- Migration system (`server/db/migrations.js`)
- `dogs.weight` column
- `dogs.height` column
- `dogs.sire_id` column
- `dogs.dam_id` column
- Schema detection logic
- Outdated documentation (marked for deletion)
### Fixed
- "no such column" errors
- Parent relationship saving
- Schema consistency issues
- Migration failures
---
**Full Diff:** [Compare branches on Gitea](https://git.alwisp.com/jason/breedr/compare/feature/enhanced-litters-and-pedigree...docs/clean-schema-and-roadmap-update)
**Next Release:** v0.5.0 - Trial Pairing Simulator (planned)

View File

@@ -1,402 +1,9 @@
# BREEDR Development Roadmap
# BREEDR Development Roadmap (v0.8.0)
## ✅ Phase 1: Foundation (COMPLETE)
### Infrastructure
- [x] Docker multi-stage build configuration
- [x] SQLite database with automatic initialization
- [x] Express.js API server
- [x] React 18 frontend with Vite
- [x] Git repository structure
### Database Schema
- [x] Dogs table with core fields (NO sire/dam columns)
- [x] Parents relationship table for sire/dam tracking
- [x] Litters breeding records
- [x] Health records tracking
- [x] Heat cycles management
- [x] Traits genetic mapping
- [x] Indexes and triggers
- [x] **litter_id column** for linking puppies to litters
- [x] **Clean schema design** - NO migrations, fresh init only
- [x] **Safe ALTER TABLE migration guards** - new columns added automatically on upgrade
### API Endpoints
- [x] `/api/dogs` - Full CRUD operations (incl. `is_champion`)
- [x] `/api/pedigree` - Tree generation and COI calculator
- [x] `/api/litters` - Breeding records
- [x] `/api/health` - Health tracking
- [x] `/api/breeding` - Heat cycles and whelping calculator
- [x] `/api/settings` - Kennel configuration (GET/PUT)
- [x] Photo upload with Multer
- [x] **Parent relationship handling** via parents table
- [x] `/static/*` - Branding and static asset serving
---
## ✅ Phase 2: Core Functionality (COMPLETE)
### Dog Management
- [x] Add new dogs with full form
- [x] Edit existing dogs
- [x] View dog details
- [x] List all dogs with search/filter
- [x] Upload multiple photos per dog
- [x] Delete photos
- [x] Parent selection (sire/dam) via parents table
- [x] **Proper error handling** for API failures
### User Interface
- [x] Dashboard with statistics
- [x] Dog list with grid view
- [x] Dog detail pages
- [x] Modal forms for add/edit
- [x] Photo management UI
- [x] Search and sex filtering
- [x] Responsive navigation
- [x] **Compact info cards** (80x80 avatars)
- [x] **Modern dark theme** with glass morphism
- [x] **Custom brand logo** (br-logo.png) in navbar
- [x] **Gold-to-rusty-red gradient** on BREEDR brand title
- [x] **Static asset serving** via Express `/static` route
- [x] **Vite dev proxy** for `/static` routes
- [x] **Route fix** - static/uploads don't fall through to React router
- [x] **Logo aspect ratio** fixed to 1:1 square
### Features Implemented
- [x] Photo upload and storage
- [x] Parent-child relationships (via parents table)
- [x] Basic information tracking
- [x] Registration numbers
- [x] Microchip tracking (optional)
- [x] **Litter linking** with litter_id
- [x] **Clean database schema** with no migrations
---
## ✅ Phase 3: Breeding Tools (COMPLETE)
### Pedigree & Genetics
- [x] **Interactive pedigree tree visualization**
- [x] Integrate React-D3-Tree
- [x] Show 3-5 generations
- [x] Click to navigate
- [x] Zoom and pan controls
- [x] Beautiful color-coded nodes
- [x] Male/Female distinction
- [x] **Litter Management**
- [x] Create litter records
- [x] Link puppies to litter
- [x] Track whelping details
- [x] Auto-link parent relationships
- [x] Database migration for litter_id
- [x] Enhanced API endpoints
- [x] Dual parent selection mode (litter/manual)
- [x] UI fix for proper layout and error handling
- [x] **Trial Pairing Simulator***(March 9, 2026)*
- [x] Sire and dam selection dropdowns
- [x] COI calculation display with color coding
- [x] Common ancestors table (sire-gen / dam-gen columns)
- [x] Risk badge: Low (<5%) / Moderate (5-10%) / High (>10%)
- [x] `/pairing` route + navbar link
- [x] `POST /api/pedigree/trial-pairing` backend
- [x] **Heat Cycle Calendar***(March 9, 2026)*
- [x] Full month grid calendar (SunSat) with prev/next navigation
- [x] Color-coded day cells by cycle phase
- [x] Start Heat Cycle modal (female dropdown + date picker)
- [x] Cycle Detail modal with phase breakdown
- [x] Breeding date logging inline
- [x] Whelping estimate (earliest/expected/latest)
- [x] Active cycles list with phase badge + day counter
- [x] `GET /api/breeding/heat-cycles` endpoint
- [x] `GET /api/breeding/heat-cycles/:id/suggestions` endpoint
- [x] **Projected Whelping Calendar Identifier***(March 9, 2026 v0.5.1)*
- [x] Gestation constants: earliest=58, expected=63, latest=65 days
- [x] `getWwhelpDates(cycle)` client-side helper (no extra API call)
- [x] Indigo whelp window cells (days 5863) on calendar grid
- [x] Indigo dot marker on expected whelp day (day 63)
- [x] `Baby` icon + "[Name] due" label inside whelp day cells
- [x] "Whelp est. [date]" row with range on active cycle cards
- [x] Jump-to-whelp-month button on active cycle cards
- [x] Live whelp preview in Cycle Detail modal (client-side, instant)
- [x] Full-width whelping banner when projected whelps exist
- [x] "Projected Whelp" legend entry with Baby icon
- [x] Updated page subtitle to include whelping dates
---
## ✅ Phase 4a: Champion & Settings (COMPLETE v0.6.0)
### Champion Bloodline Tracking
- [x] `is_champion INTEGER DEFAULT 0` column on `dogs` table
- [x] Safe `ALTER TABLE dogs ADD COLUMN is_champion` migration guard
- [x] `is_champion` included in all `GET /api/dogs` + `GET /api/dogs/:id` responses
- [x] `is_champion` persisted in `POST` and `PUT /api/dogs`
- [x] `is_champion` included on sire/dam JOIN queries and offspring query
- [x] Champion toggle checkbox in `DogForm` with amber-gold highlight + `Award` icon
- [x] `✪` suffix on champion names in sire/dam parent dropdowns
- [x] Champion Bloodline badge on offspring cards and dog detail pages
### Kennel Settings
- [x] `settings` table: `kennel_name`, `kennel_tagline`, `kennel_address`, `kennel_phone`, `kennel_email`, `kennel_website`, `kennel_akc_id`, `kennel_breed`, `owner_name`
- [x] Safe `ALTER TABLE settings ADD COLUMN` migration loop for all kennel fields
- [x] Auto-seed default row (`kennel_name = 'BREEDR'`) if table is empty
- [x] `GET /api/settings` returns single-row as flat JSON object
- [x] `PUT /api/settings` partial update via `ALLOWED_KEYS` whitelist
- [x] `SettingsProvider` / `useSettings` React context hook
- [x] Kennel name displayed in navbar from settings
- [x] `SettingsPage` component for editing kennel info
### Build & Runtime Fixes (v0.6.0)
- [x] `useSettings.js``useSettings.jsx` Vite build failed because JSX in `.js` file
- [x] `server/index.js` `initDatabase()` called with no args (was passing `DB_PATH`, now path is internal)
- [x] `server/index.js` removed duplicate `app.get('/api/health')` inline route
- [x] `server/index.js` `DATA_DIR` env var replaces `path.dirname(DB_PATH)` for directory creation
- [x] `server/routes/settings.js` rewrote from double-encoded base64 + old key/value schema to correct single-row column schema
---
## 📋 Phase 4b: Health & Genetics (NEXT UP v0.7.0)
## 🚀 Current Status: v0.8.0 (Active Development)
### 🔜 Next Up — Phase 4b: Health & Genetics Build Order
> **Context:** Golden Retriever health clearances follow GRCA Code of Ethics and OFA/CHIC standards.
> This phase builds a structured, breed-aware health tracking system aligned with those requirements.
### Tier 1 — OFA Health Clearances *(Priority 1)* 🩺
The four GRCA-required clearances that must be on record in the public OFA database before breeding.
**Database (schema additions to `health_records` table):**
- [ ] Add `test_type` ENUM-style field: `hip_ofa`, `hip_pennhip`, `elbow_ofa`, `heart_ofa`, `heart_echo`, `eye_caer`, `thyroid_ofa`, `dna_panel`
- [ ] Add `result` field: `pass`, `fail`, `carrier`, `clear`, `excellent`, `good`, `fair`, `borderline`
- [ ] Add `ofa_number` VARCHAR — official OFA certification number
- [ ] Add `chic_number` VARCHAR — CHIC certification number (dog-level field on `dogs` table)
- [ ] Add `performed_by` VARCHAR — vet or specialist name
- [ ] Add `expires_at` DATE — for annually-renewed tests (eyes, heart)
- [ ] Add `document_url` VARCHAR — path to uploaded PDF/image
- [ ] Safe ALTER TABLE migration guards for all new columns
**API:**
- [ ] `GET /api/health/:dogId` — list all health records for a dog
- [ ] `POST /api/health` — create health record
- [ ] `PUT /api/health/:id` — update health record
- [ ] `DELETE /api/health/:id` — delete health record
- [ ] `GET /api/health/:dogId/clearance-summary` — returns pass/fail/missing for all 4 OFA tiers
- [ ] `GET /api/health/:dogId/chic-eligible` — returns boolean + missing tests
**UI Components:**
- [ ] `HealthRecordForm` modal — test type dropdown, result, OFA#, date, performed-by, expiry, document upload
- [ ] `HealthTimeline` component — chronological list of all health events per dog on DogDetail page
- [ ] `ClearanceSummaryCard` — shows OFA Hip / Elbow / Heart / Eyes status in a 2x2 grid with color badges (green=pass, yellow=expiring, red=missing/fail)
- [ ] `ChicStatusBadge` — amber badge on dog cards and DogDetail if CHIC number is on file
- [ ] Expiry alert: yellow badge on dog card if any annual test expires within 90 days; red if expired
- [ ] Document upload support (PDF/image) tied to individual health records
**Clearance Tiers Tracked:**
| Test | OFA Minimum Age | Renewal | Notes |
|---|---|---|---|
| Hip Dysplasia | 24 months | Once (final) | OFA eval or PennHIP |
| Elbow Dysplasia | 24 months | Once (final) | OFA eval |
| Cardiac (Heart) | 12 months | Annual recommended | Echo preferred over auscultation |
| Eyes (CAER) | 12 months | **Annual** | Board-certified ACVO ophthalmologist |
| Thyroid (OFA) | 12 months | Annual recommended | Bonus/Tier 2 |
**Complexity:** Medium | **Impact:** High | **User Value:** Excellent
**Estimated Time:** 810 hours
---
### Tier 2 — DNA Genetic Panel *(Priority 2)* 🧬
Embark or equivalent panel results per dog. Allows carrier × clear pairing without producing affected offspring.
**Database:**
- [ ] `genetic_tests` table: `id`, `dog_id`, `test_provider` (embark/optigen/etc), `test_name`, `result` (clear/carrier/affected), `test_date`, `document_url`, `created_at`
- [ ] Safe `CREATE TABLE IF NOT EXISTS` guard
**Golden Retriever Panel — Key Markers:**
- [ ] PRA1 (Progressive Retinal Atrophy type 1)
- [ ] PRA2 (Progressive Retinal Atrophy type 2)
- [ ] prcd-PRA (Progressive Rod-Cone Degeneration)
- [ ] ICH1 / ICH2 (Ichthyosis — very common in Goldens)
- [ ] NCL (Neuronal Ceroid Lipofuscinosis — fatal neurological)
- [ ] DM (Degenerative Myelopathy)
- [ ] MD (Muscular Dystrophy)
- [ ] GR-PRA1, GR-PRA2 (Golden-specific PRA variants)
**API:**
- [ ] `GET /api/genetics/:dogId` — list all genetic test results
- [ ] `POST /api/genetics` — add genetic result
- [ ] `PUT /api/genetics/:id` — update
- [ ] `DELETE /api/genetics/:id` — delete
- [ ] `GET /api/genetics/pairing-risk?sireId=&damId=` — returns at-risk combinations for a trial pairing
**UI Components:**
- [ ] `GeneticTestForm` modal — provider, marker, result (clear/carrier/affected), date, upload
- [ ] `GeneticPanelCard` on DogDetail — color-coded grid of all markers (green=clear, yellow=carrier, red=affected, gray=not tested)
- [ ] Pairing risk overlay on Trial Pairing Simulator — flag if sire+dam are both carriers for same marker
- [ ] "Not Tested" indicator on dog cards when no DNA panel on file
**Complexity:** Medium | **Impact:** High | **User Value:** Excellent
**Estimated Time:** 68 hours
---
### Tier 3 — Cancer Lineage & Longevity Tracking *(Priority 3)* 📊
Golden Retrievers have ~60% cancer mortality rate. Lineage-based cancer history is a major differentiator for responsible breeders.
**Database:**
- [ ] `cancer_history` table: `id`, `dog_id`, `cancer_type`, `age_at_diagnosis`, `age_at_death`, `cause_of_death`, `notes`, `created_at`
- [ ] Add `age_at_death` and `cause_of_death` optional fields to `dogs` table
**API:**
- [ ] `GET /api/health/:dogId/cancer-history`
- [ ] `POST /api/health/cancer-history`
- [ ] `GET /api/pedigree/:dogId/cancer-lineage` — walks ancestors and returns cancer incidence summary
**UI:**
- [ ] Longevity section on DogDetail — age at death, cause of death
- [ ] Cancer lineage indicator on Trial Pairing Simulator — "X of 8 ancestors had cancer history"
- [ ] Optional cancer history entry on DogForm
**Complexity:** Low-Medium | **Impact:** Medium | **User Value:** High (differentiator)
**Estimated Time:** 45 hours
---
### Tier 4 — Breeding Eligibility Checker *(Priority 4)* ✅
Automatic litter eligibility gate based on health clearance status of sire and dam.
**Logic:**
- [ ] Dog is "GRCA eligible" if: Hip OFA ✅ + Elbow OFA ✅ + Heart ✅ + Eyes (non-expired) ✅ + age ≥ 24 months
- [ ] Dog is "CHIC eligible" if all four tests are in OFA public database (CHIC number on file)
- [ ] Warning flags in Trial Pairing Simulator if sire or dam is missing required clearances
- [ ] Block litter creation (with override) if either parent fails eligibility check
**UI:**
- [ ] Eligibility badge on dog cards: `GRCA Eligible` (green) / `Incomplete` (yellow) / `Not Eligible` (red)
- [ ] Eligibility breakdown tooltip on hover — shows which tests are missing
- [ ] Pre-litter warning modal when creating a litter with non-eligible parents
- [ ] CHIC number field + verification note on DogDetail
**Complexity:** Low | **Impact:** High | **User Value:** Excellent
**Estimated Time:** 34 hours
---
## 📋 Phase 5: Advanced Features (PLANNED)
### Pedigree Tools
- [ ] Reverse pedigree (descendants view)
- [ ] PDF pedigree generation
- [ ] Export to standard formats
- [ ] Print-friendly layouts
- [ ] Multi-generation COI analysis
### Breeding Planning
- [ ] Heat cycle predictions (based on cycle history)
- [ ] Expected whelping alerts / push notifications
- [ ] Breeding history reports
- [ ] iCal export for cycle events
### Search & Analytics
- [ ] Advanced search filters
- [ ] By breed, color, age
- [ ] By health clearances
- [ ] By registration status
- [ ] Statistics dashboard
- [ ] Breeding success rates
- [ ] Average litter sizes
- [ ] Popular pairings
---
## 📋 Phase 6: Polish & Optimization (PLANNED)
### User Experience
- [ ] Loading states for all operations
- [ ] Better error messages
- [ ] Confirmation dialogs
- [ ] Undo functionality
- [ ] Keyboard shortcuts
### Performance
- [ ] Image optimization
- [ ] Lazy loading
- [ ] API caching
- [ ] Database query optimization
### Mobile
- [ ] Touch-friendly interface
- [ ] Mobile photo capture
- [ ] Responsive tables
- [ ] Offline mode
### Documentation
- [x] DATABASE.md - Complete schema documentation
- [x] User-facing documentation
- [ ] API documentation
- [ ] Video tutorials
- [ ] FAQ section
---
## Future / Extended Features (BACKLOG)
### Progesterone Tracking *(Moved from Phase 3)*
- [ ] Log progesterone level readings per heat cycle
- [ ] Chart progesterone curve over cycle days
- [ ] LH surge detection
- [ ] Optimal breeding day prediction from levels
### Multi-User Support
- [ ] User authentication
- [ ] Role-based permissions
- [ ] Activity logs
- [ ] Shared access
### Integration
- [ ] Import from other systems
- [ ] Export to Excel/CSV
- [ ] Integration with kennel clubs
- [ ] Backup to cloud storage
- [ ] OFA database lookup by registration number
### Advanced Genetics
- [ ] DNA test result tracking (full Embark import)
- [ ] Genetic diversity analysis
- [ ] Breed-specific calculators
- [ ] Health risk predictions
### Kennel Management
- [ ] Breeding contracts
- [ ] Buyer tracking
- [ ] Financial records
- [ ] Stud service management
---
## 🏃 Current Sprint: v0.7.0 (Phase 4b)
### ✅ Completed This Sprint (v0.6.0)
- [x] `is_champion` flag DB column, API, DogForm toggle, offspring badge, parent dropdown `✪`
- [x] Kennel Settings `settings` table with all kennel fields, `GET/PUT /api/settings`, `SettingsProvider`, navbar kennel name
- [x] `useSettings.jsx` rename (Vite build fix)
- [x] `server/index.js` fix `initDatabase()` no-arg, duplicate health route removed
- [x] `server/routes/settings.js` rewrite: double-encoded base64 + wrong schema fixed
### ✅ Previously Completed (v0.5.1)
- [x] Projected Whelping Calendar Identifier indigo whelp window cells, due label, active card range, jump-to-month button
- [x] Live whelp preview in Cycle Detail modal (client-side, no save required)
- [x] Full-width whelping banner for months with projected whelps
- [x] "Projected Whelp" legend entry + updated page subtitle
### 🔜 Next Up — Phase 4b Build Order
#### Step 1: DB Schema Extensions
- [ ] Extend `health_records` table with OFA-specific columns (test_type, result, ofa_number, chic_number, expires_at, document_url)
@@ -430,45 +37,15 @@ Automatic litter eligibility gate based on health clearance status of sire and d
- [ ] Eligibility badge on dog cards
- [ ] Pre-litter eligibility warning modal
#### Step 6: Cancer / Longevity (Stretch)
- [ ] Cancer history form + lineage summary on Trial Pairing page
- [ ] Age at death / cause of death on DogDetail
### Testing Needed
- [x] Add/edit dog forms with litter selection
- [x] Database schema initialization
- [x] Pedigree tree rendering
- [x] Zoom/pan controls
- [x] UI layout fixes
- [x] Error handling for API failures
- [x] Parent relationship creation via parents table
- [x] Brand logo display and sizing
- [x] Gradient title rendering
- [x] Static asset serving in prod and dev
- [ ] Champion toggle DogForm save/load round-trip
- [ ] Champion badge offspring card display
- [ ] Kennel settings save + navbar name update
- [ ] Trial pairing simulator (end-to-end)
- [ ] Heat cycle calendar (start cycle, detail modal, whelping)
- [ ] Projected whelping calendar identifier (whelp cells, due label, banner)
- [ ] Health records — OFA clearance CRUD
- [ ] Genetic panel — DNA marker entry and display
- [ ] Eligibility checker — badge and litter gate
### Known Issues
- None currently
---
## How to Contribute
## 🕒 Version History & Recent Progress
1. Pick a feature from "Next Up" above
2. Create a feature branch off `master`: `feat/feature-name`
3. Implement with tests
4. Update this roadmap and README.md
5. Submit PR for review
## Version History
- **v0.8.0** (March 12, 2026) - Reverse Pedigree & External Parentage (LATEST)
- [x] **Reverse Pedigree** (descendants view) toggle on Pedigree page
- [x] **External dog parentage** improvements (allowed assigning sire/dam to external dogs)
- [x] **Universal parent selection** (sire/dam dropdowns now include all dogs)
- [x] Updated documentation and roadmap
- **v0.7.0** (In Progress) - Phase 4b: Health & Genetics
- OFA clearance tracking (Hip, Elbow, Heart, Eyes + CHIC number)
@@ -476,58 +53,86 @@ Automatic litter eligibility gate based on health clearance status of sire and d
- Cancer lineage & longevity tracking
- Breeding eligibility checker (GRCA + CHIC gates)
- **v0.6.1** (March 10, 2026) - COI Direct-Relation Fix
- Fixed `calculateCOI` to correctly compute coefficient for parent×offspring pairings (~25%)
- Removed blanket sire exclusion in ancestor mapping logic
- **v0.6.0** (March 9, 2026) - Champion Bloodline, Settings, Build Fixes
- `is_champion` flag on dogs table with ALTER TABLE migration guard
- Champion toggle in DogForm; `✪` suffix in parent dropdowns; offspring badge
- Kennel settings table + `GET/PUT /api/settings` + `SettingsProvider`
- `useSettings.jsx` rename (Vite build fix)
- `server/index.js` fix: `initDatabase()` no-arg, duplicate health route removed
- `server/routes/settings.js` rewrite: double-encoded base64 + wrong schema fixed
- `server/routes/settings.js` rewrite: double-encoded base64 fixed
- **v0.5.1** (March 9, 2026) - Projected Whelping Calendar Identifier
- **v0.5.1** (March 9, 2026) - Projected Whelping Calendar
- Indigo whelp window cells (days 5865) on month grid
- Indigo dot marker on exact expected whelp day (day 63)
- `Baby` icon + "[Name] due" label in whelp day cells
- "Whelp est." range row on active cycle cards
- Jump-to-whelp-month button on cycle cards
- Live whelp preview in Cycle Detail modal (client-side, instant)
- Full-width whelping banner when projected whelps exist
- "Projected Whelp" legend entry + updated page subtitle
- Live whelp preview in Cycle Detail modal
- **v0.5.0** (March 9, 2026) - Breeding Tools Complete
- **v0.5.0** (March 9, 2026) - Breeding Tools
- Trial Pairing Simulator: COI calculator, risk badge, common ancestors
- Heat Cycle Calendar: month grid, phase color coding, start-cycle modal
- Cycle Detail: breeding windows, inline breeding date, whelping estimate
- New API: `GET /heat-cycles`, `GET /heat-cycles/:id/suggestions`
- Progesterone tracking moved to extended backlog
- Heat Cycle Calendar: month grid, phase color coding, suggestions
- **v0.4.1** (March 9, 2026) - Branding & Header Improvements
- Custom br-logo.png in navbar
- Gold-to-rusty-red gradient title
- Static asset serving via Express
- Vite dev proxy for /static
- Route fix for static/uploads paths
- Logo 1:1 aspect ratio fix
---
- **v0.4.0** (March 9, 2026) - Clean Database Schema
- Complete database overhaul with clean normalized design
- Removed migrations, fresh init only
- Parents table for relationships
- Comprehensive documentation
- **v0.3.1** - UI Fixes & Error Handling
- Fixed blank screen issue on Add Dog modal
- Improved parent selection layout
- Added comprehensive error handling
- Enhanced visual design with proper spacing
- **v0.3.0** - Litter Management & Interactive Pedigree
- Added litter_id to dogs table
- Implemented LitterForm component
- Created PedigreeView with React-D3-Tree
- Enhanced DogForm with dual parent selection
- Fixed "no such column: sire" error
- Added comprehensive documentation
- **v0.2.0** - Dog CRUD operations complete
- **v0.1.0** - Initial foundation with API and database
## 📋 Future Roadmap
### ✅ Phase 5: Advanced Features (IN PROGRESS)
- [x] Reverse pedigree (descendants view)
- [ ] PDF pedigree generation
- [ ] Export to standard formats (CSV, JSON)
- [ ] Print-friendly layouts
- [ ] Multi-generation COI analysis
### 📅 Phase 6: Polish & Optimization
- [ ] **User Experience**: Loading states, better error messages, undo functionality
- [ ] **Performance**: Image optimization, lazy loading, API caching
- [ ] **Mobile**: Touch-friendly interface, mobile photo capture
- [ ] **Documentation**: API technical docs, video tutorials
---
## ✅ Completed Milestones
### Phase 1: Foundation
- [x] Docker multi-stage build & SQLite database
- [x] Express.js API server & React 18 frontend
- [x] Parents relationship table for sire/dam tracking
### Phase 2: Core Functionality
- [x] Dog Management (Full CRUD, photo uploads)
- [x] Modern dark theme with glass morphism
- [x] Branded navigation with custom logo
### Phase 3: Breeding Tools
- [x] Interactive pedigree tree visualization (React-D3-Tree)
- [x] Litter Management & linking puppies
- [x] Trial Pairing Simulator & Heat Cycle Calendar
- [x] Projected Whelping identifiers
### Phase 4a: Champion & Settings
- [x] Champion bloodline tracking and badges
- [x] Universal Kennel Settings system
---
## 🏃 Testing & Quality Assurance
- [x] Database schema initialization guards
- [x] Pedigree tree rendering & zoom/pan
- [x] Parent relationship creation logic
- [x] Static asset serving (prod/dev)
- [ ] Champion toggle load/save trip
- [ ] Heat cycle calendar whelping logic
- [ ] Health records OFA clearance CRUD (Upcoming)
---
## How to Contribute
1. Pick a feature from "Next Up" above
2. Create a feature branch off `master`: `feat/feature-name`
3. Implement with tests and update this roadmap
4. Submit PR for review
---
*Last Updated: March 12, 2026*

View File

@@ -1,2 +0,0 @@
123
abc456

View File

@@ -29,8 +29,8 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
const effectiveExternal = isExternal || (dog && dog.is_external)
useEffect(() => {
fetchDogs()
if (!effectiveExternal) {
fetchDogs()
fetchLitters()
}
if (dog) {
@@ -55,7 +55,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
const fetchDogs = async () => {
try {
const res = await axios.get('/api/dogs')
const res = await axios.get('/api/dogs/all')
setDogs(res.data || [])
} catch (e) {
setDogs([])
@@ -64,8 +64,8 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
const fetchLitters = async () => {
try {
const res = await axios.get('/api/litters')
const data = res.data || []
const res = await axios.get('/api/litters', { params: { limit: 200 } })
const data = res.data.data || []
setLitters(data)
setLittersAvailable(data.length > 0)
if (data.length === 0) setUseManualParents(true)
@@ -112,8 +112,8 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
...formData,
is_champion: formData.is_champion ? 1 : 0,
is_external: effectiveExternal ? 1 : 0,
sire_id: effectiveExternal ? null : (formData.sire_id || null),
dam_id: effectiveExternal ? null : (formData.dam_id || null),
sire_id: formData.sire_id || null,
dam_id: formData.dam_id || null,
litter_id: (effectiveExternal || useManualParents) ? null : (formData.litter_id || null),
registration_number: formData.registration_number || null,
birth_date: formData.birth_date || null,
@@ -162,7 +162,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
gap: '0.5rem',
}}>
<ExternalLink size={14} />
External dog not part of your kennel roster. Litter and parent fields are not applicable.
External dog not part of your kennel roster.
</div>
)}
@@ -250,32 +250,31 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
</div>
</div>
{/* Parent Section — hidden for external dogs */}
{!effectiveExternal && (
<div style={{
marginTop: '1.5rem', padding: '1rem',
background: 'rgba(194, 134, 42, 0.04)',
borderRadius: '8px',
border: '1px solid rgba(194, 134, 42, 0.15)'
}}>
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
{/* Parent Section */}
<div style={{
marginTop: '1.5rem', padding: '1rem',
background: 'rgba(194, 134, 42, 0.04)',
borderRadius: '8px',
border: '1px solid rgba(194, 134, 42, 0.15)'
}}>
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
{littersAvailable && (
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
<input type="radio" name="parentMode" checked={!useManualParents}
onChange={() => setUseManualParents(false)} style={{ width: '16px', height: '16px' }} />
<span>Link to Litter</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
<input type="radio" name="parentMode" checked={useManualParents}
onChange={() => setUseManualParents(true)} style={{ width: '16px', height: '16px' }} />
<span>Manual Parent Selection</span>
</label>
</div>
)}
{!effectiveExternal && littersAvailable && (
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
<input type="radio" name="parentMode" checked={!useManualParents}
onChange={() => setUseManualParents(false)} style={{ width: '16px', height: '16px' }} />
<span>Link to Litter</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
<input type="radio" name="parentMode" checked={useManualParents}
onChange={() => setUseManualParents(true)} style={{ width: '16px', height: '16px' }} />
<span>Manual Parent Selection</span>
</label>
</div>
)}
{!useManualParents && littersAvailable ? (
{!useManualParents && littersAvailable && !effectiveExternal ? (
<div className="form-group" style={{ marginTop: '0.5rem' }}>
<label className="label">Select Litter</label>
<select name="litter_id" className="input"
@@ -313,8 +312,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
</div>
</div>
)}
</div>
)}
</div>
<div className="form-group" style={{ marginTop: '1rem' }}>
<label className="label">Notes</label>

View File

@@ -0,0 +1,97 @@
import { useState, useEffect } from 'react'
import { Dna, Plus } from 'lucide-react'
import axios from 'axios'
import GeneticTestForm from './GeneticTestForm'
const RESULT_STYLES = {
clear: { bg: 'rgba(52,199,89,0.15)', color: 'var(--success)' },
carrier: { bg: 'rgba(255,159,10,0.15)', color: 'var(--warning)' },
affected: { bg: 'rgba(255,59,48,0.15)', color: 'var(--danger)' },
not_tested: { bg: 'var(--bg-tertiary)', color: 'var(--text-muted)' }
}
export default function GeneticPanelCard({ dogId }) {
const [data, setData] = useState(null)
const [error, setError] = useState(false)
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [editingRecord, setEditingRecord] = useState(null)
const fetchGenetics = () => {
setLoading(true)
axios.get(`/api/genetics/dog/${dogId}`)
.then(res => setData(res.data))
.catch(() => setError(true))
.finally(() => setLoading(false))
}
useEffect(() => { fetchGenetics() }, [dogId])
const openAdd = () => { setEditingRecord(null); setShowForm(true) }
const openEdit = (rec) => { setEditingRecord(rec); setShowForm(true) }
const handleSaved = () => { setShowForm(false); fetchGenetics() }
if (error || (!loading && !data)) return null
const panel = data?.panel || []
return (
<div className="card" style={{ marginBottom: '1.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2 style={{ fontSize: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', margin: 0, display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Dna size={18} /> DNA Genetics Panel
</h2>
<button className="btn btn-ghost" style={{ fontSize: '0.8rem', padding: '0.35rem 0.75rem' }} onClick={openAdd}>
<Plus size={14} /> Update Marker
</button>
</div>
{loading ? (
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)' }}>Loading...</div>
) : (
<div style={{
display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))', gap: '0.5rem'
}}>
{panel.map(item => {
const style = RESULT_STYLES[item.result] || RESULT_STYLES.not_tested
// Pass the whole test record if it exists so we can edit it
const record = item.id ? item : { marker: item.marker, result: 'not_tested' }
return (
<div
key={item.marker}
onClick={() => openEdit(record)}
style={{
padding: '0.5rem 0.75rem',
background: style.bg,
border: `1px solid ${style.color}44`,
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
transition: 'transform 0.1s ease',
}}
onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-2px)'}
onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'}
>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '0.2rem', fontWeight: 500 }}>
{item.marker}
</div>
<div style={{ fontSize: '0.875rem', color: style.color, fontWeight: 600, textTransform: 'capitalize' }}>
{item.result.replace('_', ' ')}
</div>
</div>
)
})}
</div>
)}
{showForm && (
<GeneticTestForm
dogId={dogId}
record={editingRecord}
onClose={() => setShowForm(false)}
onSave={handleSaved}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,157 @@
import { useState } from 'react'
import { X } from 'lucide-react'
import axios from 'axios'
const GR_MARKERS = [
{ value: 'PRA1', label: 'PRA1' },
{ value: 'PRA2', label: 'PRA2' },
{ value: 'prcd-PRA', label: 'prcd-PRA' },
{ value: 'GR-PRA1', label: 'GR-PRA1' },
{ value: 'GR-PRA2', label: 'GR-PRA2' },
{ value: 'ICH1', label: 'ICH1 (Ichthyosis 1)' },
{ value: 'ICH2', label: 'ICH2 (Ichthyosis 2)' },
{ value: 'NCL', label: 'Neuronal Ceroid Lipofuscinosis' },
{ value: 'DM', label: 'Degenerative Myelopathy' },
{ value: 'MD', label: 'Muscular Dystrophy' }
]
const RESULTS = [
{ value: 'clear', label: 'Clear / Normal' },
{ value: 'carrier', label: 'Carrier (1 copy)' },
{ value: 'affected', label: 'Affected / At Risk (2 copies)' },
{ value: 'not_tested', label: 'Not Tested' }
]
const EMPTY = {
test_provider: 'Embark',
marker: 'PRA1',
result: 'clear',
test_date: '',
document_url: '',
notes: ''
}
export default function GeneticTestForm({ dogId, record, onClose, onSave }) {
const [form, setForm] = useState(record || { ...EMPTY, dog_id: dogId })
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const set = (k, v) => setForm(f => ({ ...f, [k]: v }))
const handleSubmit = async (e) => {
e.preventDefault()
setSaving(true)
setError(null)
// If not tested, don't save
if (form.result === 'not_tested' && !record) {
setError('Cannot save a "Not Tested" result. Please just delete the record if it exists.')
setSaving(false)
return
}
try {
if (record && record.id) {
if (form.result === 'not_tested') {
// If changed to not_tested, just delete it
await axios.delete(`/api/genetics/${record.id}`)
} else {
await axios.put(`/api/genetics/${record.id}`, form)
}
} else {
await axios.post('/api/genetics', { ...form, dog_id: dogId })
}
onSave()
} catch (err) {
setError(err.response?.data?.error || 'Failed to save genetic record')
} finally {
setSaving(false)
}
}
const labelStyle = {
fontSize: '0.8rem', color: 'var(--text-muted)',
marginBottom: '0.25rem', display: 'block',
}
const inputStyle = {
width: '100%', background: 'var(--bg-primary)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
padding: '0.5rem 0.75rem', color: 'var(--text-primary)', fontSize: '0.9rem',
boxSizing: 'border-box',
}
const fw = { display: 'flex', flexDirection: 'column', gap: '0.25rem' }
return (
<div style={{
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)',
backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center',
justifyContent: 'center', zIndex: 1000, padding: '1rem',
}}>
<div className="card" style={{
width: '100%', maxWidth: '500px', maxHeight: '90vh',
overflowY: 'auto', position: 'relative',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h2 style={{ margin: 0 }}>{record && record.id ? 'Edit' : 'Add'} Genetic Result</h2>
<button className="btn-icon" onClick={onClose}><X size={20} /></button>
</div>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={fw}>
<label style={labelStyle}>Marker *</label>
<select style={inputStyle} value={form.marker} onChange={e => set('marker', e.target.value)} disabled={!!record}>
{GR_MARKERS.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</select>
</div>
<div style={fw}>
<label style={labelStyle}>Result *</label>
<select style={inputStyle} value={form.result} onChange={e => set('result', e.target.value)}>
{RESULTS.map(r => <option key={r.value} value={r.value}>{r.label}</option>)}
</select>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={fw}>
<label style={labelStyle}>Provider</label>
<input style={inputStyle} placeholder="Embark, PawPrint, etc." value={form.test_provider}
onChange={e => set('test_provider', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Test Date</label>
<input style={inputStyle} type="date" value={form.test_date}
onChange={e => set('test_date', e.target.value)} />
</div>
</div>
<div style={fw}>
<label style={labelStyle}>Document URL</label>
<input style={inputStyle} type="url" placeholder="Link to PDF or result page" value={form.document_url}
onChange={e => set('document_url', e.target.value)} />
</div>
<div style={fw}>
<label style={labelStyle}>Notes</label>
<textarea style={{ ...inputStyle, minHeight: '60px', resize: 'vertical' }}
value={form.notes} onChange={e => set('notes', e.target.value)} />
</div>
{error && (
<div style={{
color: 'var(--danger)', fontSize: '0.85rem', padding: '0.5rem 0.75rem',
background: 'rgba(255,59,48,0.1)', borderRadius: 'var(--radius-sm)',
}}>{error}</div>
)}
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'flex-end', marginTop: '0.5rem' }}>
<button type="button" className="btn btn-ghost" onClick={onClose}>Cancel</button>
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Saving...' : 'Save Result'}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -39,7 +39,7 @@ function LitterForm({ litter, prefill, onClose, onSave }) {
const fetchDogs = async () => {
try {
const res = await axios.get('/api/dogs')
const res = await axios.get('/api/dogs/all')
setDogs(res.data)
} catch (error) {
console.error('Error fetching dogs:', error)

View File

@@ -46,15 +46,15 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
: (isMale ? 'rgba(59,130,246,0.3)' : 'rgba(236,72,153,0.3)')
const ringColor = isRoot ? rootAccent : nodeColor
const r = isRoot ? 34 : 28
const r = isRoot ? 46 : 38
return (
<g>
{/* Glow halo */}
{/* Glow halo — kept within the circle so it doesn't bleed onto text labels */}
<circle
r={r + 10}
r={r - 4}
fill={glowColor}
style={{ filter: 'blur(6px)' }}
style={{ filter: 'blur(4px)' }}
/>
{/* Outer ring */}
@@ -95,25 +95,25 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
{/* Gender / crown icon */}
<text
fill={isRoot ? '#fff' : '#fff'}
fontSize={isRoot ? 22 : 18}
fontSize={isRoot ? 28 : 24}
textAnchor="middle"
dy="7"
style={{ pointerEvents: 'none', userSelect: 'none' }}
dy="8"
stroke="none"
style={{ fill: '#ffffff', pointerEvents: 'none', userSelect: 'none' }}
>
{isRoot ? '👑' : (isMale ? '♂' : '♀')}
</text>
{/* Name label */}
<text
fill="var(--text-primary, #f5f0e8)"
fontSize={isRoot ? 15 : 13}
fontSize={isRoot ? 22 : 18}
fontWeight={isRoot ? '700' : '600'}
fontFamily="Inter, sans-serif"
textAnchor="middle"
x="0"
y={r + 18}
style={{ pointerEvents: 'none' }}
y={r + 32}
stroke="none"
style={{ fill: isRoot ? '#ffffff' : '#f8fafc', pointerEvents: 'none' }}
>
{nodeDatum.name}
</text>
@@ -121,13 +121,13 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
{/* Breed label (subtle) */}
{breed && (
<text
fill="var(--text-muted, #8c8472)"
fontSize="10"
fontSize="14"
fontFamily="Inter, sans-serif"
textAnchor="middle"
x="0"
y={r + 31}
style={{ pointerEvents: 'none' }}
y={r + 52}
stroke="none"
style={{ fill: '#cbd5e1', pointerEvents: 'none' }}
>
{breed}
</text>
@@ -136,13 +136,13 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
{/* Registration number */}
{nodeDatum.attributes?.registration && (
<text
fill="var(--text-muted, #8c8472)"
fontSize="10"
fontSize="14"
fontFamily="Inter, sans-serif"
textAnchor="middle"
x="0"
y={r + (breed ? 44 : 31)}
style={{ pointerEvents: 'none' }}
y={r + (breed ? 70 : 52)}
stroke="none"
style={{ fill: '#94a3b8', pointerEvents: 'none' }}
>
{nodeDatum.attributes.registration}
</text>
@@ -151,13 +151,13 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
{/* Birth year */}
{nodeDatum.attributes?.birth_year && (
<text
fill="var(--text-muted, #8c8472)"
fontSize="10"
fontSize="14"
fontFamily="Inter, sans-serif"
textAnchor="middle"
x="0"
y={r + (breed ? 57 : (nodeDatum.attributes?.registration ? 44 : 31))}
style={{ pointerEvents: 'none' }}
y={r + (breed ? 88 : (nodeDatum.attributes?.registration ? 70 : 52))}
stroke="none"
style={{ fill: '#94a3b8', pointerEvents: 'none' }}
>
({nodeDatum.attributes.birth_year})
</text>
@@ -232,8 +232,8 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
}}
orientation="horizontal"
pathFunc="step"
separation={{ siblings: 1.6, nonSiblings: 2.2 }}
nodeSize={{ x: 220, y: 160 }}
separation={{ siblings: 1.8, nonSiblings: 2.4 }}
nodeSize={{ x: 280, y: 200 }}
renderCustomNodeElement={renderCustomNode}
enableLegacyTransitions
transitionDuration={300}

View File

@@ -2,7 +2,8 @@
position: relative;
width: 95vw;
height: 90vh;
background: white;
background: var(--bg-primary, #1e1e24);
border: 1px solid var(--border, #333);
border-radius: 12px;
display: flex;
flex-direction: column;
@@ -11,7 +12,7 @@
.pedigree-container {
flex: 1;
background: linear-gradient(to bottom, #f8fafc 0%, #e2e8f0 100%);
background: var(--bg-primary, #1e1e24);
position: relative;
overflow: hidden;
}
@@ -26,8 +27,8 @@
display: flex;
gap: 2rem;
padding: 0.75rem 1.5rem;
background: #f1f5f9;
border-bottom: 1px solid #e2e8f0;
background: var(--bg-elevated, #2a2a35);
border-bottom: 1px solid var(--border, #333);
justify-content: center;
}
@@ -35,8 +36,8 @@
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #475569;
font-size: 1rem;
color: var(--text-secondary, #a1a1aa);
}
.legend-color {
@@ -57,10 +58,10 @@
.pedigree-info {
padding: 0.75rem 1.5rem;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
font-size: 0.875rem;
color: #64748b;
background: var(--bg-elevated, #2a2a35);
border-top: 1px solid var(--border, #333);
font-size: 1rem;
color: var(--text-muted, #a1a1aa);
text-align: center;
}
@@ -69,7 +70,7 @@
}
.pedigree-info strong {
color: #334155;
color: var(--text-primary, #ffffff);
}
/* Override react-d3-tree styles */
@@ -94,12 +95,12 @@
.rd3t-label__title {
font-weight: 600;
fill: #1e293b;
fill: var(--text-primary, #ffffff);
}
.rd3t-label__attributes {
font-size: 0.875rem;
fill: #64748b;
font-size: 1rem;
fill: var(--text-muted, #a1a1aa);
}
/* Loading state */

View File

@@ -17,19 +17,24 @@ function PedigreeView({ dogId, onClose }) {
}, [dogId])
useEffect(() => {
const container = document.querySelector('.pedigree-container')
if (!container) return
const updateDimensions = () => {
const container = document.querySelector('.pedigree-container')
if (container) {
const width = container.offsetWidth
const height = container.offsetHeight
setDimensions({ width, height })
setTranslate({ x: width / 4, y: height / 2 })
}
const width = container.offsetWidth
const height = container.offsetHeight
setDimensions({ width, height })
setTranslate({ x: width / 4, y: height / 2 })
}
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
const resizeObserver = new ResizeObserver(() => {
updateDimensions()
})
resizeObserver.observe(container)
return () => resizeObserver.disconnect()
}, [])
const fetchPedigree = async () => {

View File

@@ -21,21 +21,21 @@ function Dashboard() {
const fetchDashboardData = async () => {
try {
const [dogsRes, littersRes, heatCyclesRes] = await Promise.all([
axios.get('/api/dogs'),
axios.get('/api/litters'),
axios.get('/api/dogs', { params: { page: 1, limit: 8 } }),
axios.get('/api/litters', { params: { page: 1, limit: 1 } }),
axios.get('/api/breeding/heat-cycles/active')
])
const dogs = dogsRes.data
const { data: recentDogsList, stats: dogStats } = dogsRes.data
setStats({
totalDogs: dogs.length,
males: dogs.filter(d => d.sex === 'male').length,
females: dogs.filter(d => d.sex === 'female').length,
totalLitters: littersRes.data.length,
totalDogs: dogStats?.total ?? 0,
males: dogStats?.males ?? 0,
females: dogStats?.females ?? 0,
totalLitters: littersRes.data.total,
activeHeatCycles: heatCyclesRes.data.length
})
setRecentDogs(dogs.slice(0, 8))
setRecentDogs(recentDogsList)
setLoading(false)
} catch (error) {
console.error('Error fetching dashboard data:', error)

View File

@@ -6,6 +6,8 @@ import DogForm from '../components/DogForm'
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
import ClearanceSummaryCard from '../components/ClearanceSummaryCard'
import HealthRecordForm from '../components/HealthRecordForm'
import GeneticPanelCard from '../components/GeneticPanelCard'
import { ShieldCheck } from 'lucide-react'
function DogDetail() {
const { id } = useParams()
@@ -262,6 +264,18 @@ function DogDetail() {
<span className="info-value" style={{ fontFamily: 'monospace' }}>{dog.registration_number}</span>
</div>
)}
{dog.chic_number && (
<div className="info-row">
<span className="info-label"><ShieldCheck size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />CHIC Status</span>
<span className="info-value">
<span style={{
fontSize: '0.75rem', fontWeight: 600, padding: '0.2rem 0.6rem',
background: 'rgba(99,102,241,0.15)', color: '#818cf8',
borderRadius: '999px', border: '1px solid rgba(99,102,241,0.3)'
}}>CHIC #{dog.chic_number}</span>
</span>
</div>
)}
{dog.microchip && (
<div className="info-row">
<span className="info-label"><Hash size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />Microchip</span>
@@ -317,6 +331,9 @@ function DogDetail() {
{/* OFA Clearance Summary */}
<ClearanceSummaryCard dogId={id} onAddRecord={openAddHealth} />
{/* DNA Genetics Panel */}
<GeneticPanelCard dogId={id} />
{/* Health Records List */}
{healthRecords.length > 0 && (
<div className="card" style={{ marginBottom: '1.5rem' }}>

View File

@@ -1,57 +1,69 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Dog, Plus, Search, Calendar, Hash, ArrowRight, Trash2 } from 'lucide-react'
import axios from 'axios'
import DogForm from '../components/DogForm'
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
const LIMIT = 50
function DogList() {
const [dogs, setDogs] = useState([])
const [filteredDogs, setFilteredDogs] = useState([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const [sexFilter, setSexFilter] = useState('all')
const [loading, setLoading] = useState(true)
const [showAddModal, setShowAddModal] = useState(false)
const [deleteTarget, setDeleteTarget] = useState(null) // { id, name }
const [deleting, setDeleting] = useState(false)
const searchTimerRef = useRef(null)
useEffect(() => { fetchDogs() }, [])
useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
useEffect(() => { fetchDogs(1, '', 'all') }, []) // eslint-disable-line
const fetchDogs = async () => {
const fetchDogs = async (p, q, s) => {
setLoading(true)
try {
const res = await axios.get('/api/dogs')
setDogs(res.data)
setLoading(false)
const params = { page: p, limit: LIMIT }
if (q) params.search = q
if (s !== 'all') params.sex = s
const res = await axios.get('/api/dogs', { params })
setDogs(res.data.data)
setTotal(res.data.total)
setPage(p)
} catch (error) {
console.error('Error fetching dogs:', error)
} finally {
setLoading(false)
}
}
const filterDogs = () => {
let filtered = dogs
if (search) {
filtered = filtered.filter(dog =>
dog.name.toLowerCase().includes(search.toLowerCase()) ||
(dog.registration_number && dog.registration_number.toLowerCase().includes(search.toLowerCase()))
)
}
if (sexFilter !== 'all') {
filtered = filtered.filter(dog => dog.sex === sexFilter)
}
setFilteredDogs(filtered)
const handleSearchChange = (value) => {
setSearch(value)
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
searchTimerRef.current = setTimeout(() => fetchDogs(1, value, sexFilter), 300)
}
const handleSave = () => { fetchDogs() }
const handleSexChange = (value) => {
setSexFilter(value)
fetchDogs(1, search, value)
}
const handleClearFilters = () => {
setSearch('')
setSexFilter('all')
fetchDogs(1, '', 'all')
}
const handleSave = () => { fetchDogs(page, search, sexFilter) }
const handleDelete = async () => {
if (!deleteTarget) return
setDeleting(true)
try {
await axios.delete(`/api/dogs/${deleteTarget.id}`)
setDogs(prev => prev.filter(d => d.id !== deleteTarget.id))
setDeleteTarget(null)
fetchDogs(page, search, sexFilter)
} catch (err) {
console.error('Delete failed:', err)
alert('Failed to delete dog. Please try again.')
@@ -60,6 +72,8 @@ function DogList() {
}
}
const totalPages = Math.ceil(total / LIMIT)
const calculateAge = (birthDate) => {
if (!birthDate) return null
const today = new Date()
@@ -85,7 +99,7 @@ function DogList() {
<div>
<h1 style={{ marginBottom: '0.25rem' }}>Dogs</h1>
<p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
{filteredDogs.length} {filteredDogs.length === 1 ? 'dog' : 'dogs'}
{total} {total === 1 ? 'dog' : 'dogs'}
{search || sexFilter !== 'all' ? ' matching filters' : ' total'}
</p>
</div>
@@ -105,11 +119,11 @@ function DogList() {
className="input"
placeholder="Search by name or registration..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ paddingLeft: '2.75rem' }}
/>
</div>
<select className="input" value={sexFilter} onChange={(e) => setSexFilter(e.target.value)} style={{ width: '140px' }}>
<select className="input" value={sexFilter} onChange={(e) => handleSexChange(e.target.value)} style={{ width: '140px' }}>
<option value="all">All Dogs</option>
<option value="male">Males </option>
<option value="female">Females </option>
@@ -117,7 +131,7 @@ function DogList() {
{(search || sexFilter !== 'all') && (
<button
className="btn btn-ghost"
onClick={() => { setSearch(''); setSexFilter('all') }}
onClick={handleClearFilters}
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
>
Clear
@@ -127,7 +141,7 @@ function DogList() {
</div>
{/* Dogs List */}
{filteredDogs.length === 0 ? (
{dogs.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
<Dog size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
<h3 style={{ marginBottom: '0.5rem' }}>
@@ -147,7 +161,7 @@ function DogList() {
</div>
) : (
<div style={{ display: 'grid', gap: '1rem' }}>
{filteredDogs.map(dog => (
{dogs.map(dog => (
<div
key={dog.id}
className="card"
@@ -259,6 +273,19 @@ function DogList() {
{dog.registration_number}
</div>
)}
{dog.chic_number && (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: '0.25rem',
padding: '0.25rem 0.5rem',
background: 'rgba(99,102,241,0.1)',
border: '1px solid rgba(99,102,241,0.3)',
borderRadius: 'var(--radius-sm)',
fontSize: '0.75rem', fontWeight: 600,
color: '#818cf8', marginLeft: '0.5rem'
}}>
CHIC #{dog.chic_number}
</div>
)}
</Link>
{/* Actions */}
@@ -300,6 +327,31 @@ function DogList() {
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '1.5rem' }}>
<button
className="btn btn-ghost"
onClick={() => fetchDogs(page - 1, search, sexFilter)}
disabled={page <= 1 || loading}
style={{ padding: '0.5rem 1rem' }}
>
Previous
</button>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
Page {page} of {totalPages}
</span>
<button
className="btn btn-ghost"
onClick={() => fetchDogs(page + 1, search, sexFilter)}
disabled={page >= totalPages || loading}
style={{ padding: '0.5rem 1rem' }}
>
Next
</button>
</div>
)}
{/* Add Dog Modal */}
{showAddModal && (
<DogForm

View File

@@ -278,7 +278,7 @@ function LitterDetail() {
const fetchAllDogs = async () => {
try {
const res = await axios.get('/api/dogs')
const res = await axios.get('/api/dogs/all')
setAllDogs(res.data)
} catch (err) { console.error('Error fetching dogs:', err) }
}

View File

@@ -4,8 +4,12 @@ import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import LitterForm from '../components/LitterForm'
const LIMIT = 50
function LitterList() {
const [litters, setLitters] = useState([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [editingLitter, setEditingLitter] = useState(null)
@@ -13,7 +17,7 @@ function LitterList() {
const navigate = useNavigate()
useEffect(() => {
fetchLitters()
fetchLitters(1)
// Auto-open form with prefill from BreedingCalendar "Record Litter" CTA
const stored = sessionStorage.getItem('prefillLitter')
if (stored) {
@@ -27,10 +31,12 @@ function LitterList() {
}
}, [])
const fetchLitters = async () => {
const fetchLitters = async (p = page) => {
try {
const res = await axios.get('/api/litters')
setLitters(res.data)
const res = await axios.get('/api/litters', { params: { page: p, limit: LIMIT } })
setLitters(res.data.data)
setTotal(res.data.total)
setPage(p)
} catch (error) {
console.error('Error fetching litters:', error)
} finally {
@@ -38,6 +44,8 @@ function LitterList() {
}
}
const totalPages = Math.ceil(total / LIMIT)
const handleCreate = () => {
setEditingLitter(null)
setPrefill(null)
@@ -56,14 +64,14 @@ function LitterList() {
if (!window.confirm('Delete this litter record? Puppies will be unlinked but not deleted.')) return
try {
await axios.delete(`/api/litters/${id}`)
fetchLitters()
fetchLitters(page)
} catch (error) {
console.error('Error deleting litter:', error)
}
}
const handleSave = () => {
fetchLitters()
fetchLitters(page)
}
if (loading) {
@@ -80,7 +88,7 @@ function LitterList() {
</button>
</div>
{litters.length === 0 ? (
{total === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '4rem' }}>
<Activity size={64} style={{ color: 'var(--text-secondary)', margin: '0 auto 1rem' }} />
<h2>No litters recorded yet</h2>
@@ -143,6 +151,31 @@ function LitterList() {
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '1.5rem' }}>
<button
className="btn btn-ghost"
onClick={() => fetchLitters(page - 1)}
disabled={page <= 1 || loading}
style={{ padding: '0.5rem 1rem' }}
>
Previous
</button>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
Page {page} of {totalPages}
</span>
<button
className="btn btn-ghost"
onClick={() => fetchLitters(page + 1)}
disabled={page >= totalPages || loading}
style={{ padding: '0.5rem 1rem' }}
>
Next
</button>
</div>
)}
{showForm && (
<LitterForm
litter={editingLitter}

View File

@@ -11,6 +11,8 @@ export default function PairingSimulator() {
const [dogsLoading, setDogsLoading] = useState(true)
const [relationWarning, setRelationWarning] = useState(null)
const [relationChecking, setRelationChecking] = useState(false)
const [geneticRisk, setGeneticRisk] = useState(null)
const [geneticChecking, setGeneticChecking] = useState(false)
useEffect(() => {
// include_external=1 ensures external sires/dams appear for pairing
@@ -27,17 +29,28 @@ export default function PairingSimulator() {
const checkRelation = useCallback(async (sid, did) => {
if (!sid || !did) {
setRelationWarning(null)
setGeneticRisk(null)
return
}
setRelationChecking(true)
setGeneticChecking(true)
try {
const res = await fetch(`/api/pedigree/relations/${sid}/${did}`)
const data = await res.json()
setRelationWarning(data.related ? data.relationship : null)
const [relRes, genRes] = await Promise.all([
fetch(`/api/pedigree/relations/${sid}/${did}`),
fetch(`/api/genetics/pairing-risk?sireId=${sid}&damId=${did}`)
])
const relData = await relRes.json()
setRelationWarning(relData.related ? relData.relationship : null)
const genData = await genRes.json()
setGeneticRisk(genData)
} catch {
setRelationWarning(null)
setGeneticRisk(null)
} finally {
setRelationChecking(false)
setGeneticChecking(false)
}
}, [])
@@ -142,7 +155,7 @@ export default function PairingSimulator() {
{relationChecking && (
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
Checking relationship...
Checking relationship and genetics...
</div>
)}
@@ -158,6 +171,31 @@ export default function PairingSimulator() {
</div>
)}
{geneticRisk && geneticRisk.risks && geneticRisk.risks.length > 0 && !geneticChecking && (
<div style={{
padding: '0.6rem 1rem', marginBottom: '0.75rem',
background: 'rgba(255,159,10,0.08)', border: '1px solid rgba(255,159,10,0.3)',
borderRadius: 'var(--radius)', fontSize: '0.875rem', color: 'var(--warning)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem', fontWeight: 600 }}>
<ShieldAlert size={16} /> Genetic Risks Detected
</div>
<ul style={{ margin: 0, paddingLeft: '1.5rem' }}>
{geneticRisk.risks.map(r => (
<li key={r.marker}>
<strong>{r.marker}</strong>: {r.message}
</li>
))}
</ul>
</div>
)}
{geneticRisk && geneticRisk.missing_data && !geneticChecking && (
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.75rem', fontStyle: 'italic' }}>
* Sire or dam has missing genetic tests. Clearances cannot be fully verified.
</div>
)}
<button
type="submit"
className="btn btn-primary"

View File

@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
import axios from 'axios'
import PedigreeTree from '../components/PedigreeTree'
import { transformPedigreeData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
import { transformPedigreeData, transformDescendantData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
function PedigreeView() {
const { id } = useParams()
@@ -14,28 +14,39 @@ function PedigreeView() {
const [pedigreeData, setPedigreeData] = useState(null)
const [coiData, setCoiData] = useState(null)
const [generations, setGenerations] = useState(5)
const [viewMode, setViewMode] = useState('ancestors')
useEffect(() => {
fetchPedigreeData()
}, [id, generations])
}, [id, generations, viewMode])
const fetchPedigreeData = async () => {
setLoading(true)
setError('')
try {
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
const dogData = pedigreeRes.data
setDog(dogData)
if (viewMode === 'ancestors') {
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
const dogData = pedigreeRes.data
setDog(dogData)
const treeData = transformPedigreeData(dogData, generations)
setPedigreeData(treeData)
const treeData = transformPedigreeData(dogData, generations)
setPedigreeData(treeData)
try {
const coiRes = await axios.get(`/api/pedigree/${id}/coi`)
setCoiData(coiRes.data)
} catch (coiError) {
console.warn('COI calculation unavailable:', coiError)
try {
const coiRes = await axios.get(`/api/pedigree/${id}/coi`)
setCoiData(coiRes.data)
} catch (coiError) {
console.warn('COI calculation unavailable:', coiError)
setCoiData(null)
}
} else {
const descendantRes = await axios.get(`/api/pedigree/${id}/descendants?generations=${generations}`)
const dogData = descendantRes.data
setDog(dogData)
const treeData = transformDescendantData(dogData, generations)
setPedigreeData(treeData)
setCoiData(null)
}
@@ -86,7 +97,7 @@ function PedigreeView() {
return (
<div className="container">
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
<button
className="btn btn-secondary"
onClick={() => navigate(`/dogs/${id}`)}
@@ -96,10 +107,10 @@ function PedigreeView() {
Back to Profile
</button>
<div style={{ flex: 1 }}>
<div style={{ flex: 1, minWidth: '200px' }}>
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
{dog?.name}'s Pedigree
{dog?.name}'s {viewMode === 'ancestors' ? 'Pedigree' : 'Descendants'}
</h1>
{dog?.registration_number && (
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
@@ -107,65 +118,86 @@ function PedigreeView() {
</p>
)}
</div>
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', padding: '4px', borderRadius: 'var(--radius)' }}>
<button
className={`btn ${viewMode === 'ancestors' ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setViewMode('ancestors')}
style={{ padding: '0.5rem 1rem' }}
>
Ancestors
</button>
<button
className={`btn ${viewMode === 'descendants' ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setViewMode('descendants')}
style={{ padding: '0.5rem 1rem' }}
>
Descendants
</button>
</div>
</div>
{/* Stats Bar */}
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
{/* COI */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Coefficient of Inbreeding
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: coiInfo.color }}>
{coiInfo.value}
</span>
<span style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: coiInfo.color + '20',
color: coiInfo.color,
textTransform: 'uppercase',
fontWeight: '600'
}}>
{coiInfo.level}
</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
{coiInfo.description}
</div>
</div>
{/* Completeness */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Pedigree Completeness
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--text-primary)' }}>
{completeness}%
</div>
<div style={{ marginTop: '0.5rem' }}>
<div style={{
height: '6px',
background: 'var(--bg-tertiary)',
borderRadius: '3px',
overflow: 'hidden',
border: '1px solid var(--border)'
}}>
<div style={{
height: '100%',
width: `${completeness}%`,
background: barColor,
borderRadius: '3px',
transition: 'width 0.4s ease',
boxShadow: `0 0 6px ${barColor}`
}} />
{viewMode === 'ancestors' && (
<>
{/* COI */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Coefficient of Inbreeding
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: '700', color: coiInfo.color }}>
{coiInfo.value}
</span>
<span style={{
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: coiInfo.color + '20',
color: coiInfo.color,
textTransform: 'uppercase',
fontWeight: '600'
}}>
{coiInfo.level}
</span>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '0.25rem' }}>
{coiInfo.description}
</div>
</div>
</div>
</div>
{/* Completeness */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
Pedigree Completeness
</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--text-primary)' }}>
{completeness}%
</div>
<div style={{ marginTop: '0.5rem' }}>
<div style={{
height: '6px',
background: 'var(--bg-tertiary)',
borderRadius: '3px',
overflow: 'hidden',
border: '1px solid var(--border)'
}}>
<div style={{
height: '100%',
width: `${completeness}%`,
background: barColor,
borderRadius: '3px',
transition: 'width 0.4s ease',
boxShadow: `0 0 6px ${barColor}`
}} />
</div>
</div>
</div>
</>
)}
{/* Generations */}
<div>

View File

@@ -180,4 +180,48 @@ export const getPedigreeCompleteness = (treeData, targetGenerations = 5) => {
const expectedTotal = Math.pow(2, targetGenerations) - 1
const actualCount = countAncestors(treeData)
return Math.min(100, Math.round((actualCount / expectedTotal) * 100))
}
/**
* Transform API descendant data to react-d3-tree format
* @param {Object} dog - Dog object from API with nested offspring array
* @param {number} maxGenerations - Maximum generations to display (default 3)
* @returns {Object} Tree data in react-d3-tree format
*/
export const transformDescendantData = (dog, maxGenerations = 3) => {
if (!dog) return null
const buildTree = (dogData, generation = 0) => {
if (!dogData || generation >= maxGenerations) {
return null
}
const node = {
name: dogData.name || 'Unknown',
attributes: {
id: dogData.id,
sex: dogData.sex,
registration: dogData.registration_number || '',
birth_year: dogData.birth_date ? new Date(dogData.birth_date).getFullYear() : ''
},
children: []
}
if (dogData.offspring && dogData.offspring.length > 0) {
dogData.offspring.forEach(child => {
const childNode = buildTree(child, generation + 1)
if (childNode) {
node.children.push(childNode)
}
})
}
if (node.children.length === 0) {
delete node.children
}
return node
}
return buildTree(dog)
}

View File

@@ -1,304 +0,0 @@
# BREEDR Verification Checklist
## Microchip Field Fix Verification
### ✅ Schema Files (All Correct)
#### 1. Database Schema: `server/db/init.js`
- [x] **Line 29:** `microchip TEXT,` (no UNIQUE constraint)
- [x] **Lines 38-43:** Partial unique index created
```sql
CREATE UNIQUE INDEX IF NOT EXISTS idx_dogs_microchip
ON dogs(microchip)
WHERE microchip IS NOT NULL
```
**Status:** ✅ Correct for future installations
---
#### 2. UI Form: `client/src/components/DogForm.jsx`
- [x] **Line 150:** Microchip input has NO `required` attribute
- [x] Label shows "Microchip Number" (no asterisk)
- [x] Field is truly optional in the UI
**Status:** ✅ Correct - users can leave microchip blank
---
#### 3. Migration Script: `server/db/migrate_microchip.js`
- [x] Exists and is executable
- [x] Safely migrates existing databases
- [x] Idempotent (can run multiple times)
- [x] Preserves all data during migration
**Status:** ✅ Available for existing installations
---
#### 4. Migration Helper: `migrate-now.sh`
- [x] Shell script for easy execution
- [x] Checks if container is running
- [x] Runs migration inside container
- [x] Restarts container after migration
**Status:** ✅ User-friendly migration tool
---
#### 5. Documentation: `docs/MICROCHIP_FIX.md`
- [x] Problem explanation
- [x] Solution details
- [x] Migration instructions (3 options)
- [x] Verification tests
- [x] Troubleshooting guide
**Status:** ✅ Complete documentation
---
#### 6. README: `README.md`
- [x] Migration notice at top
- [x] Link to detailed documentation
- [x] Upgrade instructions included
- [x] Recent updates section added
**Status:** ✅ Users will see migration notice
---
## For Future Installations
### Fresh Install (No Migration Needed)
When a user does a **fresh install** (no existing database):
1. Container starts
2. `server/db/init.js` runs automatically
3. Database created with **correct schema**
4. Microchip field is **optional from the start**
5. No migration required ✅
### Existing Installation (Migration Required)
When a user **upgrades** from an old version:
1. Pull latest code
2. Rebuild Docker image
3. Start container
4. **Must run migration:** `docker exec -it breedr node server/db/migrate_microchip.js`
5. Restart container
6. Microchip field now optional ✅
---
## Testing Checklist
### Test 1: Add Dog Without Microchip
```bash
curl -X POST http://localhost:3000/api/dogs \
-H "Content-Type: application/json" \
-d '{"name":"TestDog1","breed":"Lab","sex":"male"}'
```
**Expected:** ✅ Success (201 Created)
### Test 2: Add Multiple Dogs Without Microchips
```bash
curl -X POST http://localhost:3000/api/dogs \
-H "Content-Type: application/json" \
-d '{"name":"TestDog2","breed":"Lab","sex":"female"}'
```
**Expected:** ✅ Success (multiple NULL values allowed)
### Test 3: Add Dog With Microchip
```bash
curl -X POST http://localhost:3000/api/dogs \
-H "Content-Type: application/json" \
-d '{"name":"TestDog3","breed":"Lab","sex":"male","microchip":"123456789"}'
```
**Expected:** ✅ Success
### Test 4: Duplicate Microchip Should Fail
```bash
curl -X POST http://localhost:3000/api/dogs \
-H "Content-Type: application/json" \
-d '{"name":"TestDog4","breed":"Lab","sex":"female","microchip":"123456789"}'
```
**Expected:** ❌ Error (UNIQUE constraint still enforced for non-NULL)
---
## Database Verification
### Check Schema Directly
```bash
# Enter container
docker exec -it breedr sh
# Open SQLite CLI
sqlite3 /app/data/breedr.db
# Check table schema
.schema dogs
# Should show:
# microchip TEXT, (no UNIQUE)
# Check indexes
.indexes dogs
# Should show:
# idx_dogs_microchip (partial index)
# Verify partial index
SELECT sql FROM sqlite_master
WHERE type='index' AND name='idx_dogs_microchip';
# Should show:
# CREATE UNIQUE INDEX idx_dogs_microchip
# ON dogs(microchip) WHERE microchip IS NOT NULL
```
---
## Rollback Plan
If something goes wrong:
### Option A: Restore Backup
```bash
docker stop breedr
cp /mnt/user/appdata/breedr/breedr.db.backup \
/mnt/user/appdata/breedr/breedr.db
docker start breedr
```
### Option B: Re-run Migration
```bash
docker exec -it breedr node server/db/migrate_microchip.js
docker restart breedr
```
### Option C: Fresh Database (Data Loss)
```bash
docker stop breedr
rm /mnt/user/appdata/breedr/breedr.db
docker start breedr
```
---
## Deployment Verification
After deploying to production:
- [ ] Check container logs for schema initialization
- [ ] Verify database schema with SQLite CLI
- [ ] Test adding dog without microchip via UI
- [ ] Test adding dog with microchip via UI
- [ ] Confirm no UNIQUE constraint errors
- [ ] Verify partial index exists
- [ ] Test duplicate microchip still fails
---
## Common Issues
### Issue: Still Getting UNIQUE Constraint Error
**Cause:** Migration not run on existing database
**Solution:**
```bash
docker exec -it breedr node server/db/migrate_microchip.js
docker restart breedr
```
### Issue: Migration Script Not Found
**Cause:** Old code still in container
**Solution:**
```bash
cd /mnt/user/appdata/breedr-build
git pull
docker build -t breedr:latest .
docker stop breedr && docker rm breedr
# Recreate container with new image
```
### Issue: Microchip Required in UI
**Cause:** Browser cached old JavaScript
**Solution:**
- Hard refresh: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
- Clear browser cache
- Try incognito/private window
---
## File Manifest
### Core Files (Must Be Present)
- ✅ `server/db/init.js` - Database schema (corrected)
- ✅ `server/db/migrate_microchip.js` - Migration script
- ✅ `client/src/components/DogForm.jsx` - UI form (microchip optional)
- ✅ `migrate-now.sh` - Helper script
- ✅ `docs/MICROCHIP_FIX.md` - Detailed documentation
- ✅ `docs/VERIFICATION_CHECKLIST.md` - This file
- ✅ `README.md` - Updated with migration notice
### Validation Commands
```bash
# Check all files exist
ls -la server/db/init.js
ls -la server/db/migrate_microchip.js
ls -la client/src/components/DogForm.jsx
ls -la migrate-now.sh
ls -la docs/MICROCHIP_FIX.md
ls -la docs/VERIFICATION_CHECKLIST.md
# Verify microchip field in init.js
grep -n "microchip TEXT" server/db/init.js
# Should show line 29 with NO UNIQUE
# Verify partial index
grep -A2 "idx_dogs_microchip" server/db/init.js
# Should show WHERE clause
# Verify form has no required
grep -n 'name="microchip"' client/src/components/DogForm.jsx
# Should NOT have required attribute
```
---
## Sign-Off
### Pre-Deployment Checklist
- [x] Schema file correct (no UNIQUE on microchip)
- [x] Partial index created (WHERE IS NOT NULL)
- [x] UI form allows empty microchip
- [x] Migration script tested
- [x] Documentation complete
- [x] README updated
- [x] Tests passing
### Post-Deployment Checklist
- [ ] Container started successfully
- [ ] Schema verified in database
- [ ] UI allows empty microchip
- [ ] Multiple NULL values work
- [ ] Unique constraint still enforced for non-NULL
- [ ] No errors in logs
---
**Last Verified:** March 8, 2026
**Status:** ✅ All files correct for future installations
**Migration Required:** Only for existing databases (one-time)

View File

@@ -157,6 +157,18 @@ function initDatabase() {
)
`);
const geneticMigrations = [
['test_provider', 'TEXT'],
['marker', "TEXT NOT NULL DEFAULT 'unknown'"],
['result', "TEXT NOT NULL DEFAULT 'not_tested'"],
['test_date', 'TEXT'],
['document_url', 'TEXT'],
['notes', 'TEXT']
];
for (const [col, def] of geneticMigrations) {
try { db.exec(`ALTER TABLE genetic_tests ADD COLUMN ${col} ${def}`); } catch (_) {}
}
// ── Cancer History ────────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS cancer_history (
@@ -173,6 +185,17 @@ function initDatabase() {
)
`);
const cancerMigrations = [
['cancer_type', 'TEXT'],
['age_at_diagnosis', 'TEXT'],
['age_at_death', 'TEXT'],
['cause_of_death', 'TEXT'],
['notes', 'TEXT']
];
for (const [col, def] of cancerMigrations) {
try { db.exec(`ALTER TABLE cancer_history ADD COLUMN ${col} ${def}`); } catch (_) {}
}
// ── Settings ──────────────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS settings (

View File

@@ -55,34 +55,72 @@ function attachParents(db, dogs) {
return dogs;
}
// ── GET dogs
// ── GET dogs (paginated)
// Default: kennel dogs only (is_external = 0)
// ?include_external=1 : all active dogs (kennel + external)
// ?external_only=1 : external dogs only
// ?page=1&limit=50 : pagination
// ?search=term : filter by name or registration_number
// ?sex=male|female : filter by sex
// Response: { data, total, page, limit, stats: { total, males, females } }
// ─────────────────────────────────────────────────────────────────────────
router.get('/', (req, res) => {
try {
const db = getDatabase();
const includeExternal = req.query.include_external === '1' || req.query.include_external === 'true';
const externalOnly = req.query.external_only === '1' || req.query.external_only === 'true';
const search = (req.query.search || '').trim();
const sex = req.query.sex === 'male' || req.query.sex === 'female' ? req.query.sex : '';
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const limit = Math.min(200, Math.max(1, parseInt(req.query.limit, 10) || 50));
const offset = (page - 1) * limit;
let whereClause;
let baseWhere;
if (externalOnly) {
whereClause = 'WHERE is_active = 1 AND is_external = 1';
baseWhere = 'is_active = 1 AND is_external = 1';
} else if (includeExternal) {
whereClause = 'WHERE is_active = 1';
baseWhere = 'is_active = 1';
} else {
whereClause = 'WHERE is_active = 1 AND is_external = 0';
baseWhere = 'is_active = 1 AND is_external = 0';
}
const filters = [];
const params = [];
if (search) {
filters.push('(name LIKE ? OR registration_number LIKE ?)');
params.push(`%${search}%`, `%${search}%`);
}
if (sex) {
filters.push('sex = ?');
params.push(sex);
}
const whereClause = 'WHERE ' + [baseWhere, ...filters].join(' AND ');
const total = db.prepare(`SELECT COUNT(*) as count FROM dogs ${whereClause}`).get(...params).count;
const statsWhere = externalOnly
? 'WHERE is_active = 1 AND is_external = 1'
: includeExternal
? 'WHERE is_active = 1'
: 'WHERE is_active = 1 AND is_external = 0';
const stats = db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN sex = 'male' THEN 1 ELSE 0 END) as males,
SUM(CASE WHEN sex = 'female' THEN 1 ELSE 0 END) as females
FROM dogs ${statsWhere}
`).get();
const dogs = db.prepare(`
SELECT ${DOG_COLS}
FROM dogs
${whereClause}
ORDER BY name
`).all();
LIMIT ? OFFSET ?
`).all(...params, limit, offset);
res.json(attachParents(db, dogs));
res.json({ data: attachParents(db, dogs), total, page, limit, stats });
} catch (error) {
console.error('Error fetching dogs:', error);
res.status(500).json({ error: error.message });

View File

@@ -10,6 +10,7 @@ const GRCA_CORE = {
heart: ['heart_ofa', 'heart_echo'],
eye: ['eye_caer'],
};
const VALID_TEST_TYPES = ['hip_ofa', 'hip_pennhip', 'elbow_ofa', 'heart_ofa', 'heart_echo', 'eye_caer', 'thyroid_ofa', 'dna_panel'];
// Helper: compute clearance summary for a dog
function getClearanceSummary(db, dogId) {
@@ -103,17 +104,6 @@ router.get('/dog/:dogId/chic-eligible', (req, res) => {
}
});
// GET single health record
router.get('/:id', (req, res) => {
try {
const db = getDatabase();
const record = db.prepare('SELECT * FROM health_records WHERE id = ?').get(req.params.id);
if (!record) return res.status(404).json({ error: 'Health record not found' });
res.json(record);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST create health record
router.post('/', (req, res) => {
@@ -128,6 +118,10 @@ router.post('/', (req, res) => {
return res.status(400).json({ error: 'dog_id, record_type, and test_date are required' });
}
if (test_type && !VALID_TEST_TYPES.includes(test_type)) {
return res.status(400).json({ error: 'Invalid test_type' });
}
const db = getDatabase();
const dbResult = db.prepare(`
INSERT INTO health_records
@@ -157,6 +151,10 @@ router.put('/:id', (req, res) => {
document_url, result, vet_name, next_due, notes
} = req.body;
if (test_type && !VALID_TEST_TYPES.includes(test_type)) {
return res.status(400).json({ error: 'Invalid test_type' });
}
const db = getDatabase();
db.prepare(`
UPDATE health_records
@@ -190,4 +188,70 @@ router.delete('/:id', (req, res) => {
}
});
// GET cancer history for a dog
router.get('/dog/:dogId/cancer-history', (req, res) => {
try {
const db = getDatabase();
const records = db.prepare(`
SELECT * FROM cancer_history
WHERE dog_id = ?
ORDER BY age_at_diagnosis ASC, created_at DESC
`).all(req.params.dogId);
res.json(records);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST create cancer history record
router.post('/cancer-history', (req, res) => {
try {
const {
dog_id, cancer_type, age_at_diagnosis, age_at_death, cause_of_death, notes
} = req.body;
if (!dog_id || !cancer_type) {
return res.status(400).json({ error: 'dog_id and cancer_type are required' });
}
const db = getDatabase();
// Update dog's age_at_death and cause_of_death if provided
if (age_at_death || cause_of_death) {
db.prepare(`
UPDATE dogs SET
age_at_death = COALESCE(?, age_at_death),
cause_of_death = COALESCE(?, cause_of_death)
WHERE id = ?
`).run(age_at_death || null, cause_of_death || null, dog_id);
}
const dbResult = db.prepare(`
INSERT INTO cancer_history
(dog_id, cancer_type, age_at_diagnosis, age_at_death, cause_of_death, notes)
VALUES (?, ?, ?, ?, ?, ?)
`).run(
dog_id, cancer_type, age_at_diagnosis || null,
age_at_death || null, cause_of_death || null, notes || null
);
const record = db.prepare('SELECT * FROM cancer_history WHERE id = ?').get(dbResult.lastInsertRowid);
res.status(201).json(record);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET single health record (wildcard should go last to prevent overlap)
router.get('/:id', (req, res) => {
try {
const db = getDatabase();
const record = db.prepare('SELECT * FROM health_records WHERE id = ?').get(req.params.id);
if (!record) return res.status(404).json({ error: 'Health record not found' });
res.json(record);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -2,31 +2,50 @@ const express = require('express');
const router = express.Router();
const { getDatabase } = require('../db/init');
// GET all litters
// GET all litters (paginated)
// ?page=1&limit=50
// Response: { data, total, page, limit }
router.get('/', (req, res) => {
try {
const db = getDatabase();
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const limit = Math.min(200, Math.max(1, parseInt(req.query.limit, 10) || 50));
const offset = (page - 1) * limit;
const total = db.prepare('SELECT COUNT(*) as count FROM litters').get().count;
const litters = db.prepare(`
SELECT l.*,
SELECT l.*,
s.name as sire_name, s.registration_number as sire_reg,
d.name as dam_name, d.registration_number as dam_reg
FROM litters l
JOIN dogs s ON l.sire_id = s.id
JOIN dogs d ON l.dam_id = d.id
ORDER BY l.breeding_date DESC
`).all();
litters.forEach(litter => {
litter.puppies = db.prepare(`
SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
`).all(litter.id);
litter.puppies.forEach(puppy => {
puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : [];
LIMIT ? OFFSET ?
`).all(limit, offset);
if (litters.length > 0) {
const litterIds = litters.map(l => l.id);
const placeholders = litterIds.map(() => '?').join(',');
const allPuppies = db.prepare(`
SELECT * FROM dogs WHERE litter_id IN (${placeholders}) AND is_active = 1
`).all(...litterIds);
const puppiesByLitter = {};
allPuppies.forEach(p => {
p.photo_urls = p.photo_urls ? JSON.parse(p.photo_urls) : [];
if (!puppiesByLitter[p.litter_id]) puppiesByLitter[p.litter_id] = [];
puppiesByLitter[p.litter_id].push(p);
});
litter.actual_puppy_count = litter.puppies.length;
});
res.json(litters);
litters.forEach(l => {
l.puppies = puppiesByLitter[l.id] || [];
l.actual_puppy_count = l.puppies.length;
});
}
res.json({ data: litters, total, page, limit });
} catch (error) {
res.status(500).json({ error: error.message });
}

View File

@@ -2,6 +2,25 @@ const express = require('express');
const router = express.Router();
const { getDatabase } = require('../db/init');
const MAX_CACHE_SIZE = 1000;
const ancestorCache = new Map();
const coiCache = new Map();
function getFromCache(cache, key, computeFn) {
if (cache.has(key)) {
const val = cache.get(key);
cache.delete(key);
cache.set(key, val);
return val;
}
const val = computeFn();
if (cache.size >= MAX_CACHE_SIZE) {
cache.delete(cache.keys().next().value);
}
cache.set(key, val);
return val;
}
/**
* getAncestorMap(db, dogId, maxGen)
* Returns Map<id, [{ id, name, generation }, ...]>
@@ -9,24 +28,27 @@ const { getDatabase } = require('../db/init');
* pairings are correctly detected by calculateCOI.
*/
function getAncestorMap(db, dogId, maxGen = 6) {
const map = new Map();
const cacheKey = `${dogId}-${maxGen}`;
return getFromCache(ancestorCache, cacheKey, () => {
const map = new Map();
function recurse(id, gen) {
if (gen > maxGen) return;
const dog = db.prepare('SELECT id, name FROM dogs WHERE id = ?').get(id);
if (!dog) return;
if (!map.has(id)) map.set(id, []);
map.get(id).push({ id: dog.id, name: dog.name, generation: gen });
if (map.get(id).length === 1) {
const parents = db.prepare(`
SELECT p.parent_id FROM parents p WHERE p.dog_id = ?
`).all(id);
parents.forEach(p => recurse(p.parent_id, gen + 1));
function recurse(id, gen) {
if (gen > maxGen) return;
const dog = db.prepare('SELECT id, name FROM dogs WHERE id = ?').get(id);
if (!dog) return;
if (!map.has(id)) map.set(id, []);
map.get(id).push({ id: dog.id, name: dog.name, generation: gen });
if (map.get(id).length === 1) {
const parents = db.prepare(`
SELECT p.parent_id FROM parents p WHERE p.dog_id = ?
`).all(id);
parents.forEach(p => recurse(p.parent_id, gen + 1));
}
}
}
recurse(parseInt(dogId), 0);
return map;
recurse(parseInt(dogId), 0);
return map;
});
}
/**
@@ -68,54 +90,57 @@ function isDirectRelation(db, sireId, damId) {
* self-loops.
*/
function calculateCOI(db, sireId, damId) {
const sid = parseInt(sireId);
const did = parseInt(damId);
const sireMap = getAncestorMap(db, sid);
const damMap = getAncestorMap(db, did);
const cacheKey = `${sireId}-${damId}`;
return getFromCache(coiCache, cacheKey, () => {
const sid = parseInt(sireId);
const did = parseInt(damId);
const sireMap = getAncestorMap(db, sid);
const damMap = getAncestorMap(db, did);
// Common ancestors: in BOTH maps, but:
// - not the dam itself appearing in sireMap (would be a loop)
// - not the sire itself appearing in damMap already handled below
// We collect all IDs present in both, excluding only the direct
// subjects (did from sireMap side, sid excluded already since we
// iterate sireMap keys — but sid IS in sireMap at gen 0, and if
// damMap also has sid, that is the parent×offspring case we WANT).
const commonIds = [...sireMap.keys()].filter(
id => damMap.has(id) && id !== did
);
// Common ancestors: in BOTH maps, but:
// - not the dam itself appearing in sireMap (would be a loop)
// - not the sire itself appearing in damMap already handled below
// We collect all IDs present in both, excluding only the direct
// subjects (did from sireMap side, sid excluded already since we
// iterate sireMap keys — but sid IS in sireMap at gen 0, and if
// damMap also has sid, that is the parent×offspring case we WANT).
const commonIds = [...sireMap.keys()].filter(
id => damMap.has(id) && id !== did
);
let coi = 0;
const processedPaths = new Set();
const commonAncestorList = [];
let coi = 0;
const processedPaths = new Set();
const commonAncestorList = [];
commonIds.forEach(ancId => {
const sireOccs = sireMap.get(ancId);
const damOccs = damMap.get(ancId);
commonIds.forEach(ancId => {
const sireOccs = sireMap.get(ancId);
const damOccs = damMap.get(ancId);
sireOccs.forEach(so => {
damOccs.forEach(do_ => {
const key = `${ancId}-${so.generation}-${do_.generation}`;
if (!processedPaths.has(key)) {
processedPaths.add(key);
coi += Math.pow(0.5, so.generation + do_.generation + 1);
}
sireOccs.forEach(so => {
damOccs.forEach(do_ => {
const key = `${ancId}-${so.generation}-${do_.generation}`;
if (!processedPaths.has(key)) {
processedPaths.add(key);
coi += Math.pow(0.5, so.generation + do_.generation + 1);
}
});
});
const closestSire = sireOccs.reduce((a, b) => a.generation < b.generation ? a : b);
const closestDam = damOccs.reduce((a, b) => a.generation < b.generation ? a : b);
commonAncestorList.push({
id: ancId,
name: sireOccs[0].name,
sireGen: closestSire.generation,
damGen: closestDam.generation
});
});
const closestSire = sireOccs.reduce((a, b) => a.generation < b.generation ? a : b);
const closestDam = damOccs.reduce((a, b) => a.generation < b.generation ? a : b);
commonAncestorList.push({
id: ancId,
name: sireOccs[0].name,
sireGen: closestSire.generation,
damGen: closestDam.generation
});
return {
coefficient: coi,
commonAncestors: commonAncestorList
};
});
return {
coefficient: coi,
commonAncestors: commonAncestorList
};
}
// =====================================================================
@@ -124,8 +149,7 @@ function calculateCOI(db, sireId, damId) {
// 'trial-pairing' as dog IDs and return 404/wrong data.
// =====================================================================
// POST /api/pedigree/trial-pairing (alias for /coi)
router.post(['/trial-pairing', '/coi'], (req, res) => {
const handleTrialPairing = (req, res) => {
try {
const { sire_id, dam_id } = req.body;
if (!sire_id || !dam_id) {
@@ -156,7 +180,13 @@ router.post(['/trial-pairing', '/coi'], (req, res) => {
} catch (error) {
res.status(500).json({ error: error.message });
}
});
};
// POST /api/pedigree/trial-pairing
router.post('/trial-pairing', handleTrialPairing);
// POST /api/pedigree/coi
router.post('/coi', handleTrialPairing);
// GET /api/pedigree/:id/coi
router.get('/:id/coi', (req, res) => {
@@ -190,6 +220,63 @@ router.get('/relations/:sireId/:damId', (req, res) => {
}
});
// GET /api/pedigree/:id/cancer-lineage
router.get('/:id/cancer-lineage', (req, res) => {
try {
const db = getDatabase();
// Get ancestor map up to 5 generations
const ancestorMap = getAncestorMap(db, req.params.id, 5);
// Collect all unique ancestor IDs
const ancestorIds = Array.from(ancestorMap.keys());
if (ancestorIds.length === 0) {
return res.json({ lineage_cases: [], stats: { total_ancestors: 0, ancestors_with_cancer: 0 } });
}
// Query cancer history for all ancestors
const placeholders = ancestorIds.map(() => '?').join(',');
const cancerRecords = db.prepare(`
SELECT c.*, d.name, d.sex
FROM cancer_history c
JOIN dogs d ON c.dog_id = d.id
WHERE c.dog_id IN (${placeholders})
`).all(...ancestorIds);
// Structure the response
const cases = cancerRecords.map(record => {
// Find the closest generation this ancestor appears in
const occurrences = ancestorMap.get(record.dog_id);
const closestGen = occurrences.reduce((min, occ) => occ.generation < min ? occ.generation : min, 999);
return {
...record,
generation_distance: closestGen
};
});
// Sort by generation distance (closer relatives first)
cases.sort((a, b) => a.generation_distance - b.generation_distance);
// Count unique dogs with cancer (excluding generation 0 if we only want stats on ancestors)
const ancestorCases = cases.filter(c => c.generation_distance > 0);
const uniqueAncestorsWithCancer = new Set(ancestorCases.map(c => c.dog_id)).size;
// Number of ancestors is total unique IDs minus 1 for the dog itself
const numAncestors = ancestorIds.length > 0 && ancestorMap.get(parseInt(req.params.id)) ? ancestorIds.length - 1 : ancestorIds.length;
res.json({
lineage_cases: cases,
stats: {
total_ancestors: numAncestors,
ancestors_with_cancer: uniqueAncestorsWithCancer
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// =====================================================================
// Wildcard routes last
// =====================================================================

26
server/test_app.js Normal file
View File

@@ -0,0 +1,26 @@
const app = require('./index');
const http = require('http');
// Start temporary server
const server = http.createServer(app);
server.listen(3030, async () => {
console.log('Server started on 3030');
try {
const res = await fetch('http://localhost:3030/api/pedigree/relations/1/2');
const text = await res.text();
console.log('GET /api/pedigree/relations/1/2 RESPONSE:', res.status, text.substring(0, 150));
const postRes = await fetch('http://localhost:3030/api/pedigree/trial-pairing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sire_id: 1, dam_id: 2 })
});
const postText = await postRes.text();
console.log('POST /api/pedigree/trial-pairing RESPONSE:', postRes.status, postText.substring(0, 150));
} catch (err) {
console.error('Fetch error:', err);
} finally {
server.close();
process.exit(0);
}
});

8
server/test_express.js Normal file
View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
router.post(['/a', '/b'], (req, res) => {
res.send('ok');
});
console.log('Started successfully');