60 Commits

Author SHA1 Message Date
Martin Hohenberg
ff2b618414 Duplicate job 2026-07-06 17:17:22 +02:00
Martin Hohenberg
38e6d38a2f Show open Vikunja task count as badge on To-Do sidebar link
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>
2026-06-20 19:16:19 +02:00
Martin Hohenberg
f713fbe2fb Count cancelled jobs as done in bracket progress bar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 19:15:43 +02:00
Martin Hohenberg
7dec15d777 Exclude cancelled jobs from bracket progress bar count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:26:02 +02:00
Martin Hohenberg
5270d96b92 Sort jobs: internal always last within each priority stratum
Order: priority customer > priority internal > normal customer > normal internal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:21:19 +02:00
Martin Hohenberg
5a61a35839 Default customer to 'internal' when left blank
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:19:52 +02:00
Martin Hohenberg
dc8918d323 Isolate STL analysis in child process; fix 3MF memory usage
- _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>
2026-06-19 11:32:15 +02:00
Martin Hohenberg
6a1f146d8e Background STL analysis with computing spinner; manual geometry override
- 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>
2026-06-19 11:07:44 +02:00
Martin Hohenberg
fc13d905db Add networkx and lxml for trimesh 3MF support
Both are required by trimesh's threemf loader as soft dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 10:59:43 +02:00
Martin Hohenberg
ab52721c7f Fix 3MF analysis: concatenate scene geometries instead of force="mesh"
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>
2026-06-19 10:56:21 +02:00
Martin Hohenberg
1e74b3a149 Auto-analyse STL on job detail load; warn if file missing
- 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>
2026-06-19 10:54:21 +02:00
Martin Hohenberg
726a4d3c02 Fix UnboundLocalError for filament_low in start_job
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>
2026-06-19 08:00:16 +02:00
Martin Hohenberg
5dfa700916 Tag all Vikunja tasks with #POPS label; fix Vikunja URL
- 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>
2026-06-19 07:58:20 +02:00
Martin Hohenberg
a2e8c9e194 Replace internal todos with Vikunja API wrapper
- 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>
2026-06-19 07:40:54 +02:00
Martin Hohenberg
223a1b7f1e Auto-create todos on print events
- '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>
2026-06-19 07:37:16 +02:00
Martin Hohenberg
8ecace0493 Restyle job status badges
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>
2026-06-19 07:33:38 +02:00
Martin Hohenberg
0569cd4f02 Add printer cost (€0.35/h) to price estimation
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>
2026-06-19 07:28:46 +02:00
Martin Hohenberg
c94eeb071f Show bracket gross total price on bracket filter view
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>
2026-06-19 07:26:28 +02:00
Martin Hohenberg
cb5a2bc394 Add inline SVG logo to sidebar (nozzle + layers mark)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 05:49:51 +02:00
Martin Hohenberg
2fb08acd5b Add cancel job action
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>
2026-06-18 23:00:42 +02:00
Martin Hohenberg
17097061c3 Log filament usage to FilamentDB when starting a job
- 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>
2026-06-18 22:59:33 +02:00
Martin Hohenberg
bef57027f2 Add file download for job models
- 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>
2026-06-18 22:52:26 +02:00
Martin Hohenberg
51a7791461 Scale material factor by model size (smaller=denser, larger=fluffier)
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>
2026-06-18 22:50:32 +02:00
Martin Hohenberg
908cfbc981 Fix estimation: replace 20% infill with 50% material factor, 32 g/h speed
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>
2026-06-18 22:48:56 +02:00
Martin Hohenberg
6df42800ee Add start, stop, and finish job actions
- 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>
2026-06-18 22:45:46 +02:00
Martin Hohenberg
bc6e06a2e2 Add cost/time estimate card to job detail page
app/estimation.py:
  - 20% infill → material volume → weight (density per material)
  - print time at 20 g/h (conservative FDM average)
  - filament cost at FILAMENT_PRICE_PER_KG EUR/kg (env, default 15)
  - +20% misprint buffer, +30% gross profit margin
  - net price (ex. VAT) and gross price (VAT_RATE %, env, default 19)

