feat: Create Dockerfile.app (node→nginx multi-stage), nginx.conf (SPA +…
- "docker/Dockerfile.app" - "docker/nginx.conf" - "docker-compose.yml" GSD-Task: S02/T01
This commit is contained in:
parent
c60dd59c01
commit
06b6045d8c
20 changed files with 5210 additions and 17 deletions
|
|
@ -24,6 +24,8 @@ Agents read this before every unit. Add entries when you discover something wort
|
|||
| P010 | Font Y-axis flip: canvas_y = ascender - font_y * scale | app/src/utils/fontService.ts | opentype.js uses font coordinate system (Y-up) while canvas uses screen coordinates (Y-down). Apply `ascender * scale - font_y` to all Y values in path data. Without this, text renders upside-down. |
|
||||
| P011 | Letter spacing requires manual per-character glyph positioning | app/src/utils/fontService.ts | opentype.js getPath() doesn't support letter spacing. Must iterate characters, get individual glyph paths, accumulate x-advance + spacing per character, and compose the final SVG d-attribute manually. |
|
||||
| P012 | Adding new CanvasObject types requires exhaustive switch updates in 6 files | app/src/types/canvas.ts, KonvaStage, CanvasToolbar, ObjectPanel, ShapeProperties, AlignmentBar | TypeScript noFallthroughCasesInSwitch enforces exhaustive handling. When adding a new type to the CanvasObject union, all switch statements across these 6 files must be updated or compilation fails. |
|
||||
| P013 | Export service: compose → validate → download is pure-function pipeline | app/src/utils/exportService.ts | composeCanvasSVG() builds SVG string, validateForExport() checks for errors/warnings, triggerDownload() creates blob + hidden anchor. All pure functions, no React deps. ExportView orchestrates them. |
|
||||
| P014 | Canvas state lifted to App.tsx for cross-view sharing | app/src/App.tsx | useCanvasState() called in App.tsx, all 14 return values spread as props to DesignCanvas via UseCanvasStateReturn interface. ExportView receives objects + artboard. PNG captured via stageRef before view transition since Konva stage unmounts. |
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
|
|
@ -39,3 +41,6 @@ Agents read this before every unit. Add entries when you discover something wort
|
|||
| L008 | Engine container has dual health endpoints | /health on root (main.py) and /engine/health on router (routes.py) both exist | Use /engine/health for Docker HEALTHCHECK and external probes for namespace consistency | engine API, Docker |
|
||||
| L009 | useCallback-based triggerTrace caused infinite re-render loops | Object params create new references each render, retriggering useEffect even when values are unchanged | Restructure hook to use single useEffect with JSON.stringify(params) in dependency array instead of useCallback-based approach | app hooks, React |
|
||||
| L010 | jest-canvas-mock crashes in Vitest with "ReferenceError: jest is not defined" | jest-canvas-mock internally calls jest.fn() in createImageBitmap.js — Jest globals don't exist in Vitest | Use vitest-canvas-mock (a Vitest-compatible fork) instead. Import it in test-setup.ts. | app tests, canvas |
|
||||
| L011 | `npx tsc -b --noEmit` and `npx vitest run` fail from project root | Root has no node_modules with tsc/vitest, and vitest doesn't pick up app/vite.config.ts jsdom environment | Root package.json with `"workspaces": ["app"]` + `npm install` hoists deps. Root tsconfig.json references `./app`. Root vitest.config.ts uses `test.projects: ['app/vite.config.ts']` to delegate. | monorepo, testing |
|
||||
| L012 | DXF scale_factor is caller-computed: 1/96 for inches, 25.4/96 for mm (assuming 96 PPI artboard) | Engine generate_dxf() applies scale_factor uniformly to all coordinates but doesn't know PPI — caller must compute | App exportService computes scaleFactor based on artboard unit. Engine API `/engine/simplify` accepts `units` and `scale_factor` as form params. | engine-app contract |
|
||||
| L013 | nginx:alpine healthcheck with `wget -qO- http://localhost:80/` fails | `localhost` resolves to `::1` (IPv6) in Alpine containers, but nginx default listens on `0.0.0.0` (IPv4 only) | Use `127.0.0.1` explicitly: `wget -qO- http://127.0.0.1:80/` | Docker, healthcheck |
|
||||
|
|
|
|||
|
|
@ -14,16 +14,25 @@ Built the complete React frontend:
|
|||
- **View 2 (Design Canvas):** Konva.js-powered 2D environment with artboard shapes (rect, circle, ellipse, shield, pennant), basic shapes, text objects with opentype.js font loading and text-to-path conversion, layers panel, alignment tools, property editing, keyboard shortcuts, undo/redo
|
||||
- **95 tests, zero TypeScript errors**, 54 source files, 10,721 lines of code
|
||||
|
||||
## Queued Milestones
|
||||
## In-Progress Milestones
|
||||
|
||||
### ⬜ M003: Export, Deployment & Embedding
|
||||
Export pipeline (SVG/DXF download from canvas), production deployment, embeddable widget.
|
||||
### 🔄 M003: Export, Deployment & Embedding
|
||||
|
||||
**S01: Export Flow (View 3) + DXF Generation — ✅ Complete**
|
||||
- Engine: Extended generate_dxf() with units/scale_factor/layer_map, $INSUNITS/$MEASUREMENT headers, /engine/simplify API wiring
|
||||
- App: Lifted useCanvasState to App.tsx for cross-view state sharing, created exportService.ts (composeCanvasSVG, validateForExport, triggerDownload), added exportAsDxf API client
|
||||
- App: Built ExportView with DXF/SVG/PNG format selector, validation panel, unit selector, download wiring
|
||||
- **120 app tests + 36 engine output tests, zero TypeScript errors**
|
||||
|
||||
**S02: Docker Packaging + README — ⬜ Queued**
|
||||
|
||||
**S03: Embed Mode — ⬜ Queued**
|
||||
|
||||
## Tech Stack
|
||||
- **Engine:** Python 3.11, FastAPI, OpenCV, pypotrace, vtracer, ezdxf
|
||||
- **App:** Vite, React 18, TypeScript (strict), Konva.js, opentype.js
|
||||
- **App:** Vite, React 19, TypeScript (strict), Konva.js, opentype.js
|
||||
- **Testing:** pytest (engine), Vitest + testing-library (app)
|
||||
- **Infrastructure:** Docker multi-stage build, GHCR
|
||||
- **Infrastructure:** Docker multi-stage build, GHCR, npm workspaces monorepo
|
||||
|
||||
## Key Architecture Decisions
|
||||
- D001: Engine is standalone module, App consumes via HTTP API only
|
||||
|
|
|
|||
|
|
@ -35,3 +35,6 @@
|
|||
{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T02"},"ts":"2026-03-26T06:19:28.695Z","actor":"agent","hash":"20e62f4b5af835c3","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T03"},"ts":"2026-03-26T06:26:04.608Z","actor":"agent","hash":"b3de5441cc811cf7","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T04"},"ts":"2026-03-26T06:29:08.965Z","actor":"agent","hash":"c8adae40d118a764","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-slice","params":{"milestoneId":"M003","sliceId":"S01"},"ts":"2026-03-26T06:35:25.542Z","actor":"agent","hash":"165605976be6d69e","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"plan-slice","params":{"milestoneId":"M003","sliceId":"S02"},"ts":"2026-03-26T06:39:00.941Z","actor":"agent","hash":"24790b4f3bd69e22","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S02","taskId":"T01"},"ts":"2026-03-26T06:43:57.680Z","actor":"agent","hash":"bd4dc62bfd702053","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ Complete the Kerf App with the Export view (View 3), Docker Compose packaging fo
|
|||
## Slice Overview
|
||||
| ID | Slice | Risk | Depends | Done | After this |
|
||||
|----|-------|------|---------|------|------------|
|
||||
| S01 | Export Flow (View 3) + DXF Generation | high — dxf scale accuracy and geometry quality | — | ⬜ | Design a sign with text + imported vector, export as DXF, open in Inkscape/LightBurn with correct geometry and scale |
|
||||
| S01 | Export Flow (View 3) + DXF Generation | high — dxf scale accuracy and geometry quality | — | ✅ | Design a sign with text + imported vector, export as DXF, open in Inkscape/LightBurn with correct geometry and scale |
|
||||
| S02 | Docker Packaging + README | low — docker packaging is well-understood pattern | S01 | ⬜ | docker-compose up starts all services; Engine container runs independently; healthchecks pass |
|
||||
| S03 | Embed Mode | medium — shadow dom + konva.js + font loading interactions | S02 | ⬜ | Embed snippet in plain HTML page; component renders; styles don't bleed; download works from embedded context |
|
||||
|
|
|
|||
130
.gsd/milestones/M003/slices/S01/S01-SUMMARY.md
Normal file
130
.gsd/milestones/M003/slices/S01/S01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
id: S01
|
||||
parent: M003
|
||||
milestone: M003
|
||||
provides:
|
||||
- ExportView component with DXF/SVG/PNG download
|
||||
- exportService.ts utility functions (composeCanvasSVG, validateForExport, triggerDownload)
|
||||
- exportAsDxf() API client function
|
||||
- Engine DXF generation with real-world units and scale
|
||||
- Root monorepo config (package.json workspaces, tsconfig references, vitest projects)
|
||||
requires:
|
||||
[]
|
||||
affects:
|
||||
- S02
|
||||
- S03
|
||||
key_files:
|
||||
- engine/output/dxf.py
|
||||
- engine/api/routes.py
|
||||
- engine/tests/test_output.py
|
||||
- app/src/App.tsx
|
||||
- app/src/views/DesignCanvas.tsx
|
||||
- app/src/views/ExportView.tsx
|
||||
- app/src/views/ExportView.module.css
|
||||
- app/src/utils/exportService.ts
|
||||
- app/src/utils/__tests__/exportService.test.ts
|
||||
- app/src/api/engine.ts
|
||||
- app/src/api/__tests__/engine.test.ts
|
||||
- package.json
|
||||
- tsconfig.json
|
||||
- vitest.config.ts
|
||||
key_decisions:
|
||||
- DXF $INSUNITS codes: inches=1, mm=4; $MEASUREMENT: inches=0, mm=1 — caller computes scale_factor
|
||||
- Canvas state lifted to App.tsx via UseCanvasStateReturn interface for cross-view sharing
|
||||
- Export service uses pure-function pipeline: composeCanvasSVG → validateForExport → triggerDownload
|
||||
- PNG captured before view transition (stageRef.toDataURL) since Konva stage unmounts in export view
|
||||
- Root npm workspaces + vitest projects config enables monorepo-wide verification from project root
|
||||
patterns_established:
|
||||
- Pure-function export pipeline (compose → validate → download) with no React dependencies
|
||||
- npm workspaces monorepo with root tsconfig references and vitest workspace projects
|
||||
- Pre-transition state capture pattern for data that requires a mounted component
|
||||
observability_surfaces:
|
||||
- none
|
||||
drill_down_paths:
|
||||
- .gsd/milestones/M003/slices/S01/tasks/T01-SUMMARY.md
|
||||
- .gsd/milestones/M003/slices/S01/tasks/T02-SUMMARY.md
|
||||
- .gsd/milestones/M003/slices/S01/tasks/T03-SUMMARY.md
|
||||
- .gsd/milestones/M003/slices/S01/tasks/T04-SUMMARY.md
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-26T06:35:25.484Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# S01: Export Flow (View 3) + DXF Generation
|
||||
|
||||
**Built the complete Export view (View 3) with DXF/SVG/PNG format selection, pre-export validation, unit-aware DXF generation with real-world scale, and browser download — wired end-to-end from canvas state through engine API to file download.**
|
||||
|
||||
## What Happened
|
||||
|
||||
This slice delivered the complete export pipeline across both engine and app:
|
||||
|
||||
**Engine (T01):** Extended `generate_dxf()` with `units`, `scale_factor`, and `layer_map` parameters. DXF files now include correct `$INSUNITS` headers (1=inches, 4=mm) and `$MEASUREMENT` headers (0=imperial, 1=metric). `scale_factor` is applied uniformly to all polyline coordinates, enabling pixel-to-real-world unit conversion. The `/engine/simplify` API endpoint was extended with optional `units` and `scale_factor` form params that pass through to the DXF generator via `_format_response()`. 11 new tests verify scale conversion accuracy (384×576 px artboard → 4×6 inches or 101.6×152.4 mm).
|
||||
|
||||
**App Architecture (T02):** Lifted `useCanvasState()` from DesignCanvas to App.tsx so canvas state is accessible across views. DesignCanvas now receives all 14 state/action properties as props via the `UseCanvasStateReturn` interface. A `stageRef` is created in App.tsx and passed to DesignCanvas for PNG capture. The `handleExport` callback captures a 2x PNG data URL from the Konva stage before navigating to the export view (since the stage unmounts when switching views).
|
||||
|
||||
**Export Service (T03):** Created `exportService.ts` with three pure utility functions: `composeCanvasSVG()` renders visible canvas objects into SVG with correct viewBox and real-world unit dimensions, `validateForExport()` checks for blocking errors (unconverted text, missing artboard) and warnings (raster images), `triggerDownload()` creates a hidden anchor + blob URL for browser download. Added `exportAsDxf()` to the engine API client that POSTs SVG as FormData to `/engine/simplify` with DXF params and returns a raw blob.
|
||||
|
||||
**Export View (T04):** Built `ExportView` component with format selector (DXF/SVG/PNG cards), unit selector (inches/mm, shown for DXF/SVG only), validation panel (red blocking errors, yellow warnings), canvas preview thumbnail, and download button. Each format has its own export flow: DXF composes SVG → calls engine API → downloads blob; SVG composes SVG → downloads string blob; PNG uses pre-captured data URL → converts to blob → downloads.
|
||||
|
||||
**Root Config Fix:** Created root `package.json` with npm workspaces, `tsconfig.json` with project references, and `vitest.config.ts` with workspace projects to enable `npx tsc -b --noEmit` and `npx vitest run` from the project root.
|
||||
|
||||
## Verification
|
||||
|
||||
All verification gates pass from project root:
|
||||
- `npx tsc -b --noEmit` — exit 0, zero TypeScript errors
|
||||
- `npx vitest run` — 120/120 tests pass across 8 test files (app)
|
||||
- `cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning` — 36/36 tests pass (engine output)
|
||||
|
||||
Test coverage includes: DXF scale conversion (px→inches, px→mm), DXF header correctness ($INSUNITS, $MEASUREMENT), layer mapping, SVG composition for all canvas object types, export validation (text blocking, raster warnings, missing artboard), API client DXF export, triggerDownload blob handling.
|
||||
|
||||
## Requirements Advanced
|
||||
|
||||
- R019 — ExportView provides DXF/SVG/PNG format selection, validateForExport() catches unconverted text (blocking) and raster images (warning), pre-export validation panel renders errors/warnings with download gating
|
||||
- R020 — Engine generate_dxf() sets $INSUNITS and applies scale_factor to coordinates; exportService computes scale_factor from artboard PPI (1/96 for inches, 25.4/96 for mm); verified via tests that 384x576 px → 4x6 inch DXF
|
||||
|
||||
## Requirements Validated
|
||||
|
||||
- R019 — ExportView with format selector, validation panel, and download wiring. 120 tests pass including validateForExport() tests for text blocking and raster warnings.
|
||||
- R020 — DXF scale conversion tests: 384×576 px artboard → 4×6 inch and 101.6×152.4 mm coordinates with correct $INSUNITS headers. 11 dedicated engine tests.
|
||||
|
||||
## New Requirements Surfaced
|
||||
|
||||
None.
|
||||
|
||||
## Requirements Invalidated or Re-scoped
|
||||
|
||||
None.
|
||||
|
||||
## Deviations
|
||||
|
||||
- T01: ezdxf R2000 defaults $INSUNITS to 6 (meters) not 0 (unitless) as originally assumed — test adjusted accordingly.
|
||||
- T03: Fixed 8 pre-existing tsc errors in files outside T03 scope (useDebouncedTrace.test.ts, fontService.test.ts, fontService.ts, ImportConvert.tsx, vite.config.ts) that were blocking the verification gate.
|
||||
- Added root package.json (npm workspaces), tsconfig.json, and vitest.config.ts to enable verification commands from project root — these were not in the slice plan but required for the verification gate.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- DXF layer assignment is supported in the engine but not exposed in the ExportView UI — all paths go to the default layer. Can be added in a future slice if needed.
|
||||
- PNG export captures at 2x pixel ratio; there's no user-selectable resolution option.
|
||||
- SVG images are detected by checking src for 'image/svg+xml' or .svg extension; edge cases with unusual MIME types could be missed.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `engine/output/dxf.py` — Added units, scale_factor, layer_map keyword args to generate_dxf(); sets $INSUNITS/$MEASUREMENT headers; applies scale_factor to polyline coords
|
||||
- `engine/api/routes.py` — Added units and scale_factor Form params to /engine/simplify; passes through to _format_response() → generate_dxf()
|
||||
- `engine/tests/test_output.py` — Added 11 tests: scale conversion (inches/mm), header correctness, layer mapping, combined features
|
||||
- `app/src/App.tsx` — Lifted useCanvasState() here, creates stageRef, captures PNG data URL on export, routes to ExportView with all props
|
||||
- `app/src/views/DesignCanvas.tsx` — Receives canvas state/actions as props instead of calling useCanvasState(); uses passed stageRef; adds Export button
|
||||
- `app/src/views/ExportView.tsx` — New component: format selector, unit selector, validation panel, preview thumbnail, download button with per-format export flows
|
||||
- `app/src/views/ExportView.module.css` — New CSS module for ExportView layout and styling
|
||||
- `app/src/utils/exportService.ts` — New: composeCanvasSVG(), validateForExport(), triggerDownload() pure utility functions
|
||||
- `app/src/utils/__tests__/exportService.test.ts` — New: 21 tests for SVG composition, validation, and download trigger
|
||||
- `app/src/api/engine.ts` — Added exportAsDxf() function: POSTs SVG blob via FormData to /engine/simplify with DXF params
|
||||
- `app/src/api/__tests__/engine.test.ts` — Added exportAsDxf tests for FormData construction and blob response
|
||||
- `package.json` — New root package.json with npm workspaces pointing to app/
|
||||
- `tsconfig.json` — New root tsconfig.json with project reference to ./app
|
||||
- `vitest.config.ts` — New root vitest config delegating to app/vite.config.ts via test.projects
|
||||
152
.gsd/milestones/M003/slices/S01/S01-UAT.md
Normal file
152
.gsd/milestones/M003/slices/S01/S01-UAT.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# S01: Export Flow (View 3) + DXF Generation — UAT
|
||||
|
||||
**Milestone:** M003
|
||||
**Written:** 2026-03-26T06:35:25.485Z
|
||||
|
||||
## UAT: S01 — Export Flow (View 3) + DXF Generation
|
||||
|
||||
### Preconditions
|
||||
- Engine running at localhost:8000 (`cd engine && .venv/bin/python -m uvicorn api.main:app --host 0.0.0.0 --port 8000`)
|
||||
- App dev server running at localhost:5173 (`cd app && npm run dev`)
|
||||
- A raster image available for tracing (e.g., a simple logo PNG)
|
||||
- Inkscape or LightBurn installed for DXF verification (optional but recommended)
|
||||
|
||||
---
|
||||
|
||||
### Test 1: Navigate to Export View
|
||||
**Steps:**
|
||||
1. Open http://localhost:5173
|
||||
2. Upload a raster image in View 1
|
||||
3. Select a preset and click "Use This" to proceed to View 2 (Design Canvas)
|
||||
4. Add at least one shape or imported vector to the canvas
|
||||
5. Click the "Export" button in the toolbar
|
||||
|
||||
**Expected:**
|
||||
- App transitions to the Export view (View 3)
|
||||
- A canvas preview thumbnail is displayed showing the design
|
||||
- Format selector shows three options: DXF, SVG, PNG
|
||||
- DXF is selected by default (or first format card is highlighted)
|
||||
- No validation errors appear if all objects are vector-based
|
||||
|
||||
---
|
||||
|
||||
### Test 2: Pre-Export Validation — Unconverted Text Blocking
|
||||
**Steps:**
|
||||
1. In View 2, add a text object (type some text, do NOT convert to paths)
|
||||
2. Click "Export"
|
||||
|
||||
**Expected:**
|
||||
- Export view shows a red blocking error: text indicating unconverted text objects
|
||||
- Download button is disabled
|
||||
- Error message identifies the text object that needs conversion
|
||||
|
||||
---
|
||||
|
||||
### Test 3: Pre-Export Validation — Raster Image Warning
|
||||
**Steps:**
|
||||
1. In View 2, import a raster image (PNG/JPG) onto the canvas
|
||||
2. Click "Export"
|
||||
|
||||
**Expected:**
|
||||
- Export view shows a yellow warning about raster images
|
||||
- Download button remains enabled (warnings don't block export)
|
||||
- Warning text indicates raster images will be skipped in vector export
|
||||
|
||||
---
|
||||
|
||||
### Test 4: DXF Export with Inches
|
||||
**Steps:**
|
||||
1. Set up a design with an artboard (e.g., 4×6 inch rectangle)
|
||||
2. Add vector objects (shapes, imported SVGs)
|
||||
3. Navigate to Export view
|
||||
4. Select DXF format
|
||||
5. Select "inches" as the unit
|
||||
6. Click Download
|
||||
|
||||
**Expected:**
|
||||
- Browser downloads a file named `export.dxf`
|
||||
- Open the DXF in Inkscape or a text editor:
|
||||
- `$INSUNITS` header value is `1` (inches)
|
||||
- `$MEASUREMENT` header value is `0` (imperial)
|
||||
- Polyline coordinates span approximately 0–4 × 0–6 (matching artboard dimensions in inches)
|
||||
- Geometry renders correctly with no corrupt paths
|
||||
|
||||
---
|
||||
|
||||
### Test 5: DXF Export with Millimeters
|
||||
**Steps:**
|
||||
1. Same design as Test 4
|
||||
2. Select DXF format, select "mm" as the unit
|
||||
3. Click Download
|
||||
|
||||
**Expected:**
|
||||
- Downloaded `export.dxf` has:
|
||||
- `$INSUNITS` header value is `4` (mm)
|
||||
- `$MEASUREMENT` header value is `1` (metric)
|
||||
- Coordinates span approximately 0–101.6 × 0–152.4 (4×6 inches in mm)
|
||||
|
||||
---
|
||||
|
||||
### Test 6: SVG Export
|
||||
**Steps:**
|
||||
1. Design with vector objects on artboard
|
||||
2. Navigate to Export view
|
||||
3. Select SVG format
|
||||
4. Select unit (inches or mm)
|
||||
5. Click Download
|
||||
|
||||
**Expected:**
|
||||
- Browser downloads `export.svg`
|
||||
- SVG file opens correctly in a browser or Inkscape
|
||||
- SVG has correct `viewBox` matching artboard pixel dimensions
|
||||
- SVG `width`/`height` attributes use real-world units (e.g., `width="4in"`)
|
||||
- All visible canvas objects are present as SVG elements (rect, circle, ellipse, polyline, etc.)
|
||||
- Hidden objects are not included
|
||||
|
||||
---
|
||||
|
||||
### Test 7: PNG Export
|
||||
**Steps:**
|
||||
1. Design with any objects (vector or raster)
|
||||
2. Navigate to Export view
|
||||
3. Select PNG format
|
||||
|
||||
**Expected:**
|
||||
- Unit selector is hidden (not applicable for PNG)
|
||||
- Validation panel may not show vector-specific warnings
|
||||
- Click Download → browser downloads `export.png`
|
||||
- PNG is a 2x resolution capture of the canvas
|
||||
|
||||
---
|
||||
|
||||
### Test 8: Back to Design Navigation
|
||||
**Steps:**
|
||||
1. From Export view, click "← Back to Design" button
|
||||
|
||||
**Expected:**
|
||||
- App navigates back to View 2 (Design Canvas)
|
||||
- All canvas objects and state are preserved
|
||||
- Can make edits and return to Export view again
|
||||
|
||||
---
|
||||
|
||||
### Test 9: Empty Canvas Export Attempt
|
||||
**Steps:**
|
||||
1. Navigate to Export view with no artboard set
|
||||
|
||||
**Expected:**
|
||||
- Validation shows a blocking error about missing artboard
|
||||
- Download button is disabled
|
||||
|
||||
---
|
||||
|
||||
### Test 10: Automated Test Verification
|
||||
**Steps:**
|
||||
1. From project root: `npx tsc -b --noEmit`
|
||||
2. From project root: `npx vitest run`
|
||||
3. From engine dir: `cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning`
|
||||
|
||||
**Expected:**
|
||||
- TypeScript compilation: zero errors
|
||||
- Vitest: 120/120 tests pass across 8 test files
|
||||
- Engine pytest: 36/36 tests pass including DXF scale/units tests
|
||||
30
.gsd/milestones/M003/slices/S01/tasks/T04-VERIFY.json
Normal file
30
.gsd/milestones/M003/slices/S01/tasks/T04-VERIFY.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T04",
|
||||
"unitId": "M003/S01/T04",
|
||||
"timestamp": 1774506561926,
|
||||
"passed": false,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd app",
|
||||
"exitCode": 0,
|
||||
"durationMs": 4,
|
||||
"verdict": "pass"
|
||||
},
|
||||
{
|
||||
"command": "npx tsc -b --noEmit",
|
||||
"exitCode": 1,
|
||||
"durationMs": 729,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "npx vitest run",
|
||||
"exitCode": 1,
|
||||
"durationMs": 1566,
|
||||
"verdict": "fail"
|
||||
}
|
||||
],
|
||||
"retryAttempt": 1,
|
||||
"maxRetries": 2
|
||||
}
|
||||
|
|
@ -1,6 +1,43 @@
|
|||
# S02: Docker Packaging + README
|
||||
|
||||
**Goal:** Create Docker Compose configuration with kerf-engine, kerf-app, kerf-server services, environment config, volumes, and healthchecks
|
||||
**Goal:** Package the full Kerf stack (engine + app) into Docker Compose with healthchecks, and provide comprehensive README documentation.
|
||||
**Demo:** After this: docker-compose up starts all services; Engine container runs independently; healthchecks pass
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Create Dockerfile.app (node→nginx multi-stage), nginx.conf (SPA + /engine proxy), and docker-compose.yml with healthchecks — both services start healthy** — Create the three Docker infrastructure files that package the Kerf stack: a multi-stage Dockerfile for the app (node build → nginx), an nginx config that serves static files and proxies /engine/* to the engine container, and a docker-compose.yml that wires both services with healthchecks. Verify by building images and running the stack with curl checks.
|
||||
|
||||
**Key constraints from research:**
|
||||
- The app builds via `tsc -b && vite build` in `app/` — needs all tsconfig files (tsconfig.json, tsconfig.app.json, tsconfig.node.json)
|
||||
- Root `package.json` declares workspaces — run `npm ci` inside `app/` directory to avoid workspace resolution issues, OR copy root package files and install from root
|
||||
- nginx must handle SPA routing with `try_files $uri $uri/ /index.html`
|
||||
- nginx must proxy `/engine/` to `http://kerf-engine:8000` (Docker Compose service name)
|
||||
- Engine Dockerfile already exists at `docker/Dockerfile.engine` — no changes needed
|
||||
- Engine HEALTHCHECK uses `curl -sf http://localhost:8000/engine/health`
|
||||
- App healthcheck: `curl -sf http://localhost:80/` or use nginx built-in
|
||||
- Font MIME types (.ttf/.otf) should be handled by nginx (included by default in nginx:alpine)
|
||||
- App port: 80 internal (nginx default), mapped to 3000 on host
|
||||
- Engine port: 8000 internal, also exposed on host for standalone use
|
||||
- Estimate: 45m
|
||||
- Files: docker/Dockerfile.app, docker/nginx.conf, docker-compose.yml
|
||||
- Verify: docker compose build && docker compose up -d && sleep 12 && docker compose ps && curl -sf http://localhost:8000/engine/health && curl -sf http://localhost:3000/ | head -c 100 && curl -sf http://localhost:3000/engine/health && docker compose down
|
||||
- [ ] **T02: Rewrite README.md with project overview, quick start, API reference, and usage docs** — Replace the placeholder README with comprehensive documentation covering: what Kerf is, quick start with Docker Compose, engine API reference (all 4 endpoints with request/response examples), font system explanation, preset system, engine standalone usage, repository structure, and known limitations.
|
||||
|
||||
**Content outline:**
|
||||
1. Project title + one-paragraph description
|
||||
2. Quick Start — `docker compose up`, then visit localhost:3000
|
||||
3. Repository Structure — engine/, app/, docker/ directories
|
||||
4. Engine API Reference — document all 4 endpoints:
|
||||
- `GET /engine/health` — healthcheck
|
||||
- `GET /engine/presets` — list available presets
|
||||
- `POST /engine/trace` — raster-to-vector with preprocessing+vectorization+postprocessing
|
||||
- `POST /engine/simplify` — SVG simplification with optional DXF export (units, scale_factor)
|
||||
5. Font System — bundled fonts (Lato, OpenSans, Roboto), text-to-path conversion
|
||||
6. Presets — 5 built-in presets (sign, patch, stencil, detailed, custom), how they work
|
||||
7. Engine Standalone Usage — running engine independently, Docker image, API examples
|
||||
8. Development — local setup for engine (Python venv) and app (npm), dev proxy setup
|
||||
9. Known Limitations
|
||||
|
||||
**Sources for API details:** `engine/api/routes.py` for endpoint signatures, `engine/presets/*.json` for preset list, `app/src/utils/fontService.ts` for font system.
|
||||
- Estimate: 30m
|
||||
- Files: README.md
|
||||
- Verify: grep -c '^## ' README.md | xargs test 6 -le && grep -q 'docker compose up' README.md && grep -q '/engine/trace' README.md && grep -q '/engine/simplify' README.md && grep -q '/engine/presets' README.md && grep -q '/engine/health' README.md && echo 'README OK'
|
||||
|
|
|
|||
82
.gsd/milestones/M003/slices/S02/S02-RESEARCH.md
Normal file
82
.gsd/milestones/M003/slices/S02/S02-RESEARCH.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# S02 — Docker Packaging + README — Research
|
||||
|
||||
**Date:** 2026-03-26
|
||||
|
||||
## Summary
|
||||
|
||||
This slice packages the full Kerf stack (engine + app) into Docker Compose and writes a comprehensive README. The work is straightforward — the engine Dockerfile already exists and works (D004, `docker/Dockerfile.engine`), the app is a standard Vite/React build that produces static files, and all app→engine communication uses relative `/engine/*` URLs that only need an nginx reverse proxy.
|
||||
|
||||
The GSD-INITIATE brief mentions a `kerf-server` service, but no server-side code exists and none is needed — the app makes all API calls directly to the engine via `/engine/*` endpoints. The `kerf-app` container should be nginx serving the Vite build output, with a reverse proxy block forwarding `/engine/*` to the `kerf-engine` container. This matches how the Vite dev proxy already works (see `app/vite.config.ts`).
|
||||
|
||||
R021 requires `docker-compose up` to start all services healthy. R022 requires the engine container to be independently deployable. Both are achievable with the existing Dockerfile.engine and a new Dockerfile.app + docker-compose.yml + nginx config.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Build three artifacts: `docker/Dockerfile.app` (multi-stage: node build → nginx), `docker/nginx.conf` (serve static + proxy `/engine/*`), and `docker-compose.yml` (two services with healthchecks). Skip `kerf-server` — it doesn't exist and isn't needed for this milestone. The README should replace the current placeholder with full documentation per the GSD-INITIATE spec (quick start, engine API reference, font system, presets, engine standalone usage).
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### Key Files
|
||||
|
||||
- `docker/Dockerfile.engine` — **Exists, complete.** Multi-stage build with pypotrace compilation. HEALTHCHECK on `/engine/health`. CMD runs uvicorn on port 8000. No changes needed.
|
||||
- `docker/Dockerfile.app` — **New.** Multi-stage: Stage 1 `node:20-slim` runs `npm ci && npm run build` in `app/`. Stage 2 `nginx:alpine` copies the `app/dist/` output + custom nginx.conf. The app's `vite build` command (in `app/package.json`: `"build": "tsc -b && vite build"`) produces static files in `app/dist/`.
|
||||
- `docker/nginx.conf` — **New.** Serves static files from `/usr/share/nginx/html`, reverse-proxies `/engine/` to `http://kerf-engine:8000`. Must handle SPA routing (try_files $uri /index.html). Include font MIME types for .ttf/.otf.
|
||||
- `docker-compose.yml` — **New.** Two services: `kerf-engine` (build from `docker/Dockerfile.engine`, port 8000, healthcheck) and `kerf-app` (build from `docker/Dockerfile.app`, port 80 mapped to host, depends_on kerf-engine healthy). Fonts bundled in the app image via `app/public/fonts/` (3 .ttf files already there — no volume mount needed for V1).
|
||||
- `README.md` — **Rewrite.** Current file is a 20-line placeholder. Replace with comprehensive docs per GSD-INITIATE spec: what Kerf is, quick start (`docker compose up`), engine API reference (4 endpoints), font system, preset system, engine standalone usage, known limitations.
|
||||
- `.dockerignore` — **Exists, adequate.** Already excludes .git, node_modules, .venv, __pycache__, etc.
|
||||
|
||||
### Supporting Files (read-only context)
|
||||
|
||||
- `engine/main.py` — FastAPI app, mounts router, CORS middleware, `/health` endpoint
|
||||
- `engine/api/routes.py` — Router with `/engine/health`, `/engine/presets` (GET), `/engine/trace` (POST), `/engine/simplify` (POST)
|
||||
- `engine/pyproject.toml` — Python deps list (fastapi, uvicorn, opencv-headless, pypotrace, vtracer, multipart, Pillow, ezdxf)
|
||||
- `engine/presets/*.json` — 5 preset files (custom, detailed, patch, sign, stencil)
|
||||
- `app/package.json` — Build script: `tsc -b && vite build`, deps include react, konva, opentype.js
|
||||
- `app/vite.config.ts` — Dev proxy: `/engine` → `http://localhost:8000`
|
||||
- `app/src/api/engine.ts` — All API calls use relative `/engine/*` URLs (no hardcoded host)
|
||||
- `app/public/fonts/` — 3 bundled .ttf files (Lato, OpenSans, Roboto)
|
||||
|
||||
### Build Order
|
||||
|
||||
1. **Dockerfile.app + nginx.conf** — New files, but the pattern is standard (Vite build → nginx). The nginx proxy config is the key piece that replaces the Vite dev proxy for production.
|
||||
2. **docker-compose.yml** — Wires the two services together. Depends on both Dockerfiles existing.
|
||||
3. **README.md** — Independent of Docker files. Can be built in parallel or after.
|
||||
4. **Verification** — `docker compose build` (both images build), `docker compose up -d` (both services start), healthchecks pass, app loads in browser at localhost, engine API responds independently.
|
||||
|
||||
### Verification Approach
|
||||
|
||||
```bash
|
||||
# Build both images
|
||||
docker compose build
|
||||
|
||||
# Start stack, wait for healthy
|
||||
docker compose up -d
|
||||
docker compose ps # both services "healthy"
|
||||
|
||||
# Engine responds independently
|
||||
curl -sf http://localhost:8000/engine/health # {"status":"ok"}
|
||||
curl -sf http://localhost:8000/engine/presets | head -c 200 # JSON presets
|
||||
|
||||
# App serves and proxies
|
||||
curl -sf http://localhost:3000/ | head -c 200 # HTML content
|
||||
curl -sf http://localhost:3000/engine/health # proxied to engine
|
||||
|
||||
# Teardown
|
||||
docker compose down
|
||||
```
|
||||
|
||||
Also verify: `docker compose up kerf-engine` starts the engine alone (R022).
|
||||
|
||||
## Constraints
|
||||
|
||||
- **No `kerf-server` service.** The GSD-INITIATE brief mentions it but no code exists. The app calls engine directly via `/engine/*`. Omit from this slice; add if/when a backend is built.
|
||||
- **Fonts are bundled, not volume-mounted.** The 3 .ttf files live in `app/public/fonts/` and get baked into the nginx image via Vite build. The GSD-INITIATE mentions volume-mounting fonts, but the current font system uses a hardcoded `BUNDLED_FONTS` array in `fontService.ts`, not a `fonts.json` manifest. Volume mounting can be added later.
|
||||
- **Root `package.json` has `workspaces: ["app"]`.** The `npm ci` in the Dockerfile must run inside `app/` (not root) to avoid workspace resolution issues, OR copy root `package.json` + `package-lock.json` and run from root. Simpler: just `WORKDIR /app/app` and install there.
|
||||
- **Engine CORS is currently `allow_origins=["*"]`.** Fine for Docker networking where nginx proxies. No CORS issues expected.
|
||||
- **Port mapping:** Engine internal 8000, App/nginx internal 80. Expose app on host port 3000 (or 80) via compose. Engine can optionally be exposed for standalone use.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **SPA routing in nginx** — Without `try_files $uri $uri/ /index.html`, direct navigation to app routes returns 404. The Vite app is a SPA with client-side routing.
|
||||
- **App Dockerfile npm ci location** — The root `package.json` declares workspaces. Running `npm ci` from root would try to install workspace deps. Simpler to copy just `app/package.json` + `app/package-lock.json` and install in isolation, ignoring the workspace root.
|
||||
- **App build needs `app/tsconfig*.json`** — The build script runs `tsc -b` before `vite build`, so all tsconfig files must be copied into the build stage.
|
||||
43
.gsd/milestones/M003/slices/S02/tasks/T01-PLAN.md
Normal file
43
.gsd/milestones/M003/slices/S02/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
estimated_steps: 12
|
||||
estimated_files: 3
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Create Dockerfile.app, nginx.conf, and docker-compose.yml for full-stack packaging
|
||||
|
||||
Create the three Docker infrastructure files that package the Kerf stack: a multi-stage Dockerfile for the app (node build → nginx), an nginx config that serves static files and proxies /engine/* to the engine container, and a docker-compose.yml that wires both services with healthchecks. Verify by building images and running the stack with curl checks.
|
||||
|
||||
**Key constraints from research:**
|
||||
- The app builds via `tsc -b && vite build` in `app/` — needs all tsconfig files (tsconfig.json, tsconfig.app.json, tsconfig.node.json)
|
||||
- Root `package.json` declares workspaces — run `npm ci` inside `app/` directory to avoid workspace resolution issues, OR copy root package files and install from root
|
||||
- nginx must handle SPA routing with `try_files $uri $uri/ /index.html`
|
||||
- nginx must proxy `/engine/` to `http://kerf-engine:8000` (Docker Compose service name)
|
||||
- Engine Dockerfile already exists at `docker/Dockerfile.engine` — no changes needed
|
||||
- Engine HEALTHCHECK uses `curl -sf http://localhost:8000/engine/health`
|
||||
- App healthcheck: `curl -sf http://localhost:80/` or use nginx built-in
|
||||
- Font MIME types (.ttf/.otf) should be handled by nginx (included by default in nginx:alpine)
|
||||
- App port: 80 internal (nginx default), mapped to 3000 on host
|
||||
- Engine port: 8000 internal, also exposed on host for standalone use
|
||||
|
||||
## Inputs
|
||||
|
||||
- `docker/Dockerfile.engine`
|
||||
- `app/package.json`
|
||||
- `app/tsconfig.json`
|
||||
- `app/tsconfig.app.json`
|
||||
- `app/tsconfig.node.json`
|
||||
- `app/vite.config.ts`
|
||||
- `engine/main.py`
|
||||
- `engine/api/routes.py`
|
||||
- `.dockerignore`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `docker/Dockerfile.app`
|
||||
- `docker/nginx.conf`
|
||||
- `docker-compose.yml`
|
||||
|
||||
## Verification
|
||||
|
||||
docker compose build && docker compose up -d && sleep 12 && docker compose ps && curl -sf http://localhost:8000/engine/health && curl -sf http://localhost:3000/ | head -c 100 && curl -sf http://localhost:3000/engine/health && docker compose down
|
||||
85
.gsd/milestones/M003/slices/S02/tasks/T01-SUMMARY.md
Normal file
85
.gsd/milestones/M003/slices/S02/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S02
|
||||
milestone: M003
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["docker/Dockerfile.app", "docker/nginx.conf", "docker-compose.yml"]
|
||||
key_decisions: ["Used npm workspace root install (npm ci --workspace=app) for proper lockfile resolution", "Used wget with 127.0.0.1 for nginx:alpine healthcheck (curl unavailable, localhost resolves to IPv6)", "Set proxy_read_timeout 120s and client_max_body_size 50m for large image processing through proxy"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Built both images with docker compose build (success). Started stack with docker compose up -d. Both containers reported (healthy) in docker compose ps. Verified: engine health direct (curl :8000/engine/health → ok), app serves HTML (curl :3000/ → doctype html), engine health proxied (curl :3000/engine/health → ok), engine presets proxied (curl :3000/engine/presets → preset JSON). Clean teardown with docker compose down."
|
||||
completed_at: 2026-03-26T06:43:57.639Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Create Dockerfile.app (node→nginx multi-stage), nginx.conf (SPA + /engine proxy), and docker-compose.yml with healthchecks — both services start healthy
|
||||
|
||||
> Create Dockerfile.app (node→nginx multi-stage), nginx.conf (SPA + /engine proxy), and docker-compose.yml with healthchecks — both services start healthy
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S02
|
||||
milestone: M003
|
||||
key_files:
|
||||
- docker/Dockerfile.app
|
||||
- docker/nginx.conf
|
||||
- docker-compose.yml
|
||||
key_decisions:
|
||||
- Used npm workspace root install (npm ci --workspace=app) for proper lockfile resolution
|
||||
- Used wget with 127.0.0.1 for nginx:alpine healthcheck (curl unavailable, localhost resolves to IPv6)
|
||||
- Set proxy_read_timeout 120s and client_max_body_size 50m for large image processing through proxy
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-26T06:43:57.650Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Create Dockerfile.app (node→nginx multi-stage), nginx.conf (SPA + /engine proxy), and docker-compose.yml with healthchecks — both services start healthy
|
||||
|
||||
**Create Dockerfile.app (node→nginx multi-stage), nginx.conf (SPA + /engine proxy), and docker-compose.yml with healthchecks — both services start healthy**
|
||||
|
||||
## What Happened
|
||||
|
||||
Created three Docker infrastructure files to package the full Kerf stack:\n\n1. **docker/Dockerfile.app** — Multi-stage build using node:22-alpine for the Vite/React build (npm workspace-aware install and build) and nginx:1.27-alpine for runtime. Copies built dist/ into nginx html root.\n\n2. **docker/nginx.conf** — Serves SPA with try_files fallback to index.html, proxies /engine/* to kerf-engine:8000 with 120s timeouts for large image processing, 50m client_max_body_size for multipart uploads, gzip compression, and static asset caching.\n\n3. **docker-compose.yml** — Two services: kerf-engine (port 8000) and kerf-app (port 3000→80). App depends on engine being healthy. Both have Docker healthchecks.\n\nFixed an IPv6 resolution issue where nginx:alpine healthcheck using localhost failed because it resolved to ::1 while nginx listens on 0.0.0.0 only. Used 127.0.0.1 explicitly.
|
||||
|
||||
## Verification
|
||||
|
||||
Built both images with docker compose build (success). Started stack with docker compose up -d. Both containers reported (healthy) in docker compose ps. Verified: engine health direct (curl :8000/engine/health → ok), app serves HTML (curl :3000/ → doctype html), engine health proxied (curl :3000/engine/health → ok), engine presets proxied (curl :3000/engine/presets → preset JSON). Clean teardown with docker compose down.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `docker compose build` | 0 | ✅ pass | 16300ms |
|
||||
| 2 | `docker compose up -d && sleep 20 && docker compose ps` | 0 | ✅ pass (both healthy) | 26000ms |
|
||||
| 3 | `curl -sf http://localhost:8000/engine/health` | 0 | ✅ pass | 200ms |
|
||||
| 4 | `curl -sf http://localhost:3000/ | head -c 100` | 0 | ✅ pass | 200ms |
|
||||
| 5 | `curl -sf http://localhost:3000/engine/health` | 0 | ✅ pass | 200ms |
|
||||
| 6 | `docker compose down` | 0 | ✅ pass | 3000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Changed healthcheck URL from localhost to 127.0.0.1 in Dockerfile.app and docker-compose.yml to fix IPv6 resolution failure in nginx:alpine containers.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `docker/Dockerfile.app`
|
||||
- `docker/nginx.conf`
|
||||
- `docker-compose.yml`
|
||||
|
||||
|
||||
## Deviations
|
||||
Changed healthcheck URL from localhost to 127.0.0.1 in Dockerfile.app and docker-compose.yml to fix IPv6 resolution failure in nginx:alpine containers.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
47
.gsd/milestones/M003/slices/S02/tasks/T02-PLAN.md
Normal file
47
.gsd/milestones/M003/slices/S02/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
estimated_steps: 16
|
||||
estimated_files: 1
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: Rewrite README.md with project overview, quick start, API reference, and usage docs
|
||||
|
||||
Replace the placeholder README with comprehensive documentation covering: what Kerf is, quick start with Docker Compose, engine API reference (all 4 endpoints with request/response examples), font system explanation, preset system, engine standalone usage, repository structure, and known limitations.
|
||||
|
||||
**Content outline:**
|
||||
1. Project title + one-paragraph description
|
||||
2. Quick Start — `docker compose up`, then visit localhost:3000
|
||||
3. Repository Structure — engine/, app/, docker/ directories
|
||||
4. Engine API Reference — document all 4 endpoints:
|
||||
- `GET /engine/health` — healthcheck
|
||||
- `GET /engine/presets` — list available presets
|
||||
- `POST /engine/trace` — raster-to-vector with preprocessing+vectorization+postprocessing
|
||||
- `POST /engine/simplify` — SVG simplification with optional DXF export (units, scale_factor)
|
||||
5. Font System — bundled fonts (Lato, OpenSans, Roboto), text-to-path conversion
|
||||
6. Presets — 5 built-in presets (sign, patch, stencil, detailed, custom), how they work
|
||||
7. Engine Standalone Usage — running engine independently, Docker image, API examples
|
||||
8. Development — local setup for engine (Python venv) and app (npm), dev proxy setup
|
||||
9. Known Limitations
|
||||
|
||||
**Sources for API details:** `engine/api/routes.py` for endpoint signatures, `engine/presets/*.json` for preset list, `app/src/utils/fontService.ts` for font system.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `engine/api/routes.py`
|
||||
- `engine/main.py`
|
||||
- `engine/presets/sign.json`
|
||||
- `engine/presets/patch.json`
|
||||
- `engine/presets/stencil.json`
|
||||
- `engine/presets/detailed.json`
|
||||
- `engine/presets/custom.json`
|
||||
- `app/package.json`
|
||||
- `docker/Dockerfile.engine`
|
||||
- `docker-compose.yml`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `README.md`
|
||||
|
||||
## Verification
|
||||
|
||||
grep -c '^## ' README.md | xargs test 6 -le && grep -q 'docker compose up' README.md && grep -q '/engine/trace' README.md && grep -q '/engine/simplify' README.md && grep -q '/engine/presets' README.md && grep -q '/engine/health' README.md && echo 'README OK'
|
||||
File diff suppressed because one or more lines are too long
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Kerf — full-stack Docker Compose
|
||||
# Runs the vectorization engine + web app behind nginx reverse-proxy.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose up -d # start all services
|
||||
# docker compose ps # check health
|
||||
# docker compose down # tear down
|
||||
|
||||
services:
|
||||
kerf-engine:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.engine
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8000/engine/health"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
start_period: 10s
|
||||
retries: 3
|
||||
|
||||
kerf-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.app
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
kerf-engine:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:80/"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
start_period: 5s
|
||||
retries: 3
|
||||
39
docker/Dockerfile.app
Normal file
39
docker/Dockerfile.app
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# ── Kerf App — multi-stage Docker build ──
|
||||
# Stage 1: Build the Vite/React app
|
||||
# Stage 2: Serve via nginx with reverse-proxy to engine
|
||||
|
||||
# ── Stage 1: Build ──
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy root workspace config first (npm ci needs it for workspace resolution)
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Copy the app workspace
|
||||
COPY app/ ./app/
|
||||
|
||||
# Install dependencies from the workspace root
|
||||
RUN npm ci --workspace=app
|
||||
|
||||
# Build the app (tsc -b && vite build)
|
||||
RUN npm run build --workspace=app
|
||||
|
||||
# ── Stage 2: Runtime — nginx serving static files ──
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
|
||||
# Remove default nginx site
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Copy built assets from builder
|
||||
COPY --from=builder /build/app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1:80/ || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
43
docker/nginx.conf
Normal file
43
docker/nginx.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Kerf App — nginx reverse-proxy + SPA static server
|
||||
# Serves the Vite-built app and proxies /engine/* to the kerf-engine container.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# ── Gzip ──
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 256;
|
||||
|
||||
# ── Proxy /engine/* to the engine container ──
|
||||
location /engine/ {
|
||||
proxy_pass http://kerf-engine:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Engine trace/simplify can take a while on large images
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
|
||||
# Allow large image uploads (engine accepts multipart)
|
||||
client_max_body_size 50m;
|
||||
}
|
||||
|
||||
# ── SPA fallback — serve index.html for client-side routes ──
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# ── Static asset caching ──
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|otf|eot)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
4271
package-lock.json
generated
Normal file
4271
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
6
package.json
Normal file
6
package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "kerf-engine-root",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"workspaces": ["app"]
|
||||
}
|
||||
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./app" }
|
||||
]
|
||||
}
|
||||
7
vitest.config.ts
Normal file
7
vitest.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
projects: ['app/vite.config.ts'],
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue