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`
|
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
|
## Tech Stack Overview
|
||||||
|
|
||||||
- **Monorepo Structure**:
|
### Backend
|
||||||
- `server/`: Express.js backend.
|
- **Node.js & Express**: Core API server.
|
||||||
- `client/`: React/Vite frontend.
|
- **better-sqlite3**: High-performance SQLite driver.
|
||||||
- `data/`: SQLite database storage.
|
- **Multer**: Multi-part form data handling for photo uploads.
|
||||||
- `uploads/`: Uploaded images and documents.
|
- **Bcrypt & JWT**: (Planned) Authentication and security.
|
||||||
- `static/`: Static assets for the application.
|
|
||||||
|
|
||||||
- **Backend**: Node.js, Express, better-sqlite3, multer, bcrypt, jsonwebtoken.
|
### Frontend
|
||||||
- **Frontend**: React 18, Vite, React Router 6, Axios, Lucide React, D3 (for pedigree trees).
|
- **React 18 & Vite**: Modern reactive UI with fast HMR.
|
||||||
- **Database**: SQLite (managed with better-sqlite3).
|
- **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
|
## Database Architecture
|
||||||
|
|
||||||
### Data Storage
|
### SQLite Implementation
|
||||||
The database is a single SQLite file located at `data/breedr.db`. This directory is automatically created on startup if it doesn't exist.
|
The database is a single file located at `data/breedr.db`. This directory is automatically created on startup.
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### "Parents Table" Approach
|
### "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
|
**Benefits**: Supports recursive lookups, avoids `ALTER TABLE` complexity for lineage changes, and allows historical mapping of ancestors without full profiles.
|
||||||
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)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Benefits**:
|
### Safe Migrations
|
||||||
- Avoids complex `ALTER TABLE` operations when changing pedigree logic.
|
BREEDR use a migration-free synchronization approach:
|
||||||
- Cleanly separates dog attributes from lineage relationships.
|
1. `server/db/init.js` defines the latest table structures.
|
||||||
- Supports indexing for fast recursive pedigree lookups.
|
2. Safe `ALTER TABLE` guards inject missing columns on startup.
|
||||||
|
3. This ensures data persistence across updates without manual migration scripts.
|
||||||
|
|
||||||
### Key Tables
|
### Key Tables
|
||||||
- **`dogs`**: Core dog data (name, breed, sex, microchip, etc.).
|
- `dogs`: Registry for kennel and external dogs.
|
||||||
- **`parents`**: Lineage relationships (Sire/Dam).
|
- `parents`: Ancestry relationships.
|
||||||
- **`litters`**: Groups of dogs from a single breeding.
|
- `litters`: Produced breeding groups.
|
||||||
- **`breeding_records`**: Planned or completed breeding events.
|
- `health_records`: OFA clearances and vet records.
|
||||||
- **`health_records`**: OFA results, vaccinations, and other health tests.
|
- `genetic_tests`: DNA panel results.
|
||||||
- **`genetic_tests`**: DNA panel results.
|
- `settings`: Kennel-wide configuration (single row).
|
||||||
- **`settings`**: Kennel-wide configurations.
|
|
||||||
|
|
||||||
## Backend Development
|
---
|
||||||
|
|
||||||
### API Routes
|
## Frontend Documentation
|
||||||
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.
|
|
||||||
|
|
||||||
### File Uploads
|
### Project Structure
|
||||||
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`).
|
```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
|
### Key Components
|
||||||
- **Settings**: Managed globally via `SettingsProvider` in `client/src/hooks/useSettings.jsx`.
|
- **PedigreeTree**: horizontal, D3-powered tree with zoom/pan.
|
||||||
- **Component State**: Local `useState` and `useEffect` are preferred for feature-specific data.
|
- **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
|
## API & Backend Development
|
||||||
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.
|
|
||||||
|
|
||||||
## 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 |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `PORT` | Server port | `3000` |
|
| `PORT` | Server port | `3000` |
|
||||||
| `NODE_ENV` | Environment mode | `development` |
|
| `DB_PATH` | Path to .db file | `../data/breedr.db` |
|
||||||
| `DATA_DIR` | Path to DB storage | `../data` |
|
| `UPLOAD_PATH`| Path to photo storage| `../uploads` |
|
||||||
| `UPLOAD_PATH` | Path to uploads | `../uploads` |
|
|
||||||
| `STATIC_PATH` | Path to static assets | `../static` |
|
---
|
||||||
| `DB_PATH` | Full path to .db file | `../data/breedr.db` |
|
|
||||||
|
## 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 code
|
||||||
COPY server/ ./server/
|
COPY server/ ./server/
|
||||||
|
|
||||||
|
# Copy static assets (branding, etc.) to ensure default logo is present
|
||||||
|
COPY static/ ./static/
|
||||||
|
|
||||||
# Copy built frontend from previous stage
|
# Copy built frontend from previous stage
|
||||||
COPY --from=frontend-builder /app/client/dist ./client/dist
|
COPY --from=frontend-builder /app/client/dist ./client/dist
|
||||||
|
|
||||||
# Create necessary directories (including static for branding assets)
|
# Create data and uploads directories
|
||||||
RUN mkdir -p /app/data /app/uploads /app/static
|
RUN mkdir -p /app/data /app/uploads
|
||||||
|
|
||||||
# Initialize database schema on build
|
|
||||||
RUN node server/db/init.js || true
|
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV NODE_ENV=production
|
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.
|
A reactive, interactive dog breeding genealogy mapping system for professional kennel management.
|
||||||
|
|
||||||
## ✅ Current Features
|
---
|
||||||
|
|
||||||
### Core Functionality
|
## 🌟 Recent Highlights (v0.8.0)
|
||||||
- **✅ Dog Registry** - Complete CRUD operations with comprehensive profiles
|
- **✅ Reverse Pedigree** — Toggle between ancestors and descendants view for full lineage tracking.
|
||||||
- **✅ Photo Management** - Multiple photos per dog with upload/delete capabilities
|
- **✅ External Dog Mapping** — Assign parents to external dogs, allowing for full genealogy of outside lines.
|
||||||
- **✅ Parent Relationships** - Clean database design using `parents` table (no sire/dam columns in dogs table)
|
- **✅ Universal Parent Selection** — Select any dog (kennel or external) as a sire/dam from any profile.
|
||||||
- **✅ 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
|
|
||||||
|
|
||||||
### 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)
|
## 🚀 Quick Start
|
||||||
- **✅ 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)
|
|
||||||
|
|
||||||
|
### 1. Docker Deployment (Recommended)
|
||||||
```bash
|
```bash
|
||||||
# Clone repository
|
|
||||||
git clone https://git.alwisp.com/jason/breedr.git
|
git clone https://git.alwisp.com/jason/breedr.git
|
||||||
cd breedr
|
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
|
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.
|
### 2. Manual Development Setup
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Run development server (frontend + backend, nodemon auto-reload)
|
|
||||||
npm run dev
|
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/
|
breedr/
|
||||||
├── client/ # React frontend
|
├── client/ # React frontend (Pages: Pedigree, Pairing, Calendar, Settings)
|
||||||
│ ├── src/
|
├── server/ # Node.js backend (Routes: Dogs, Pedigree, Breeding, Settings)
|
||||||
│ │ ├── pages/
|
├── static/ # Branded assets (logos, etc.)
|
||||||
│ │ │ ├── BreedingCalendar.jsx # Heat cycle calendar + whelping identifiers
|
├── data/ # SQLite database storage (mapped in Docker)
|
||||||
│ │ │ ├── PairingSimulator.jsx # Trial pairing + COI
|
├── uploads/ # Dog photo storage (mapped in Docker)
|
||||||
│ │ │ ├── SettingsPage.jsx # Kennel settings form
|
└── docs/ # Technical documentation and design history
|
||||||
│ │ │ ├── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints
|
---
|
||||||
|
|
||||||
### Dogs
|
## 🕒 Release Summary
|
||||||
- `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
|
|
||||||
|
|
||||||
### Settings
|
- **v0.8.0** (Mar 2026): Reverse Pedigree & External dog parentage.
|
||||||
- `GET /api/settings` - Get kennel settings
|
- **v0.7.0** (In Progress): Health & Genetics (OFA clearances, DNA panels).
|
||||||
- `PUT /api/settings` - Update kennel settings (partial update supported)
|
- **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
|
## ❓ Troubleshooting
|
||||||
- `GET /api/breeding/heat-cycles` - All heat cycles
|
- **COI shows 0.00%?**: Ensure both parents are mapped and have shared ancestors.
|
||||||
- `GET /api/breeding/heat-cycles/active` - Active cycles with dog info
|
- **Missing Columns?**: Restart the server; auto-init guards add columns automatically.
|
||||||
- `GET /api/breeding/heat-cycles/dog/:dogId` - Cycles for a specific dog
|
- **Logo not appearing?**: Place `br-logo.png` in the `static/` directory.
|
||||||
- `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
|
|
||||||
|
|
||||||
### Litters
|
---
|
||||||
- `GET/POST /api/litters` - Litter management
|
|
||||||
|
|
||||||
### Assets
|
**Full Documentation**:
|
||||||
- `GET /static/*` - Branding and static assets
|
[Installation Guide](INSTALL.md) | [Development & Architecture](DEVELOPMENT.md) | [API Reference](API.md) | [Roadmap](ROADMAP.md)
|
||||||
- `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
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
547
ROADMAP.md
547
ROADMAP.md
@@ -1,402 +1,9 @@
|
|||||||
# BREEDR Development Roadmap
|
# BREEDR Development Roadmap (v0.8.0)
|
||||||
|
|
||||||
## ✅ Phase 1: Foundation (COMPLETE)
|
## 🚀 Current Status: v0.8.0 (Active Development)
|
||||||
|
|
||||||
### 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)
|
|
||||||
|
|
||||||
|
### 🔜 Next Up — Phase 4b: Health & Genetics Build Order
|
||||||
> **Context:** Golden Retriever health clearances follow GRCA Code of Ethics and OFA/CHIC standards.
|
> **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
|
#### Step 1: DB Schema Extensions
|
||||||
- [ ] Extend `health_records` table with OFA-specific columns (test_type, result, ofa_number, chic_number, expires_at, document_url)
|
- [ ] 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
|
- [ ] Eligibility badge on dog cards
|
||||||
- [ ] Pre-litter eligibility warning modal
|
- [ ] 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
|
- **v0.8.0** (March 12, 2026) - Reverse Pedigree & External Parentage (LATEST)
|
||||||
2. Create a feature branch off `master`: `feat/feature-name`
|
- [x] **Reverse Pedigree** (descendants view) toggle on Pedigree page
|
||||||
3. Implement with tests
|
- [x] **External dog parentage** improvements (allowed assigning sire/dam to external dogs)
|
||||||
4. Update this roadmap and README.md
|
- [x] **Universal parent selection** (sire/dam dropdowns now include all dogs)
|
||||||
5. Submit PR for review
|
- [x] Updated documentation and roadmap
|
||||||
|
|
||||||
## Version History
|
|
||||||
|
|
||||||
- **v0.7.0** (In Progress) - Phase 4b: Health & Genetics
|
- **v0.7.0** (In Progress) - Phase 4b: Health & Genetics
|
||||||
- OFA clearance tracking (Hip, Elbow, Heart, Eyes + CHIC number)
|
- 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
|
- Cancer lineage & longevity tracking
|
||||||
- Breeding eligibility checker (GRCA + CHIC gates)
|
- 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
|
- **v0.6.0** (March 9, 2026) - Champion Bloodline, Settings, Build Fixes
|
||||||
- `is_champion` flag on dogs table with ALTER TABLE migration guard
|
- `is_champion` flag on dogs table with ALTER TABLE migration guard
|
||||||
- Champion toggle in DogForm; `✪` suffix in parent dropdowns; offspring badge
|
- Champion toggle in DogForm; `✪` suffix in parent dropdowns; offspring badge
|
||||||
- Kennel settings table + `GET/PUT /api/settings` + `SettingsProvider`
|
- Kennel settings table + `GET/PUT /api/settings` + `SettingsProvider`
|
||||||
- `useSettings.jsx` rename (Vite build fix)
|
- `useSettings.jsx` rename (Vite build fix)
|
||||||
- `server/index.js` fix: `initDatabase()` no-arg, duplicate health route removed
|
- `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 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
|
- `Baby` icon + "[Name] due" label in whelp day cells
|
||||||
- "Whelp est." range row on active cycle cards
|
- Live whelp preview in Cycle Detail modal
|
||||||
- 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
|
|
||||||
|
|
||||||
- **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
|
- Trial Pairing Simulator: COI calculator, risk badge, common ancestors
|
||||||
- Heat Cycle Calendar: month grid, phase color coding, start-cycle modal
|
- Heat Cycle Calendar: month grid, phase color coding, suggestions
|
||||||
- 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
|
|
||||||
|
|
||||||
- **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
|
## 📋 Future Roadmap
|
||||||
- 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
|
### ✅ Phase 5: Advanced Features (IN PROGRESS)
|
||||||
- Fixed blank screen issue on Add Dog modal
|
- [x] Reverse pedigree (descendants view)
|
||||||
- Improved parent selection layout
|
- [ ] PDF pedigree generation
|
||||||
- Added comprehensive error handling
|
- [ ] Export to standard formats (CSV, JSON)
|
||||||
- Enhanced visual design with proper spacing
|
- [ ] Print-friendly layouts
|
||||||
|
- [ ] Multi-generation COI analysis
|
||||||
|
|
||||||
- **v0.3.0** - Litter Management & Interactive Pedigree
|
### 📅 Phase 6: Polish & Optimization
|
||||||
- Added litter_id to dogs table
|
- [ ] **User Experience**: Loading states, better error messages, undo functionality
|
||||||
- Implemented LitterForm component
|
- [ ] **Performance**: Image optimization, lazy loading, API caching
|
||||||
- Created PedigreeView with React-D3-Tree
|
- [ ] **Mobile**: Touch-friendly interface, mobile photo capture
|
||||||
- Enhanced DogForm with dual parent selection
|
- [ ] **Documentation**: API technical docs, video tutorials
|
||||||
- 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
|
|
||||||
|
## ✅ 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)
|
const effectiveExternal = isExternal || (dog && dog.is_external)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!effectiveExternal) {
|
|
||||||
fetchDogs()
|
fetchDogs()
|
||||||
|
if (!effectiveExternal) {
|
||||||
fetchLitters()
|
fetchLitters()
|
||||||
}
|
}
|
||||||
if (dog) {
|
if (dog) {
|
||||||
@@ -55,7 +55,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
|
|
||||||
const fetchDogs = async () => {
|
const fetchDogs = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/dogs')
|
const res = await axios.get('/api/dogs/all')
|
||||||
setDogs(res.data || [])
|
setDogs(res.data || [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setDogs([])
|
setDogs([])
|
||||||
@@ -64,8 +64,8 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
|
|
||||||
const fetchLitters = async () => {
|
const fetchLitters = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/litters')
|
const res = await axios.get('/api/litters', { params: { limit: 200 } })
|
||||||
const data = res.data || []
|
const data = res.data.data || []
|
||||||
setLitters(data)
|
setLitters(data)
|
||||||
setLittersAvailable(data.length > 0)
|
setLittersAvailable(data.length > 0)
|
||||||
if (data.length === 0) setUseManualParents(true)
|
if (data.length === 0) setUseManualParents(true)
|
||||||
@@ -112,8 +112,8 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
...formData,
|
...formData,
|
||||||
is_champion: formData.is_champion ? 1 : 0,
|
is_champion: formData.is_champion ? 1 : 0,
|
||||||
is_external: effectiveExternal ? 1 : 0,
|
is_external: effectiveExternal ? 1 : 0,
|
||||||
sire_id: effectiveExternal ? null : (formData.sire_id || null),
|
sire_id: formData.sire_id || null,
|
||||||
dam_id: effectiveExternal ? null : (formData.dam_id || null),
|
dam_id: formData.dam_id || null,
|
||||||
litter_id: (effectiveExternal || useManualParents) ? null : (formData.litter_id || null),
|
litter_id: (effectiveExternal || useManualParents) ? null : (formData.litter_id || null),
|
||||||
registration_number: formData.registration_number || null,
|
registration_number: formData.registration_number || null,
|
||||||
birth_date: formData.birth_date || null,
|
birth_date: formData.birth_date || null,
|
||||||
@@ -162,7 +162,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
}}>
|
}}>
|
||||||
<ExternalLink size={14} />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -250,8 +250,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Parent Section — hidden for external dogs */}
|
{/* Parent Section */}
|
||||||
{!effectiveExternal && (
|
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: '1.5rem', padding: '1rem',
|
marginTop: '1.5rem', padding: '1rem',
|
||||||
background: 'rgba(194, 134, 42, 0.04)',
|
background: 'rgba(194, 134, 42, 0.04)',
|
||||||
@@ -260,7 +259,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
}}>
|
}}>
|
||||||
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
|
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
|
||||||
|
|
||||||
{littersAvailable && (
|
{!effectiveExternal && littersAvailable && (
|
||||||
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
<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' }}>
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
|
||||||
<input type="radio" name="parentMode" checked={!useManualParents}
|
<input type="radio" name="parentMode" checked={!useManualParents}
|
||||||
@@ -275,7 +274,7 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!useManualParents && littersAvailable ? (
|
{!useManualParents && littersAvailable && !effectiveExternal ? (
|
||||||
<div className="form-group" style={{ marginTop: '0.5rem' }}>
|
<div className="form-group" style={{ marginTop: '0.5rem' }}>
|
||||||
<label className="label">Select Litter</label>
|
<label className="label">Select Litter</label>
|
||||||
<select name="litter_id" className="input"
|
<select name="litter_id" className="input"
|
||||||
@@ -314,7 +313,6 @@ function DogForm({ dog, onClose, onSave, isExternal = false }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group" style={{ marginTop: '1rem' }}>
|
<div className="form-group" style={{ marginTop: '1rem' }}>
|
||||||
<label className="label">Notes</label>
|
<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 () => {
|
const fetchDogs = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/dogs')
|
const res = await axios.get('/api/dogs/all')
|
||||||
setDogs(res.data)
|
setDogs(res.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching dogs:', 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)')
|
: (isMale ? 'rgba(59,130,246,0.3)' : 'rgba(236,72,153,0.3)')
|
||||||
const ringColor = isRoot ? rootAccent : nodeColor
|
const ringColor = isRoot ? rootAccent : nodeColor
|
||||||
|
|
||||||
const r = isRoot ? 34 : 28
|
const r = isRoot ? 46 : 38
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
{/* Glow halo */}
|
{/* Glow halo — kept within the circle so it doesn't bleed onto text labels */}
|
||||||
<circle
|
<circle
|
||||||
r={r + 10}
|
r={r - 4}
|
||||||
fill={glowColor}
|
fill={glowColor}
|
||||||
style={{ filter: 'blur(6px)' }}
|
style={{ filter: 'blur(4px)' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Outer ring */}
|
{/* Outer ring */}
|
||||||
@@ -95,25 +95,25 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
|
|||||||
|
|
||||||
{/* Gender / crown icon */}
|
{/* Gender / crown icon */}
|
||||||
<text
|
<text
|
||||||
fill={isRoot ? '#fff' : '#fff'}
|
fontSize={isRoot ? 28 : 24}
|
||||||
fontSize={isRoot ? 22 : 18}
|
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dy="7"
|
dy="8"
|
||||||
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
stroke="none"
|
||||||
|
style={{ fill: '#ffffff', pointerEvents: 'none', userSelect: 'none' }}
|
||||||
>
|
>
|
||||||
{isRoot ? '👑' : (isMale ? '♂' : '♀')}
|
{isRoot ? '👑' : (isMale ? '♂' : '♀')}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
{/* Name label */}
|
{/* Name label */}
|
||||||
<text
|
<text
|
||||||
fill="var(--text-primary, #f5f0e8)"
|
fontSize={isRoot ? 22 : 18}
|
||||||
fontSize={isRoot ? 15 : 13}
|
|
||||||
fontWeight={isRoot ? '700' : '600'}
|
fontWeight={isRoot ? '700' : '600'}
|
||||||
fontFamily="Inter, sans-serif"
|
fontFamily="Inter, sans-serif"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="0"
|
x="0"
|
||||||
y={r + 18}
|
y={r + 32}
|
||||||
style={{ pointerEvents: 'none' }}
|
stroke="none"
|
||||||
|
style={{ fill: isRoot ? '#ffffff' : '#f8fafc', pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
{nodeDatum.name}
|
{nodeDatum.name}
|
||||||
</text>
|
</text>
|
||||||
@@ -121,13 +121,13 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
|
|||||||
{/* Breed label (subtle) */}
|
{/* Breed label (subtle) */}
|
||||||
{breed && (
|
{breed && (
|
||||||
<text
|
<text
|
||||||
fill="var(--text-muted, #8c8472)"
|
fontSize="14"
|
||||||
fontSize="10"
|
|
||||||
fontFamily="Inter, sans-serif"
|
fontFamily="Inter, sans-serif"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="0"
|
x="0"
|
||||||
y={r + 31}
|
y={r + 52}
|
||||||
style={{ pointerEvents: 'none' }}
|
stroke="none"
|
||||||
|
style={{ fill: '#cbd5e1', pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
{breed}
|
{breed}
|
||||||
</text>
|
</text>
|
||||||
@@ -136,13 +136,13 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
|
|||||||
{/* Registration number */}
|
{/* Registration number */}
|
||||||
{nodeDatum.attributes?.registration && (
|
{nodeDatum.attributes?.registration && (
|
||||||
<text
|
<text
|
||||||
fill="var(--text-muted, #8c8472)"
|
fontSize="14"
|
||||||
fontSize="10"
|
|
||||||
fontFamily="Inter, sans-serif"
|
fontFamily="Inter, sans-serif"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="0"
|
x="0"
|
||||||
y={r + (breed ? 44 : 31)}
|
y={r + (breed ? 70 : 52)}
|
||||||
style={{ pointerEvents: 'none' }}
|
stroke="none"
|
||||||
|
style={{ fill: '#94a3b8', pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
{nodeDatum.attributes.registration}
|
{nodeDatum.attributes.registration}
|
||||||
</text>
|
</text>
|
||||||
@@ -151,13 +151,13 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
|
|||||||
{/* Birth year */}
|
{/* Birth year */}
|
||||||
{nodeDatum.attributes?.birth_year && (
|
{nodeDatum.attributes?.birth_year && (
|
||||||
<text
|
<text
|
||||||
fill="var(--text-muted, #8c8472)"
|
fontSize="14"
|
||||||
fontSize="10"
|
|
||||||
fontFamily="Inter, sans-serif"
|
fontFamily="Inter, sans-serif"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
x="0"
|
x="0"
|
||||||
y={r + (breed ? 57 : (nodeDatum.attributes?.registration ? 44 : 31))}
|
y={r + (breed ? 88 : (nodeDatum.attributes?.registration ? 70 : 52))}
|
||||||
style={{ pointerEvents: 'none' }}
|
stroke="none"
|
||||||
|
style={{ fill: '#94a3b8', pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
({nodeDatum.attributes.birth_year})
|
({nodeDatum.attributes.birth_year})
|
||||||
</text>
|
</text>
|
||||||
@@ -232,8 +232,8 @@ const PedigreeTree = ({ dogId, pedigreeData, coi }) => {
|
|||||||
}}
|
}}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
pathFunc="step"
|
pathFunc="step"
|
||||||
separation={{ siblings: 1.6, nonSiblings: 2.2 }}
|
separation={{ siblings: 1.8, nonSiblings: 2.4 }}
|
||||||
nodeSize={{ x: 220, y: 160 }}
|
nodeSize={{ x: 280, y: 200 }}
|
||||||
renderCustomNodeElement={renderCustomNode}
|
renderCustomNodeElement={renderCustomNode}
|
||||||
enableLegacyTransitions
|
enableLegacyTransitions
|
||||||
transitionDuration={300}
|
transitionDuration={300}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
background: white;
|
background: var(--bg-primary, #1e1e24);
|
||||||
|
border: 1px solid var(--border, #333);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
|
|
||||||
.pedigree-container {
|
.pedigree-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: linear-gradient(to bottom, #f8fafc 0%, #e2e8f0 100%);
|
background: var(--bg-primary, #1e1e24);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -26,8 +27,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
background: #f1f5f9;
|
background: var(--bg-elevated, #2a2a35);
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid var(--border, #333);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +36,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 1rem;
|
||||||
color: #475569;
|
color: var(--text-secondary, #a1a1aa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-color {
|
.legend-color {
|
||||||
@@ -57,10 +58,10 @@
|
|||||||
|
|
||||||
.pedigree-info {
|
.pedigree-info {
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
background: #f8fafc;
|
background: var(--bg-elevated, #2a2a35);
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid var(--border, #333);
|
||||||
font-size: 0.875rem;
|
font-size: 1rem;
|
||||||
color: #64748b;
|
color: var(--text-muted, #a1a1aa);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pedigree-info strong {
|
.pedigree-info strong {
|
||||||
color: #334155;
|
color: var(--text-primary, #ffffff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override react-d3-tree styles */
|
/* Override react-d3-tree styles */
|
||||||
@@ -94,12 +95,12 @@
|
|||||||
|
|
||||||
.rd3t-label__title {
|
.rd3t-label__title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
fill: #1e293b;
|
fill: var(--text-primary, #ffffff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rd3t-label__attributes {
|
.rd3t-label__attributes {
|
||||||
font-size: 0.875rem;
|
font-size: 1rem;
|
||||||
fill: #64748b;
|
fill: var(--text-muted, #a1a1aa);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading state */
|
/* Loading state */
|
||||||
|
|||||||
@@ -17,19 +17,24 @@ function PedigreeView({ dogId, onClose }) {
|
|||||||
}, [dogId])
|
}, [dogId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDimensions = () => {
|
|
||||||
const container = document.querySelector('.pedigree-container')
|
const container = document.querySelector('.pedigree-container')
|
||||||
if (container) {
|
if (!container) return
|
||||||
|
|
||||||
|
const updateDimensions = () => {
|
||||||
const width = container.offsetWidth
|
const width = container.offsetWidth
|
||||||
const height = container.offsetHeight
|
const height = container.offsetHeight
|
||||||
setDimensions({ width, height })
|
setDimensions({ width, height })
|
||||||
setTranslate({ x: width / 4, y: height / 2 })
|
setTranslate({ x: width / 4, y: height / 2 })
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
updateDimensions()
|
updateDimensions()
|
||||||
window.addEventListener('resize', updateDimensions)
|
|
||||||
return () => window.removeEventListener('resize', updateDimensions)
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateDimensions()
|
||||||
|
})
|
||||||
|
resizeObserver.observe(container)
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchPedigree = async () => {
|
const fetchPedigree = async () => {
|
||||||
|
|||||||
@@ -21,21 +21,21 @@ function Dashboard() {
|
|||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
try {
|
try {
|
||||||
const [dogsRes, littersRes, heatCyclesRes] = await Promise.all([
|
const [dogsRes, littersRes, heatCyclesRes] = await Promise.all([
|
||||||
axios.get('/api/dogs'),
|
axios.get('/api/dogs', { params: { page: 1, limit: 8 } }),
|
||||||
axios.get('/api/litters'),
|
axios.get('/api/litters', { params: { page: 1, limit: 1 } }),
|
||||||
axios.get('/api/breeding/heat-cycles/active')
|
axios.get('/api/breeding/heat-cycles/active')
|
||||||
])
|
])
|
||||||
|
|
||||||
const dogs = dogsRes.data
|
const { data: recentDogsList, stats: dogStats } = dogsRes.data
|
||||||
setStats({
|
setStats({
|
||||||
totalDogs: dogs.length,
|
totalDogs: dogStats?.total ?? 0,
|
||||||
males: dogs.filter(d => d.sex === 'male').length,
|
males: dogStats?.males ?? 0,
|
||||||
females: dogs.filter(d => d.sex === 'female').length,
|
females: dogStats?.females ?? 0,
|
||||||
totalLitters: littersRes.data.length,
|
totalLitters: littersRes.data.total,
|
||||||
activeHeatCycles: heatCyclesRes.data.length
|
activeHeatCycles: heatCyclesRes.data.length
|
||||||
})
|
})
|
||||||
|
|
||||||
setRecentDogs(dogs.slice(0, 8))
|
setRecentDogs(recentDogsList)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching dashboard data:', error)
|
console.error('Error fetching dashboard data:', error)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import DogForm from '../components/DogForm'
|
|||||||
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
||||||
import ClearanceSummaryCard from '../components/ClearanceSummaryCard'
|
import ClearanceSummaryCard from '../components/ClearanceSummaryCard'
|
||||||
import HealthRecordForm from '../components/HealthRecordForm'
|
import HealthRecordForm from '../components/HealthRecordForm'
|
||||||
|
import GeneticPanelCard from '../components/GeneticPanelCard'
|
||||||
|
import { ShieldCheck } from 'lucide-react'
|
||||||
|
|
||||||
function DogDetail() {
|
function DogDetail() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -262,6 +264,18 @@ function DogDetail() {
|
|||||||
<span className="info-value" style={{ fontFamily: 'monospace' }}>{dog.registration_number}</span>
|
<span className="info-value" style={{ fontFamily: 'monospace' }}>{dog.registration_number}</span>
|
||||||
</div>
|
</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 && (
|
{dog.microchip && (
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span className="info-label"><Hash size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />Microchip</span>
|
<span className="info-label"><Hash size={14} style={{ display: 'inline', marginRight: '0.25rem' }} />Microchip</span>
|
||||||
@@ -317,6 +331,9 @@ function DogDetail() {
|
|||||||
{/* OFA Clearance Summary */}
|
{/* OFA Clearance Summary */}
|
||||||
<ClearanceSummaryCard dogId={id} onAddRecord={openAddHealth} />
|
<ClearanceSummaryCard dogId={id} onAddRecord={openAddHealth} />
|
||||||
|
|
||||||
|
{/* DNA Genetics Panel */}
|
||||||
|
<GeneticPanelCard dogId={id} />
|
||||||
|
|
||||||
{/* Health Records List */}
|
{/* Health Records List */}
|
||||||
{healthRecords.length > 0 && (
|
{healthRecords.length > 0 && (
|
||||||
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
<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 { Link, useNavigate } from 'react-router-dom'
|
||||||
import { Dog, Plus, Search, Calendar, Hash, ArrowRight, Trash2 } from 'lucide-react'
|
import { Dog, Plus, Search, Calendar, Hash, ArrowRight, Trash2 } from 'lucide-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import DogForm from '../components/DogForm'
|
import DogForm from '../components/DogForm'
|
||||||
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
||||||
|
|
||||||
|
const LIMIT = 50
|
||||||
|
|
||||||
function DogList() {
|
function DogList() {
|
||||||
const [dogs, setDogs] = useState([])
|
const [dogs, setDogs] = useState([])
|
||||||
const [filteredDogs, setFilteredDogs] = useState([])
|
const [total, setTotal] = useState(0)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [sexFilter, setSexFilter] = useState('all')
|
const [sexFilter, setSexFilter] = useState('all')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
const [deleteTarget, setDeleteTarget] = useState(null) // { id, name }
|
const [deleteTarget, setDeleteTarget] = useState(null) // { id, name }
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
|
const searchTimerRef = useRef(null)
|
||||||
|
|
||||||
useEffect(() => { fetchDogs() }, [])
|
useEffect(() => { fetchDogs(1, '', 'all') }, []) // eslint-disable-line
|
||||||
useEffect(() => { filterDogs() }, [dogs, search, sexFilter])
|
|
||||||
|
|
||||||
const fetchDogs = async () => {
|
const fetchDogs = async (p, q, s) => {
|
||||||
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/dogs')
|
const params = { page: p, limit: LIMIT }
|
||||||
setDogs(res.data)
|
if (q) params.search = q
|
||||||
setLoading(false)
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching dogs:', error)
|
console.error('Error fetching dogs:', error)
|
||||||
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterDogs = () => {
|
const handleSearchChange = (value) => {
|
||||||
let filtered = dogs
|
setSearch(value)
|
||||||
if (search) {
|
if (searchTimerRef.current) clearTimeout(searchTimerRef.current)
|
||||||
filtered = filtered.filter(dog =>
|
searchTimerRef.current = setTimeout(() => fetchDogs(1, value, sexFilter), 300)
|
||||||
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 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 () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteTarget) return
|
if (!deleteTarget) return
|
||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/dogs/${deleteTarget.id}`)
|
await axios.delete(`/api/dogs/${deleteTarget.id}`)
|
||||||
setDogs(prev => prev.filter(d => d.id !== deleteTarget.id))
|
|
||||||
setDeleteTarget(null)
|
setDeleteTarget(null)
|
||||||
|
fetchDogs(page, search, sexFilter)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Delete failed:', err)
|
console.error('Delete failed:', err)
|
||||||
alert('Failed to delete dog. Please try again.')
|
alert('Failed to delete dog. Please try again.')
|
||||||
@@ -60,6 +72,8 @@ function DogList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / LIMIT)
|
||||||
|
|
||||||
const calculateAge = (birthDate) => {
|
const calculateAge = (birthDate) => {
|
||||||
if (!birthDate) return null
|
if (!birthDate) return null
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@@ -85,7 +99,7 @@ function DogList() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 style={{ marginBottom: '0.25rem' }}>Dogs</h1>
|
<h1 style={{ marginBottom: '0.25rem' }}>Dogs</h1>
|
||||||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>
|
<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'}
|
{search || sexFilter !== 'all' ? ' matching filters' : ' total'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,11 +119,11 @@ function DogList() {
|
|||||||
className="input"
|
className="input"
|
||||||
placeholder="Search by name or registration..."
|
placeholder="Search by name or registration..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
style={{ paddingLeft: '2.75rem' }}
|
style={{ paddingLeft: '2.75rem' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="all">All Dogs</option>
|
||||||
<option value="male">Males ♂</option>
|
<option value="male">Males ♂</option>
|
||||||
<option value="female">Females ♀</option>
|
<option value="female">Females ♀</option>
|
||||||
@@ -117,7 +131,7 @@ function DogList() {
|
|||||||
{(search || sexFilter !== 'all') && (
|
{(search || sexFilter !== 'all') && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost"
|
className="btn btn-ghost"
|
||||||
onClick={() => { setSearch(''); setSexFilter('all') }}
|
onClick={handleClearFilters}
|
||||||
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
|
style={{ padding: '0.625rem 1rem', fontSize: '0.875rem' }}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
@@ -127,7 +141,7 @@ function DogList() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dogs List */}
|
{/* Dogs List */}
|
||||||
{filteredDogs.length === 0 ? (
|
{dogs.length === 0 ? (
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
<div className="card" style={{ textAlign: 'center', padding: '4rem 2rem' }}>
|
||||||
<Dog size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
|
<Dog size={64} style={{ color: 'var(--text-muted)', margin: '0 auto 1rem', opacity: 0.5 }} />
|
||||||
<h3 style={{ marginBottom: '0.5rem' }}>
|
<h3 style={{ marginBottom: '0.5rem' }}>
|
||||||
@@ -147,7 +161,7 @@ function DogList() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
{filteredDogs.map(dog => (
|
{dogs.map(dog => (
|
||||||
<div
|
<div
|
||||||
key={dog.id}
|
key={dog.id}
|
||||||
className="card"
|
className="card"
|
||||||
@@ -259,6 +273,19 @@ function DogList() {
|
|||||||
{dog.registration_number}
|
{dog.registration_number}
|
||||||
</div>
|
</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>
|
</Link>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
@@ -300,6 +327,31 @@ function DogList() {
|
|||||||
</div>
|
</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 */}
|
{/* Add Dog Modal */}
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
<DogForm
|
<DogForm
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ function LitterDetail() {
|
|||||||
|
|
||||||
const fetchAllDogs = async () => {
|
const fetchAllDogs = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/dogs')
|
const res = await axios.get('/api/dogs/all')
|
||||||
setAllDogs(res.data)
|
setAllDogs(res.data)
|
||||||
} catch (err) { console.error('Error fetching dogs:', err) }
|
} catch (err) { console.error('Error fetching dogs:', err) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import LitterForm from '../components/LitterForm'
|
import LitterForm from '../components/LitterForm'
|
||||||
|
|
||||||
|
const LIMIT = 50
|
||||||
|
|
||||||
function LitterList() {
|
function LitterList() {
|
||||||
const [litters, setLitters] = useState([])
|
const [litters, setLitters] = useState([])
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showForm, setShowForm] = useState(false)
|
const [showForm, setShowForm] = useState(false)
|
||||||
const [editingLitter, setEditingLitter] = useState(null)
|
const [editingLitter, setEditingLitter] = useState(null)
|
||||||
@@ -13,7 +17,7 @@ function LitterList() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLitters()
|
fetchLitters(1)
|
||||||
// Auto-open form with prefill from BreedingCalendar "Record Litter" CTA
|
// Auto-open form with prefill from BreedingCalendar "Record Litter" CTA
|
||||||
const stored = sessionStorage.getItem('prefillLitter')
|
const stored = sessionStorage.getItem('prefillLitter')
|
||||||
if (stored) {
|
if (stored) {
|
||||||
@@ -27,10 +31,12 @@ function LitterList() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const fetchLitters = async () => {
|
const fetchLitters = async (p = page) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/litters')
|
const res = await axios.get('/api/litters', { params: { page: p, limit: LIMIT } })
|
||||||
setLitters(res.data)
|
setLitters(res.data.data)
|
||||||
|
setTotal(res.data.total)
|
||||||
|
setPage(p)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching litters:', error)
|
console.error('Error fetching litters:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -38,6 +44,8 @@ function LitterList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(total / LIMIT)
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
setEditingLitter(null)
|
setEditingLitter(null)
|
||||||
setPrefill(null)
|
setPrefill(null)
|
||||||
@@ -56,14 +64,14 @@ function LitterList() {
|
|||||||
if (!window.confirm('Delete this litter record? Puppies will be unlinked but not deleted.')) return
|
if (!window.confirm('Delete this litter record? Puppies will be unlinked but not deleted.')) return
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/api/litters/${id}`)
|
await axios.delete(`/api/litters/${id}`)
|
||||||
fetchLitters()
|
fetchLitters(page)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting litter:', error)
|
console.error('Error deleting litter:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
fetchLitters()
|
fetchLitters(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -80,7 +88,7 @@ function LitterList() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{litters.length === 0 ? (
|
{total === 0 ? (
|
||||||
<div className="card" style={{ textAlign: 'center', padding: '4rem' }}>
|
<div className="card" style={{ textAlign: 'center', padding: '4rem' }}>
|
||||||
<Activity size={64} style={{ color: 'var(--text-secondary)', margin: '0 auto 1rem' }} />
|
<Activity size={64} style={{ color: 'var(--text-secondary)', margin: '0 auto 1rem' }} />
|
||||||
<h2>No litters recorded yet</h2>
|
<h2>No litters recorded yet</h2>
|
||||||
@@ -143,6 +151,31 @@ function LitterList() {
|
|||||||
</div>
|
</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 && (
|
{showForm && (
|
||||||
<LitterForm
|
<LitterForm
|
||||||
litter={editingLitter}
|
litter={editingLitter}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default function PairingSimulator() {
|
|||||||
const [dogsLoading, setDogsLoading] = useState(true)
|
const [dogsLoading, setDogsLoading] = useState(true)
|
||||||
const [relationWarning, setRelationWarning] = useState(null)
|
const [relationWarning, setRelationWarning] = useState(null)
|
||||||
const [relationChecking, setRelationChecking] = useState(false)
|
const [relationChecking, setRelationChecking] = useState(false)
|
||||||
|
const [geneticRisk, setGeneticRisk] = useState(null)
|
||||||
|
const [geneticChecking, setGeneticChecking] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// include_external=1 ensures external sires/dams appear for pairing
|
// include_external=1 ensures external sires/dams appear for pairing
|
||||||
@@ -27,17 +29,28 @@ export default function PairingSimulator() {
|
|||||||
const checkRelation = useCallback(async (sid, did) => {
|
const checkRelation = useCallback(async (sid, did) => {
|
||||||
if (!sid || !did) {
|
if (!sid || !did) {
|
||||||
setRelationWarning(null)
|
setRelationWarning(null)
|
||||||
|
setGeneticRisk(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setRelationChecking(true)
|
setRelationChecking(true)
|
||||||
|
setGeneticChecking(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/pedigree/relations/${sid}/${did}`)
|
const [relRes, genRes] = await Promise.all([
|
||||||
const data = await res.json()
|
fetch(`/api/pedigree/relations/${sid}/${did}`),
|
||||||
setRelationWarning(data.related ? data.relationship : null)
|
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 {
|
} catch {
|
||||||
setRelationWarning(null)
|
setRelationWarning(null)
|
||||||
|
setGeneticRisk(null)
|
||||||
} finally {
|
} finally {
|
||||||
setRelationChecking(false)
|
setRelationChecking(false)
|
||||||
|
setGeneticChecking(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -142,7 +155,7 @@ export default function PairingSimulator() {
|
|||||||
|
|
||||||
{relationChecking && (
|
{relationChecking && (
|
||||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.75rem' }}>
|
||||||
Checking relationship...
|
Checking relationship and genetics...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -158,6 +171,31 @@ export default function PairingSimulator() {
|
|||||||
</div>
|
</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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
|||||||
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
|
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PedigreeTree from '../components/PedigreeTree'
|
import PedigreeTree from '../components/PedigreeTree'
|
||||||
import { transformPedigreeData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
|
import { transformPedigreeData, transformDescendantData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
|
||||||
|
|
||||||
function PedigreeView() {
|
function PedigreeView() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -14,16 +14,18 @@ function PedigreeView() {
|
|||||||
const [pedigreeData, setPedigreeData] = useState(null)
|
const [pedigreeData, setPedigreeData] = useState(null)
|
||||||
const [coiData, setCoiData] = useState(null)
|
const [coiData, setCoiData] = useState(null)
|
||||||
const [generations, setGenerations] = useState(5)
|
const [generations, setGenerations] = useState(5)
|
||||||
|
const [viewMode, setViewMode] = useState('ancestors')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPedigreeData()
|
fetchPedigreeData()
|
||||||
}, [id, generations])
|
}, [id, generations, viewMode])
|
||||||
|
|
||||||
const fetchPedigreeData = async () => {
|
const fetchPedigreeData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (viewMode === 'ancestors') {
|
||||||
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
|
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
|
||||||
const dogData = pedigreeRes.data
|
const dogData = pedigreeRes.data
|
||||||
setDog(dogData)
|
setDog(dogData)
|
||||||
@@ -38,6 +40,15 @@ function PedigreeView() {
|
|||||||
console.warn('COI calculation unavailable:', coiError)
|
console.warn('COI calculation unavailable:', coiError)
|
||||||
setCoiData(null)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -86,7 +97,7 @@ function PedigreeView() {
|
|||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{/* Header */}
|
{/* 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
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => navigate(`/dogs/${id}`)}
|
onClick={() => navigate(`/dogs/${id}`)}
|
||||||
@@ -96,10 +107,10 @@ function PedigreeView() {
|
|||||||
Back to Profile
|
Back to Profile
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||||
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
|
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
|
||||||
{dog?.name}'s Pedigree
|
{dog?.name}'s {viewMode === 'ancestors' ? 'Pedigree' : 'Descendants'}
|
||||||
</h1>
|
</h1>
|
||||||
{dog?.registration_number && (
|
{dog?.registration_number && (
|
||||||
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
|
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
|
||||||
@@ -107,12 +118,31 @@ function PedigreeView() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Stats Bar */}
|
{/* Stats Bar */}
|
||||||
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
|
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
|
||||||
|
{viewMode === 'ancestors' && (
|
||||||
|
<>
|
||||||
{/* COI */}
|
{/* COI */}
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
||||||
@@ -166,6 +196,8 @@ function PedigreeView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Generations */}
|
{/* Generations */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -181,3 +181,47 @@ export const getPedigreeCompleteness = (treeData, targetGenerations = 5) => {
|
|||||||
const actualCount = countAncestors(treeData)
|
const actualCount = countAncestors(treeData)
|
||||||
return Math.min(100, Math.round((actualCount / expectedTotal) * 100))
|
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 ────────────────────────────────────────────────────────
|
// ── Cancer History ────────────────────────────────────────────────────────
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS cancer_history (
|
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 ──────────────────────────────────────────────────────────────
|
// ── Settings ──────────────────────────────────────────────────────────────
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
|||||||
@@ -55,34 +55,72 @@ function attachParents(db, dogs) {
|
|||||||
return dogs;
|
return dogs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GET dogs
|
// ── GET dogs (paginated)
|
||||||
// Default: kennel dogs only (is_external = 0)
|
// Default: kennel dogs only (is_external = 0)
|
||||||
// ?include_external=1 : all active dogs (kennel + external)
|
// ?include_external=1 : all active dogs (kennel + external)
|
||||||
// ?external_only=1 : external dogs only
|
// ?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) => {
|
router.get('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const includeExternal = req.query.include_external === '1' || req.query.include_external === 'true';
|
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 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) {
|
if (externalOnly) {
|
||||||
whereClause = 'WHERE is_active = 1 AND is_external = 1';
|
baseWhere = 'is_active = 1 AND is_external = 1';
|
||||||
} else if (includeExternal) {
|
} else if (includeExternal) {
|
||||||
whereClause = 'WHERE is_active = 1';
|
baseWhere = 'is_active = 1';
|
||||||
} else {
|
} 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(`
|
const dogs = db.prepare(`
|
||||||
SELECT ${DOG_COLS}
|
SELECT ${DOG_COLS}
|
||||||
FROM dogs
|
FROM dogs
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY name
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching dogs:', error);
|
console.error('Error fetching dogs:', error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const GRCA_CORE = {
|
|||||||
heart: ['heart_ofa', 'heart_echo'],
|
heart: ['heart_ofa', 'heart_echo'],
|
||||||
eye: ['eye_caer'],
|
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
|
// Helper: compute clearance summary for a dog
|
||||||
function getClearanceSummary(db, dogId) {
|
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
|
// POST create health record
|
||||||
router.post('/', (req, res) => {
|
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' });
|
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 db = getDatabase();
|
||||||
const dbResult = db.prepare(`
|
const dbResult = db.prepare(`
|
||||||
INSERT INTO health_records
|
INSERT INTO health_records
|
||||||
@@ -157,6 +151,10 @@ router.put('/:id', (req, res) => {
|
|||||||
document_url, result, vet_name, next_due, notes
|
document_url, result, vet_name, next_due, notes
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
if (test_type && !VALID_TEST_TYPES.includes(test_type)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid test_type' });
|
||||||
|
}
|
||||||
|
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE health_records
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getDatabase } = require('../db/init');
|
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) => {
|
router.get('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
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(`
|
const litters = db.prepare(`
|
||||||
SELECT l.*,
|
SELECT l.*,
|
||||||
s.name as sire_name, s.registration_number as sire_reg,
|
s.name as sire_name, s.registration_number as sire_reg,
|
||||||
@@ -14,19 +22,30 @@ router.get('/', (req, res) => {
|
|||||||
JOIN dogs s ON l.sire_id = s.id
|
JOIN dogs s ON l.sire_id = s.id
|
||||||
JOIN dogs d ON l.dam_id = d.id
|
JOIN dogs d ON l.dam_id = d.id
|
||||||
ORDER BY l.breeding_date DESC
|
ORDER BY l.breeding_date DESC
|
||||||
`).all();
|
LIMIT ? OFFSET ?
|
||||||
|
`).all(limit, offset);
|
||||||
|
|
||||||
litters.forEach(litter => {
|
if (litters.length > 0) {
|
||||||
litter.puppies = db.prepare(`
|
const litterIds = litters.map(l => l.id);
|
||||||
SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
|
const placeholders = litterIds.map(() => '?').join(',');
|
||||||
`).all(litter.id);
|
const allPuppies = db.prepare(`
|
||||||
litter.puppies.forEach(puppy => {
|
SELECT * FROM dogs WHERE litter_id IN (${placeholders}) AND is_active = 1
|
||||||
puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : [];
|
`).all(...litterIds);
|
||||||
});
|
|
||||||
litter.actual_puppy_count = litter.puppies.length;
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,25 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getDatabase } = require('../db/init');
|
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)
|
* getAncestorMap(db, dogId, maxGen)
|
||||||
* Returns Map<id, [{ id, name, generation }, ...]>
|
* Returns Map<id, [{ id, name, generation }, ...]>
|
||||||
@@ -9,6 +28,8 @@ const { getDatabase } = require('../db/init');
|
|||||||
* pairings are correctly detected by calculateCOI.
|
* pairings are correctly detected by calculateCOI.
|
||||||
*/
|
*/
|
||||||
function getAncestorMap(db, dogId, maxGen = 6) {
|
function getAncestorMap(db, dogId, maxGen = 6) {
|
||||||
|
const cacheKey = `${dogId}-${maxGen}`;
|
||||||
|
return getFromCache(ancestorCache, cacheKey, () => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
|
|
||||||
function recurse(id, gen) {
|
function recurse(id, gen) {
|
||||||
@@ -27,6 +48,7 @@ function getAncestorMap(db, dogId, maxGen = 6) {
|
|||||||
|
|
||||||
recurse(parseInt(dogId), 0);
|
recurse(parseInt(dogId), 0);
|
||||||
return map;
|
return map;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +90,8 @@ function isDirectRelation(db, sireId, damId) {
|
|||||||
* self-loops.
|
* self-loops.
|
||||||
*/
|
*/
|
||||||
function calculateCOI(db, sireId, damId) {
|
function calculateCOI(db, sireId, damId) {
|
||||||
|
const cacheKey = `${sireId}-${damId}`;
|
||||||
|
return getFromCache(coiCache, cacheKey, () => {
|
||||||
const sid = parseInt(sireId);
|
const sid = parseInt(sireId);
|
||||||
const did = parseInt(damId);
|
const did = parseInt(damId);
|
||||||
const sireMap = getAncestorMap(db, sid);
|
const sireMap = getAncestorMap(db, sid);
|
||||||
@@ -116,6 +140,7 @@ function calculateCOI(db, sireId, damId) {
|
|||||||
coefficient: coi,
|
coefficient: coi,
|
||||||
commonAncestors: commonAncestorList
|
commonAncestors: commonAncestorList
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -124,8 +149,7 @@ function calculateCOI(db, sireId, damId) {
|
|||||||
// 'trial-pairing' as dog IDs and return 404/wrong data.
|
// 'trial-pairing' as dog IDs and return 404/wrong data.
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
// POST /api/pedigree/trial-pairing (alias for /coi)
|
const handleTrialPairing = (req, res) => {
|
||||||
router.post(['/trial-pairing', '/coi'], (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { sire_id, dam_id } = req.body;
|
const { sire_id, dam_id } = req.body;
|
||||||
if (!sire_id || !dam_id) {
|
if (!sire_id || !dam_id) {
|
||||||
@@ -156,7 +180,13 @@ router.post(['/trial-pairing', '/coi'], (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
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
|
// GET /api/pedigree/:id/coi
|
||||||
router.get('/:id/coi', (req, res) => {
|
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
|
// 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