Fetched async via /api/todos/count so it doesn't block page render.
Badge hidden when count is zero.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _load_geometry(): compute extents/volume from individual Scene meshes
without concatenating — avoids the RAM spike that crashed the container
- analyse_in_background(): spawns a child process (spawn context, not fork)
with a 120s timeout; OOM/crash in child no longer takes down the app
- _failed set: tracks files that timed out or crashed; spinner stops and
a warning with inline override form is shown instead of looping forever
- analysis_failed state passed to template; override form shown immediately
when analysis failed (no expand needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- analyse_in_background() runs in FastAPI BackgroundTasks with its own
DB session; _in_progress set prevents duplicate concurrent analyses
- Job detail shows a spinner + auto-reloads every 3s while computing
- 'Compute now' button also triggers background analysis
- StlCache.manually_set flag (migration 0011): entries marked manual are
never overwritten by auto-analysis
- 'Override values' collapsible form on job detail lets user set
bounding box and volume directly from slicer data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
trimesh loads 3MF files as a Scene; force="mesh" fails on those.
Detect Scene and concatenate all geometries before computing extents/volume.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- If file exists but has no cache entry, run analyse() on page load
- If file doesn't exist on disk, show a red alert in the File & Geometry card
- Hide Download/Compute buttons when file is missing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Variable was only assigned inside a nested if-block; initialise to False
at the top of the filament-logging section so all code paths define it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- On create_task(), look up the 'POPS' label (create it if missing) and
attach it to every task. Label ID is cached for the process lifetime.
- Corrected VIKUNJA_URL typo (tasks → task)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New app/vikunja.py: get_tasks, create_task, update_task, delete_task
- /todos page now reads from and writes to Vikunja (create, mark done, delete)
- Auto-todos (clean build plate, filament low) post to Vikunja after DB commit
- /api/todos router proxies to Vikunja
- .env.install: VIKUNJA_URL, VIKUNJA_API_TOKEN, VIKUNJA_PROJECT_ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 'Clean build plate on {printer}' after every 3rd completed print per printer
- 'Check filament status' when a roll drops below 100 g after starting a job
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
queued: black + yellow outline + yellow text
printing: yellow + black text
done: green (unchanged)
cancelled: grey
failed: red (unchanged)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds depreciation/electricity as a separate line item in the production
cost breakdown. Included in the markup base so it contributes to margin.
Configurable via PRINTER_COST_PER_HOUR env var (default 0.35).
Bracket gross total updates automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sums gross_price estimates for all bracket jobs with known STL volume
and displays it as a badge next to the bracket name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /jobs/{id}/cancel sets status → cancelled, frees printer if printing,
logs 'Job cancelled'. Button visible in all non-terminal states (queued, printing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- filamentdb.get_ams_slots(): parses /ams/ HTML to extract slot, material,
color, weight_remaining_g, roll_id for each loaded AMS slot
- filamentdb.find_matching_ams_roll(): filters by material+color (case-insensitive),
picks the roll with least filament remaining
- filamentdb.log_print_on_roll(): POSTs to /rolls/{id}/prints
- start_job: after setting job to printing, looks up AMS, computes weight
(math.ceil of estimate), logs against matching roll, writes result to job log
- Verified parser against live AMS: PLA Black → slot 3 (30g) over slot 1 (1000g)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /files/{filename}: serves file from STL_UPLOAD_DIR with path-traversal guard
- Download button in File & Geometry card header (always visible when file attached)
- Download button also appears at the top of the Start Job modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walls are fixed thickness regardless of model size, so small models are
wall-dominated (high material factor) while large models are infill-dominated.
_effective_factor() uses log-linear interpolation:
factor = MATERIAL_FACTOR - log10(volume / 25 cm³) × 0.15
Example curve:
0.5 cm³ → 0.76 1 cm³ → 0.71 10 cm³ → 0.56
26 cm³ → 0.50 100 cm³ → 0.41 500 cm³ → 0.31
MATERIAL_FACTOR env var is now the anchor at 25 cm³ (validated reference).
The card header shows the effective (size-adjusted) factor for the specific job.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 20% infill assumption ignored walls and solid layers which dominate
most prints. Validated against Bambu Lab Studio for a GF lid:
model volume 26.09 cm³ × 0.50 × 1.24 g/cm³ = 16.2 g (actual: 16.24 g)
16.2 g / 32 g/h × 60 = 30 min (actual: 30.5 min)
New env vars (read at request time, no rebuild needed):
MATERIAL_FACTOR=0.50 fraction of model volume that becomes filament
PRINT_SPEED_G_PER_HOUR=32 matches Bambu Lab at normal quality
Card header now shows active assumptions including speed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /jobs/{id}/start: modal picks from idle/offline printers,
sets job → printing, printer → printing, logs "Job started on {name}"
- POST /jobs/{id}/stop: job → queued, clears printer_id + started_at,
printer → idle, logs "Job stopped (was on {name})"
- POST /jobs/{id}/finish: job → done, sets finished_at + duration_minutes,
printer → idle, logs "Job finished on {name}"
- Job detail shows Start / Stop+Finish buttons based on current status
- All printers currently printing are excluded from the Start printer picker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sort order: printing → queued → rest, then priority desc, then created_at asc.
Finished jobs (done/failed/cancelled) older than 24h are hidden by default;
a footer note shows the count with a 'Show all' link (?show_all=1).
Filter params (bracket_id, q) are preserved on the show-all link.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bracket names in the list now link directly to /jobs?bracket_id=X
- /brackets/{id} redirects to /jobs?bracket_id=X (keeps old links working)
- Bracket link on job detail page updated accordingly
- bracket_detail.html and add_jobs_to_bracket route removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /jobs/{id}/edit updates: customer, material, color, priority, bracket,
printer, status, notes. Bracket field has '— none —' to detach from bracket.
Save auto-logs 'Job updated'. Edit button in page header.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- After job creation always redirect to /jobs (never bracket detail)
- If a bracket was involved, redirect to /jobs?bracket_id=X instead
- GET /jobs?bracket_id=X filters the list and shows a dismissible bracket badge
- Amount label now says '(≥2 creates a bracket)' to make the side effect clear
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- filamentdb.spool_counts(): builds (material, color_name) → roll count
from cached stock report; counts rolls_in_stock + rolls_in_use (non-graveyard)
- Jobs list: coloured dot before material — green (2+), yellow (1), red (0)
- FILAMENTDB_URL added to .env
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /jobs/{id}/analyse clears the stl_cache entry and re-runs trimesh,
then redirects back to the detail page. Button hidden for .gcode files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 0010: priority BOOLEAN NOT NULL DEFAULT 0 on print_jobs
- Priority jobs sort above normal jobs in the list
- Checkbox in Add Job modal; warning icon in list; badge on detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filament removal:
- Deleted app/models/filament.py, routers/filaments.py, templates/filaments.html
- Migration 0009: drops filaments table and filament_id FK from print_jobs
- Removed Filaments from sidebar and all internal references
FilamentDB integration (read-only):
- app/filamentdb.py: GET /reports/stock with 5-min in-memory cache
- available_materials() / available_colors() populate job form dropdowns
- Falls back to static constants if FilamentDB unreachable
- Dashboard filament count now from FilamentDB rolls_in_stock
- FILAMENTDB_URL added to .env.install; httpx added to requirements
Job detail page (/jobs/{id}):
- Two cards: Job info (customer, material, printer, bracket, timing, cost)
and File & Geometry (bounding box + volume from stl_cache)
- Log timeline with Add Entry modal
- Filename in jobs list links to detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app/constants.py: MATERIALS and COLORS standard value lists
- Migration 0008: job_material + job_color columns on print_jobs
- Job creation (both /jobs and /brackets/{id}/jobs) stores these independently
of the filament inventory
- All job tables updated to display job_material · job_color
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Amount > 1 with no bracket selected: auto-creates a bracket named
'{customer} – {file} ×{amount}', assigns all jobs to it, and redirects
to the new bracket page. Amount = 1 behaves as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JobBracket model: name, customer, notes
- Migration 0006: job_brackets table + bracket_id FK on print_jobs
- /brackets: list with per-bracket progress bar
- /brackets/{id}: detail with jobs table + Add Job(s) modal (quantity field
creates N identical jobs in one shot, each with its own UUID and log entry)
- /jobs Add Job modal: optional bracket selector
- Sidebar: Brackets nav item added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PrintJob: job_uuid (UUID4), customer, logs relationship
- JobLog model + migration 0004 (also includes job_logs table)
- POST /jobs: upload STL or select from library, auto-logs 'Job created'
- GET /jobs?q=: search by customer or filename across all statuses
- app/stl.py: STL_UPLOAD_DIR helper (from env, default /data/stl)
- docker-compose: named volume stl_files mounted at /data/stl
- .env.install: added STL_UPLOAD_DIR and TZ=Europe/Berlin
- CLAUDE.md: full project context for future sessions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Printer: bought_at, last_maintenance_at, decommissioned_at (all Date, nullable)
- PrinterLog: per-printer timestamped text entries (CASCADE delete)
- Migration 0003: adds columns + printer_logs table
- /printers/{id}: detail page showing lifecycle info + scrollable log
- POST /printers/{id}/log: add a log entry via modal form
- Printer names in list are now links to the detail page
- Add Printer modal includes optional purchase date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PrinterType model: name, bed_x_mm/y/z, linked to Printer via type_id
- Migration 0002: creates printer_types, adds type_id to printers, drops model
- /printer-types page: table + Add Type modal form
- /printers page: Add Printer modal with type dropdown (disabled if no types)
- API: GET/POST/DELETE /api/printer-types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- basicConfig so the pops logger actually emits output
- Global exception handler logs full traceback + returns detail in 500
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app/templates/: base layout with sidebar nav + 6 pages
(dashboard, printers, jobs, filaments, todos, pricing)
- app/routers/ui.py: HTML routes querying the DB live
- API routes moved to /api/* prefix
- requirements: jinja2, python-multipart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>