A web-based application for tracking analog film rolls through their lifecycle: from purchase → loading → shooting → developing → scanning.
Backend:
- Python 3.x + FastAPI
- SQLAlchemy 2.0 + SQLite
- Uvicorn ASGI server
Frontend:
- React 19 + Vite 7
- React Router DOM 7
- dnd-kit (drag & drop)
- Tailwind CSS 3
- Framer Motion 11
- axios 1.7
- Make scripts executable:
chmod +x scripts/*.sh- Set up Python virtual environment (backend):
cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cd ..- Install frontend dependencies:
cd frontend
npm install
cd ..Use this for active development with hot-reload:
Terminal 1 - Backend:
cd backend
source venv/bin/activate
uvicorn app.main:app --reload --host 0.0.0.0 --port 8200Terminal 2 - Frontend:
cd frontend
npm run devAccess at: http://localhost:5173 (Vite proxies API requests to backend on port 8200)
Use this for local network access from mobile devices:
# Build frontend (first time or after changes)
./scripts/build-production.sh
# Start server
./scripts/start-production.shAccess at:
- Local: http://localhost:8200
- Network: http://YOUR_IP:8200 (shown in terminal when server starts)
The production server serves both frontend and backend from port 8200
- Kanban-style board with drag-and-drop status transitions
- Track rolls from NEW → LOADED → EXPOSED → DEVELOPED → SCANNED
- Film stock metadata (name, format, exposures, order ID)
- Push/pull processing tracking
- Cost calculations (film cost, dev cost, per-shot cost)
- 5-star rating system
- Notes and custom fields
- "Not mine" flag for friend's rolls
- Duplicate functionality for quick entry
- Track chemistry batches with mix and retirement dates
- Automatic roll counter and cost-per-roll calculation
- C41 development time calculator (3:30 base + 2% per roll)
- Manual offset adjustment for chemistry usage
- Active/retired batch organization with pagination
- Link to view all rolls using each batch
- Duplicate and retire batch actions
- Touch-friendly mobile-responsive design
- Drag-and-drop with visual feedback
- Modal dialogs for date/chemistry/rating entry
- Autocomplete for film stock names and order IDs
- Loading states with skeleton screens
- Toast notifications for user actions
- Error handling with retry options
- Pagination for large datasets
Backend (Phases 1-4):
- SQLAlchemy models with computed properties (status, costs, C41 dev time)
- CRUD endpoints for film rolls and chemistry batches
- PATCH endpoints for status transitions (load, unload, assign chemistry, rate)
- Automatic chemistry roll count updates
- Comprehensive validation and error handling
- All endpoints tested and working
Frontend (Phases 5-11):
- React + Vite with Tailwind CSS styling
- Drag-and-drop Kanban board with dnd-kit
- Film roll and chemistry CRUD operations
- Status transition modals (date picker, chemistry picker, rating)
- Autocomplete for film stocks and order IDs
- Duplicate functionality for quick data entry
- Chemistry page with active/retired sections
- Pagination for NEW and SCANNED rolls, active/retired batches
- Data migration scripts from spreadsheet
- Loading states with skeleton screens
- Comprehensive error handling and validation
- 12.1 Loading states and skeleton screens
- 12.2 Error handling and validation
- 12.3 Keyboard shortcuts (optional)
- 12.4 Basic user documentation
- 12.5 Auto-start on macOS (optional)
- 12.6 Database backup script
- 12.7 🎬 Start using the app!
GET /api/rolls- List rolls (filter:?status=NEW&order_id=42)POST /api/rolls- Create rollGET /api/rolls/{id}- Get rollPUT /api/rolls/{id}- Update rollDELETE /api/rolls/{id}- Delete rollPATCH /api/rolls/{id}/load- Set date_loadedPATCH /api/rolls/{id}/unload- Set date_unloadedPATCH /api/rolls/{id}/chemistry- Assign chemistryPATCH /api/rolls/{id}/rating- Set stars (1-5)
GET /api/chemistry- List batches (filter:?active_only=true&chemistry_type=C41)POST /api/chemistry- Create batchGET /api/chemistry/{id}- Get batchPUT /api/chemistry/{id}- Update batchDELETE /api/chemistry/{id}- Delete batch
Location: backend/data/emulsion.db (SQLite)
Tables:
film_rolls- Film roll tracking with computed statuschemistry_batches- Chemistry batch tracking with C41 dev time calculation
Changing Database Location:
Edit backend/app/core/config.py:
database_url: str = "sqlite:///path/to/your/emulsion.db"Import existing data from CSV files (e.g., from Numbers spreadsheet):
cd migration/scripts
# Import chemistry batches first (required before rolls)
python import_chemistry.py --db-path ../../backend/data/emulsion.db
# Import film rolls
python import_rolls.py --db-path ../../backend/data/emulsion.db
# Validate imported data
python validate.py --db-path ../../backend/data/emulsion.dbSee migration/README.md for CSV format requirements.
Development Mode:
Browser → Port 5173 (Vite) → Proxy → Port 8200 (FastAPI)
- Frontend dev server with hot-reload
- API requests automatically proxied to backend
- Best for active development
Production Mode:
Browser → Port 8200 (FastAPI)
├── /api/* → Backend API
└── /* → Frontend (React SPA)
- Single server serves both frontend and backend
- FastAPI serves built frontend static files
- SPA routing support with fallback to index.html
- Best for local network access (mobile devices)
Status is computed, not stored. The FilmRoll.status property derives status from field presence:
- NEW: No dates, no chemistry, no stars
- LOADED: Has
date_loaded - EXPOSED: Has
date_unloaded - DEVELOPED: Has
chemistry_id - SCANNED: Has
stars > 0
This flexible approach avoids rigid state machines and allows data corrections.
Both models use @property decorators for on-the-fly calculations (never stored):
- FilmRoll:
status,dev_cost,total_cost,cost_per_shot,duration_days - ChemistryBatch:
batch_cost,rolls_developed,cost_per_roll,development_time_formatted - C41 development time: Base 3:30 + 2% per roll used
emulsion/
├── backend/
│ ├── app/
│ │ ├── api/ # API endpoints
│ │ ├── core/ # Config & database
│ │ ├── models/ # SQLAlchemy models
│ │ └── main.py # FastAPI app
│ ├── data/ # SQLite database
│ ├── requirements.txt
│ └── venv/
├── frontend/
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── pages/ # RollsPage, ChemistryPage
│ │ ├── services/ # API clients
│ │ ├── utils/ # Helpers
│ │ └── App.jsx
│ ├── package.json
│ └── vite.config.js
├── migration/ # Data migration scripts
├── plan.md # Full architecture plan
└── README.md
Frontend:
FilmRollCard- Drag-and-drop film roll displayStatusColumn- Kanban column with drop zonesAddRollForm/EditRollForm- Roll managementAddChemistryForm/EditChemistryForm- Chemistry managementSkeletonCard- Loading state placeholderErrorMessage- Consistent error display
Backend:
FilmRollmodel - Status, cost, duration calculated propertiesChemistryBatchmodel - Roll count, C41 dev time calculation- CRUD + PATCH endpoints for all operations
- Open
http://YOUR_IP:8200in Safari - Tap the Share button (square with arrow)
- Tap "Add to Home Screen"
- Name it "Emulsion" and tap "Add"
- Open
http://YOUR_IP:8200in Chrome - Tap the three-dot menu
- Tap "Add to Home screen"
- Name it "Emulsion" and tap "Add"
./scripts/backup-database.shBackups are stored in: backend/data/backups/
Set up a cron job to run daily at 2 AM:
crontab -eAdd this line (replace YOUR_PATH with actual path):
0 2 * * * /YOUR_PATH/Emulsion/scripts/backup-database.sh# Find what's using port 8200
lsof -ti:8200
# Kill the process
kill $(lsof -ti:8200)- Check firewall settings (allow incoming connections for Python/uvicorn)
- Verify both devices are on same WiFi network
- Use IP address (not localhost) when accessing from phone
- Try:
http://YOUR_IP:8200(find IP withipconfig getifaddr en0on macOS)
# Rebuild frontend
cd frontend
npm run build
# Restart server
cd ..
./scripts/start-production.shBackend is configured to allow all local network origins. If you see CORS errors:
- Check
backend/app/core/config.pyforcors_originssettings - Verify API requests use
/apiprefix
- Adding authentication
- Using HTTPS
- Implementing rate limiting
- Adding security headers
- plan.md - Complete architecture and development plan
- Backend API: http://localhost:8200/docs (Swagger UI when running)