Card shows three columns: Material & Time / Production cost / Suggested price.
Only rendered when STL volume is available. Assumptions shown in card header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:40:52 +02:00
Martin Hohenberg
4a3c370317 Job list: sort by status/priority/age, hide old finished jobs by default
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>
2026-06-18 22:35:36 +02:00
Martin Hohenberg
6c60dc5960 Replace bracket detail page with /jobs?bracket_id= filter
- 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>
2026-06-18 22:33:55 +02:00
Martin Hohenberg
6e43557ae0 Add job edit modal on detail page
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>
2026-06-18 22:30:18 +02:00
Martin Hohenberg
109306f5f8 Fix post-create redirect and add bracket filter to jobs list
- 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>
2026-06-18 22:28:51 +02:00
Martin Hohenberg
392acef28e Show spool availability indicator per job on jobs list
- 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>
2026-06-18 22:22:32 +02:00
Martin Hohenberg
bc1769625e Link filament spools card on dashboard to FilamentDB (new tab)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:20:49 +02:00
Martin Hohenberg
b9710faf73 Add 'Compute now' button to job detail File & Geometry card
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>
2026-06-18 22:20:01 +02:00
Martin Hohenberg
13422ba29b Remove stale Filament query references left after filament removal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:18:28 +02:00
Martin Hohenberg
6f85cd5d1c Add priority flag to jobs
- 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>
2026-06-18 22:17:55 +02:00
Martin Hohenberg
e7df987e5b Fix missing Numeric import in print_job model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:16:17 +02:00
Martin Hohenberg
1259a7294a Remove internal filament management, integrate FilamentDB, add job detail page
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>
2026-06-18 22:12:47 +02:00
Martin Hohenberg
6868862d77 Replace combined filament dropdown with separate material + color dropdowns
- 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>
2026-06-18 22:08:29 +02:00
Martin Hohenberg
dc73c9a8c1 Add amount field to job creation — auto-creates bracket when > 1
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>
2026-06-18 22:06:35 +02:00
Martin Hohenberg
4c7acb1c52 Analyse STL/3MF on upload: bounding box + volume, cached in stl_cache
- StlCache model: filename, cube_x/y/z (mm), volume_ccm
- Migration 0007: stl_cache table
- app/stl_analysis.py: trimesh-based analyse() + get_cache_map()
  - Runs immediately after upload in both /jobs and /brackets/{id}/jobs
  - Skips .gcode (no geometry), caches per filename, idempotent
- Library selector in both modals now shows dimensions and volume
- requirements: trimesh==4.5.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:04:11 +02:00
Martin Hohenberg
ccf6a64787 Add job brackets for grouping multi-part or repeated jobs
- 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>
2026-06-18 22:03:01 +02:00
Martin Hohenberg
977e0deb24 Make printer optional on job creation — assigned when job starts
Migration 0005 makes print_jobs.printer_id nullable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 21:57:07 +02:00
Martin Hohenberg
fe594e8675 Add job creation with UUID, STL upload/library, search, job log, TZ, and CLAUDE.md
- 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>
2026-06-18 21:55:49 +02:00
Martin Hohenberg
c78f8c91cd Add printer lifecycle fields and printer log
- 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>
2026-06-18 21:47:48 +02:00
Martin Hohenberg
750edbbcec Add printer types with bed volume + add-printer UI
- 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>
2026-06-18 21:46:26 +02:00
Martin Hohenberg
5fb15b36bf Grant ALL PRIVILEGES so Alembic can run DDL migrations
CRUD-only was too restrictive — CREATE TABLE and ALTER TABLE are needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 21:40:19 +02:00
Martin Hohenberg
ea1e383cfc Fix logging and surface unhandled exceptions
- 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>
2026-06-18 21:38:40 +02:00
Martin Hohenberg
541bb88847 Catch migration exceptions so app doesn't crash-loop on startup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 21:37:02 +02:00
Martin Hohenberg
a4010c63a2 Copy alembic/ and alembic.ini into Docker image
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 21:35:14 +02:00
Martin Hohenberg
ee0b1b39e9 Add Jinja2 web UI with Bootstrap 5 dark theme
- 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>
2026-06-18 21:26:23 +02:00