Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 761387388d | |||
| 4394286d0b | |||
|
|
b8633863b0 | ||
|
|
fa7a336588 | ||
| c3696ba015 | |||
| c483096c63 | |||
| e4e3b44fcf | |||
| 78e15d08af | |||
| 454665b9cb | |||
| d8557fcfca | |||
| 5f68ca0e8b | |||
| 42bab14ac3 | |||
| 5ca594fdc7 | |||
| 13185a5281 | |||
| 17b008a674 | |||
|
|
9b3210a81e | ||
| 81357e87ae | |||
|
|
8abd5e2db6 | ||
| a63617d9c0 | |||
|
|
7195aaecfc | ||
| 34bf29d8bf | |||
|
|
4f3074b1f4 | ||
| 3c7ba1775f | |||
|
|
0a0a5d232c | ||
|
|
58b53c981e |
25
.gitea/workflows/docker-build.yml
Normal file
25
.gitea/workflows/docker-build.yml
Normal 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
|
||||
@@ -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
2
API.md
@@ -1,4 +1,4 @@
|
||||
# BREEDR API Documentation (v0.6.1)
|
||||
# BREEDR API Documentation (v0.8.0)
|
||||
|
||||
Base URL: `/api`
|
||||
|
||||
|
||||
115
CLEANUP_NOTES.md
115
CLEANUP_NOTES.md
@@ -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
|
||||
222
DATABASE.md
222
DATABASE.md
@@ -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.
|
||||
161
DEVELOPMENT.md
161
DEVELOPMENT.md
@@ -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*
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -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
|
||||
|
||||
@@ -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.
|
||||
340
QUICKSTART.md
340
QUICKSTART.md
@@ -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
391
README.md
@@ -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 58–65 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 earliest–latest 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 58–65) 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 earliest–latest 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)
|
||||
|
||||
@@ -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)
|
||||
553
ROADMAP.md
553
ROADMAP.md
@@ -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 (Sun–Sat) 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 58–63) 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:** 8–10 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:** 6–8 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:** 4–5 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:** 3–4 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 58–65) 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*
|
||||
|
||||
@@ -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>
|
||||
|
||||
97
client/src/components/GeneticPanelCard.jsx
Normal file
97
client/src/components/GeneticPanelCard.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
157
client/src/components/GeneticTestForm.jsx
Normal file
157
client/src/components/GeneticTestForm.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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
26
server/test_app.js
Normal 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
8
server/test_express.js
Normal 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');
|
||||
Reference in New Issue
Block a user