PrintFarmer uses a two-tier client-server architecture with real-time communication via SignalR.
┌────────────────────────────────────────┐
│ React TypeScript Frontend │
│ (Vite @ localhost:3000) │
│ - Dashboard UI │
│ - Printer management │
│ - Location organization │
│ - CSV import/export │
├────────────────────────────────────────┤
│ HTTP/REST + WebSocket (SignalR) │
├────────────────────────────────────────┤
│ ASP.NET Core 10 API Backend │
│ (Kestrel @ localhost:5245) │
│ - REST API endpoints │
│ - Backend Plugin Architecture: │
│ • Moonraker (Klipper) │
│ • PrusaLink (Prusa) │
│ • OctoPrint │
│ • SDCP (Generic printers) │
│ • FlashForge (FlashForge printers) │
│ • Core (base interfaces) │
│ - Background services │
│ - Job queue management │
│ - Profile/catalog management │
│ - Real-time hub (SignalR) │
├────────────────────────────────────────┤
│ Entity Framework Core ORM │
├────────────────────────────────────────┤
│ Multi-Database Support │
│ SQLite • PostgreSQL │
│ SQL Server │
└────────────────────────────────────────┘
Controllers (REST API entry points)
↓
Services (Business logic, orchestration)
↓
Repositories (Data access)
↓
Entity Framework Core (ORM)
↓
Database (SQLite/PostgreSQL/SQL Server)
Backend plugins are the authority for translating semantic printer actions into
their backend protocol. Shared services pass the configured Printer.BackendUrl
and credentials through capability interfaces; they must not select Moonraker
ports, build firmware commands, or infer supported controls from backend names.
The redundant PrinterBackendEndpointResolver is removed.
PrintersService coordinates lookup and application results. Direct printer
routes retain authorization and database physical-actuation fencing against
concurrent controls and print dispatch. Plugins provide command translation,
transport, supported-operation facts, and fresh backend safety evidence.
Manual motion uses one ordinary authenticated HTTP send and returns backend
acceptance, not proof of physical completion. There is no tracked-motion worker,
command channel, receipt, or recovery workflow; uncertain sends are never
automatically replayed. Attempt-bound print lifecycle safeguards remain separate.
A broad legacy interface is not evidence that every method is implemented.
API and frontend addresses remain separate. Moonraker camera discovery queries the backend API while the plugin resolves relative webcam URLs against the configured frontend port. Model-specific camera defaults and triggered snapshot selection also belong to the plugin; camera persistence remains shared.
This boundary applies to control execution and capability projection. Existing catalog/onboarding inference and legacy import/export normalization are separate responsibilities and are not redesigned by this control-path refactor.
REST API endpoints organized by feature:
PrintersController- Printer CRUD and managementLocationsController- Location organizationCatalogController- Printer models and manufacturersDiscoveryController- Network printer discoveryJobQueueController- Job queue managementTagsController- Job tagging and categorizationMaintenanceController- Maintenance trackingJobSchedulingController- Job scheduling and automationRetriesController- Job retry management
Business logic layer:
PrinterService- Printer management, state trackingLocationService- Location CRUD and organizationDiscoveryService- Moonraker/PrusaLink discoveryJobQueueService- Job management and monitoringBackendClientService- Abstracts different printer APIsCatalogService- Model and manufacturer management
External API integration (typed HTTP clients via Refit):
IMoonrakerClient- Moonraker API (Klipper firmware)IPrusaLinkClient- PrusaLink API (Prusa 3D printers)IOctoPrintClient- OctoPrint APIISdcpClient- SDCP protocol (generic printers)IFlashForgeClient- FlashForge printer APIISpoolmanClient- Spoolman integration (filament tracking)
Data access abstraction:
PrinterRepository- Printer persistenceLocationRepository- Location persistenceJobRepository- Job trackingCatalogRepository- Model/manufacturer data
Real-time communication:
PrinterHub- Live printer status updatesprinterUpdated- Status change eventprinterConnected- Printer comes onlineprinterDisconnected- Printer goes offlinejobProgressUpdated- Job progress eventsjobAutoDispatched- Auto-dispatch event (when job auto-assigned)dispatchSuggestion- Dispatch suggestion event (Suggest mode)dispatchFailed- Dispatch failure event
Printers
├── Id (GUID)
├── Name (string)
├── Url (string)
├── ApiKey (encrypted)
├── BackendType (enum: Moonraker, PrusaLink, SDCP)
├── LocationId (FK → Locations)
├── IsActive (soft delete)
├── CreatedAt, ModifiedAt
└── Location (navigation)
Locations
├── Id (GUID)
├── Name (string, unique)
├── Description (string, optional)
├── PrinterCount (int, denormalized)
├── IsActive (soft delete)
├── CreatedAt, ModifiedAt
└── Printers (navigation collection)
Jobs
├── Id (GUID)
├── PrinterId (FK)
├── Filename (string)
├── Status (enum: Queued, Printing, Completed, Error)
├── Progress (decimal)
├── StartedAt, CompletedAt
└── Printer (navigation)
Catalog (Manufacturers & Models)
├── Manufacturer
│ ├── Id
│ ├── Name (unique)
│ └── Models (navigation)
└── PrinterModel
├── Id
├── Name
├── ManufacturerId (FK)
└── Specifications (JSON)
ModelCollection
├── Id (GUID)
├── Name (string)
├── Description (string, optional)
├── OwnerUserId (GUID, cross-context reference — no FK)
├── IsShared (bool)
├── Revision (long, per-entity, app-incremented — sync base revision)
├── ConcurrencyToken (GUID, optimistic-concurrency ETag)
├── CreatedAt, UpdatedAt
└── Memberships (navigation collection)
ModelCollectionMembership
├── Id (GUID)
├── CollectionId (FK → ModelCollections, cascade)
├── ModelId (GUID, cross-context reference to Model3D — no FK)
├── Revision (long, per-entity)
├── CreatedAt, UpdatedAt
└── Collection (navigation)
LibrarySyncChange (append-only sync journal + tombstones)
├── Revision (long, store-generated identity PK — global monotonic cursor)
├── EntityType (string enum: ModelCollection | ModelCollectionMembership | Tag)
├── EntityId (GUID, soft reference — no FK, survives hard delete)
├── Operation (string enum: Create | Update | Delete)
├── OwnerUserId (GUID?, owner at time of change)
├── Visibility (string enum: Private | Shared)
├── ActorUserId (GUID, who made the change)
└── Timestamp (UTC)
Soft Deletes: All major entities have IsActive boolean flag for soft deletion
Timestamps: All entities track CreatedAt and ModifiedAt in UTC
Normalization: Printer counts denormalized on Location for performance
Encryption: API keys encrypted at rest in database
Cross-Context References: Some entities (e.g. ModelCollectionMembership.ModelId, OwnerUserId) reference records owned by another bounded context/DbContext. These are stored as plain GUIDs with no EF foreign key; existence is validated at the service layer through the model query abstraction, following the tag/context-boundary precedent.
The library sync layer records an append-only change journal that a desktop client can pull to mirror collection/membership state (epic #835; the pull/apply endpoints are described in the Library Sync Pull/Apply Endpoints section below).
LibrarySyncChangeis the journal. ItsRevisionis a store-generated identity primary key, which is provider-safe and strictly monotonic across PostgreSQL (bigintidentity), SQL Server (IDENTITY), and SQLite (AUTOINCREMENT). Ordering byRevisionis the pull cursor.- Transactional atomicity:
ILibrarySyncJournal.Record(...)enlists a journal row into the sameAppDbContextchange tracker used byEfModelCollectionRepository. The service's singleSaveChangesAsynccommits the entity mutation and its journal entry in one transaction, so state and journal cannot diverge. Every collection/membership mutation (create/update/delete/set-shared/add-member/remove-member/replace-members) journals an entry. - Durable tombstones: a
Deletejournal row has no foreign key to the entity it references, so it persists after the row is hard-deleted. Deleting a collection first emits a membershipDeletetombstone for each current membership and then a collectionDeletetombstone, letting a puller propagate removals. - Optimistic concurrency:
ModelCollectionandTagcarry aRevision(base revision) and aConcurrencyToken(IsConcurrencyToken, regenerated each write).TagService.UpdateTagAsyncguards on anExpectedRevisionand throwsTagConcurrencyException(HTTP 409) on a stale write. The token is a plainGuid(notrowversion/xmin) to stay portable across all providers, including SQLite. These columns are additive with safe defaults, so the change is backward-compatible.
LibrarySyncService (behind ILibrarySyncService) and LibrarySyncController expose the journal
to desktop clients as a cursor-based pull and a batched, transactional apply. Both reuse the
#843 collection repository and the #844 journal, so mutations and their journal rows still commit
under one unit of work (exactly-once journaling).
- Pull —
GET /api/library-sync/changes?cursor={opaque}&limit={n}:- Orders strictly by the monotonic
Revisionand pages deterministically. The service fetchespageSize + 1rows to computehasMorewithout a second query. limitis clamped to[1, MaxPageSize=500](DefaultPageSize=100). Thecursoris an opaque, validated token (SyncCursor) encoding only the last-seen revision; a malformed cursor yields HTTP 400 (InvalidSyncCursorException), never a leak.- Visibility is always enforced in-store via
GetVisibleChangesSinceAsync: a non-admin sees only rows they own,Sharedrows, or owner-less rows; an admin sees all. Because filtering runs in the query — not after paging — a forged cursor can never surface another user's changes. - Tombstone (
Delete) rows flow through so clients propagate removals. The response carrieschanges,nextCursor,hasMore, andserverRevision(all camelCase; string enums).
- Orders strictly by the monotonic
- Apply —
POST /api/library-sync/apply:- Accepts up to
MaxBatchSize=500operations. Each op is authorized with the same owner-or-administrator policy as the collection service (EnsureCanWrite→ HTTP 403 on denial). - Collection update/delete require a base revision and/or concurrency token; a stale or missing token is rejected (HTTP 409/400) rather than silently overwriting.
- The batch is processed sequentially without saving; conflicts are accumulated. If any
operation conflicts, the unit of work is abandoned (no
SaveChangesAsync), rolling the whole batch back, and the service throwsSyncConflictException→ HTTP 409 with a structured body ofconflicts(each carrying the safe server version and the submitted version) plus the currentserverRevision. ADbUpdateConcurrencyExceptionat save time is translated into the same 409 shape. - Auto-merge is limited to genuinely independent collection-membership changes: re-adding an
existing membership or removing an absent one is idempotent (
merged: true, no duplicate journal row). Idempotent no-ops (deleting an already-gone entity) likewise succeed without journaling. - Actor/audit metadata and exactly-once journal semantics are preserved; the endpoints honor
CancellationTokenand are backward-compatible (additive routes only).
- Accepts up to
External Printer API (Moonraker/PrusaLink)
↓
MoonrakerSubscriptionService (background service)
↓
PrinterHub.SendAsync("printerUpdated", status)
↓
React Client receives real-time update
↓
UI re-renders with new state
React UI: User assigns printer to location
↓
POST /api/printers/{id}/location { locationId }
↓
PrintersController.AssignLocation()
↓
PrinterService.AssignToLocation()
↓
LocationService.UpdatePrinterCount()
↓
Database updated
↓
SignalR broadcast: printerUpdated
↓
All connected clients update UI
The location system organizes printers into a tree structure using the Adjacency List + Cached Path approach, enabling hierarchical organization (Warehouse > Floor > Room > Rack) while maintaining query efficiency.
Locations Table
├── Id (GUID, Primary Key)
├── ParentId (FK → Locations, nullable) — Enables tree structure
├── Name (string)
├── Path (string, indexed) — Cached materialized path (e.g., "/Warehouse 1/Room A")
├── Depth (int, indexed) — Tree depth (0 = root)
├── SortOrder (int) — Display order among siblings
├── LocationTypeId (FK → LocationTypes) — Building, Floor, Room, Rack, etc.
├── PrinterCount (int) — Printers directly assigned to this location
├── TotalPrinterCount (int) — All printers in subtree (denormalized)
├── IsActive (bool) — Soft delete flag
├── CreatedAt, ModifiedAt (DateTime)
└── UNIQUE(ParentId, Name) — No duplicate names at same level
- Arbitrary Depth — No fixed level limit. Customers define their own hierarchy.
- User-Defined Types — LocationType entity allows custom organizational vocabulary (7 seeded types: Building, Floor, Room, Zone, Rack, Shelf, Workstation).
- Cached Path — Single
Pathcolumn (e.g., "/Warehouse 1/Room A/Rack 3") enables:- Fast breadcrumb generation without recursion
- Efficient descendant queries using
Path LIKE '/Warehouse%' - Automatic propagation when moving locations
- Printer Assignment — Printers can attach to ANY level (leaf or intermediate nodes).
- Denormalized Counts —
TotalPrinterCountavoids expensive recursive counting.
GetTree (nested hierarchy):
Query: SELECT * FROM Locations WHERE IsActive = true
Execution: Build tree in-memory using ParentId as join key
Result: Hierarchical LocationTreeDto with children
GetAncestors (breadcrumbs):
Query: SELECT * FROM Locations WHERE Path LIKE (target Path + '%')
Execution: Path provides direct ancestry chain
Result: Ordered list from root to target node
GetDescendants (subtree):
Query: SELECT * FROM Locations WHERE Path LIKE (parent Path + '%') AND IsActive = true
Execution: Indexed Path lookup eliminates recursion
Result: Flat list of all children/descendants
Move (reparent with circular reference detection):
Checks:
1. Validate new parent exists
2. Prevent circular reference (new parent cannot be descendant of node being moved)
3. Check max depth (prevent trees > 50 levels)
4. Verify no name duplication at destination level
Operations:
1. Update Location.ParentId
2. Recalculate Path and Depth
3. Cascade Path updates to all descendants
4. Update TotalPrinterCount for affected nodes
- LocationTreePicker — Tree selector for assignment UI
- LocationBreadcrumb — Navigation breadcrumbs using ancestors
- LocationManagement — CRUD and drag-drop reorganization
- LocationSelector — Dropdown for printer assignment
| Endpoint | Method | Purpose |
|---|---|---|
/api/locations |
GET | Flat list of all locations |
/api/locations/tree |
GET | Full hierarchy (nested) |
/api/locations/{id} |
GET | Single location details |
/api/locations/{id}/ancestors |
GET | Path to root (breadcrumbs) |
/api/locations/{id}/descendants |
GET | All children (subtree) |
/api/locations |
POST | Create new location |
/api/locations/{id} |
PUT | Update location |
/api/locations/{id}/move |
POST | Reparent location |
/api/locations/{id} |
DELETE | Soft delete location |
The auto-dispatch system scores all available printers against job requirements using a 9-factor multi-criteria algorithm, enabling intelligent printer selection with full transparency into scoring decisions.
9 Scoring Factors (4 hard filters + 5 soft scoring):
| # | Factor | Weight | Type | Description |
|---|---|---|---|---|
| 1 | Material Match | 100 | HARD | Job material must exist on printer |
| 2 | Nozzle Diameter | 100 | HARD | Must match ±0.01mm tolerance |
| 3 | Build Volume | 50 | SOFT | Printer must fit part dimensions |
| 4 | Enclosure | 80 | COND. | Bonus if available; required for some materials |
| 5 | Nozzle Hardness | 80 | COND. | Bonus for hardened nozzles with abrasive materials |
| 6 | Model Match | 60 | SOFT | Bonus for printer model matching job profile |
| 7 | Queue Depth | 30 | SOFT | Penalty: printers with many queued jobs score lower |
| 8 | Preferred | 40 | COND. | Bonus if user marked printer as preferred for material |
| 9 | Availability | 0 | HARD | Pre-filter: must be online, idle, and accepting jobs |
Hard Filters eliminate candidates immediately (score = 0, elimination reason logged).
Soft Factors contribute to final score (max 360 points possible).
Conditional Factors apply only in specific scenarios (e.g., enclosure bonus, preferred printer).
JobDispatchService (orchestration)
├── DispatchScorer (algorithm)
│ ├── EvaluateMaterialMatch()
│ ├── EvaluateNozzleDiameter()
│ ├── EvaluateBuildVolume()
│ ├── EvaluateEnclosure()
│ ├── EvaluateNozzleHardness()
│ ├── EvaluateModelMatch()
│ ├── EvaluateQueueDepth()
│ ├── EvaluatePreferred()
│ └── EvaluateAvailability()
├── DispatchLog (audit trail)
│ └── Records every dispatch decision + scores
└── DispatchSettings (configuration)
└── AutoDispatchEnabled, AutoDispatchMode, IdleThresholdSeconds, MinimumScoreThreshold
The AutoDispatchBackgroundService monitors idle printers and queued jobs using an event-driven model:
Architecture:
- Channel<Guid> for fire-and-forget idle notifications
- SemaphoreSlim(1, 1) to serialize dispatch decisions
- Per-printer CancellationTokenSource for cancellation
- DispatchSettings singleton for configuration
Trigger:
1. Printer goes idle → MoonrakerSubscriptionService publishes idle event
2. Event queued to Channel<Guid>
3. Background service dequeues → FindCandidatesAsync(jobId)
4. Top candidate scored
5. Based on DispatchSettings.AutoDispatchMode:
- "Suggest": SignalR notify operator (UI shows recommendation)
- "Auto": Directly dispatch to top candidate
Safety:
- Idle threshold (default 30s) prevents mid-print triggers
- Minimum score threshold prevents low-quality dispatch
- Max concurrent dispatches limits system load
Suggest Mode (Default):
- Operator receives notification: "Recommend Printer X for Job Y (score: 95/100)"
- Operator confirms or selects different printer
- Useful for safety-critical or high-value jobs
Auto Mode:
- System automatically dispatches to highest-scoring printer if score ≥ minimumScoreThreshold
- No operator intervention required
- Dispatcher monitors and can manually intervene if needed
| Event | Payload | Trigger |
|---|---|---|
jobAutoDispatched |
{ jobId, printerId, score, dispatchedBy, timestamp } |
Job auto-assigned (Auto mode) |
dispatchSuggestion |
{ jobId, topCandidates[], dispatchedBy, timestamp } |
Suggestion sent (Suggest mode) |
dispatchFailed |
{ jobId, reason, timestamp } |
Dispatch error or no eligible printers |
| Endpoint | Method | Purpose |
|---|---|---|
/api/job-queue/{id}/candidates |
GET | Score all printers for job |
/api/job-queue/{id}/dispatch-to |
POST | Assign job to specific printer |
/api/dispatch-settings |
GET | Read current settings |
/api/dispatch-settings |
PUT | Update settings |
All dispatch decisions recorded for audit, analysis, and machine learning:
DispatchLog Table
├── Id (GUID)
├── JobId (FK)
├── PrinterId (FK)
├── DispatchedBy (user email)
├── DispatchScore (decimal)
├── ScoreFactors (JSON: all 9 factors + individual scores)
├── DispatchMode ("Suggest" or "Auto")
├── IsSuccessful (bool)
├── ErrorReason (nullable)
├── CreatedAt (DateTime)
└── Candidates (nullable JSON: all scored printers)
src/Web/ReactApp/
├── src/
│ ├── components/ # React components
│ │ ├── Dashboard.tsx
│ │ ├── PrinterCard.tsx
│ │ ├── LocationManagement.tsx
│ │ ├── LocationSelector.tsx
│ │ ├── PrinterLocationDragDrop.tsx
│ │ └── ...
│ ├── pages/ # Page-level components
│ │ ├── PrintersPage.tsx
│ │ ├── AdminControlCenterPage.tsx # /admin hub of admin destination tiles
│ │ ├── SettingsShell.tsx # URL-driven admin/settings shell (/settings, /admin/settings; standalone operations)
│ │ └── ...
│ ├── contexts/ # React Context for state
│ │ ├── AuthContext.tsx
│ │ ├── PrinterContext.tsx
│ │ └── ...
│ ├── services/ # API communication
│ │ ├── apiClient.ts
│ │ ├── printerService.ts
│ │ ├── locationService.ts
│ │ ├── jobService.ts
│ │ ├── printerSignalR.ts
│ │ └── ...
│ ├── types/ # TypeScript interfaces
│ │ ├── api.ts
│ │ ├── models.ts
│ │ └── ...
│ ├── utils/ # Utility functions
│ └── App.tsx # Root component
├── public/ # Static assets
├── vite.config.ts # Vite configuration
└── tsconfig.json # TypeScript configuration
App
├── Router (React Router)
│ ├── Dashboard Page
│ │ ├── PrinterGrid
│ │ │ └── PrinterCard[] (real-time via Context)
│ │ ├── JobQueue
│ │ └── LocationFilter
│ ├── Admin Page
│ │ ├── LocationManagement (CRUD)
│ │ ├── PrinterLocationDragDrop (assignment)
│ │ ├── CatalogManagement
│ │ └── DiscoveryTool
│ └── Settings Page
├── AuthContext (user, token)
├── PrinterContext (live status from SignalR)
└── SignalR Connection (auto-reconnect)
React Query for server state:
- Cached API responses
- Automatic refetching
- Optimistic updates
React Context for app state:
- Authentication (user, token, roles)
- Real-time printer status
- UI state (modals, notifications)
SignalR Hubs for real-time updates:
- Printer status changes
- Job progress
- Discovery events
- Location changes
- Tailwind CSS - Utility-first CSS framework
- Color System - Consistent palette (primary, success, warning, error)
- Responsive Design - Mobile-first approach
- Dark Mode Support - Theme context for light/dark
-
Moonraker (Klipper-based)
- WebSocket for real-time updates
- REST API for commands
- Temperature, job status, system info
-
PrusaLink (Prusa printers)
- REST API only
- Polling for status (no WebSocket)
- Print job management
-
SDCP (Some printers)
- WebSocket + JSON request/response protocol (typically at
/websocket) - Polled for status (no real-time subscription)
- Connections are short-lived per operation; an idle keepalive/ping is not required in the current implementation
- File operations are not fully implemented yet (e.g., file listing may be empty)
- WebSocket + JSON request/response protocol (typically at
- OrcaSlicer profiles bundled with system
- Profile matching via printer model
- Automatic profile recommendation for job slicing
React App Startup
↓
Create SignalR Connection to /hubs/printers
↓
On Connected: Request initial printer state
↓
Listen for events:
- printerUpdated (periodic heartbeat or change)
- printerConnected (new printer online)
- printerDisconnected (printer offline)
- jobProgressUpdated (slicing/printing progress)
↓
Update Context state on event
↓
Components re-render (using Context)
All events use camelCase JSON for TypeScript compatibility:
{
"id": "printer-uuid",
"name": "Printer Name",
"isOnline": true,
"state": "Printing",
"hotendTemp": 210.5,
"hotendTarget": 210,
"bedTemp": 60,
"bedTarget": 60,
"progress": 45.5,
"eta": 1800
}- JWT token-based
- Secure HttpOnly cookies
- Token refresh mechanism
- Permission-based access control: roles are the assignment/grant vehicle,
but authorization checks (
[RequirePermission]) are enforced againstresource:actionpermission claims, not role names. See Permission Model.
- API keys encrypted at rest
- HTTPS in production
- CORS validation
- Input validation and sanitization
- Printer discovery limited to local network
- API key masking in logs
- Secrets not hardcoded
- Environment-based configuration
- Multi-provider support enables vendor choice
- Connection pooling optimized
- Printer count denormalization for large fleets
- SignalR automatic reconnection
- Heartbeat mechanism prevents client stale state
- Broadcast optimization for large printer counts
- Background job processing
- Async/await throughout
- Rate limiting on external APIs
- Circuit breaker pattern for resilience
- Dependency injection for testability
- Async/await for I/O operations
- Structured logging (Serilog)
- Unit and integration tests
- Feature-based architecture - Components organized by domain (gcode, printers, models3d, slicer, etc.)
- Component composition and reusability
- Custom hooks for logic sharing
- TypeScript for type safety
- React Testing Library for tests
- Error boundaries for resilience
src/Web/ReactApp/src/
├── features/ # Feature-based organization
│ ├── gcode/ # G-code library and management
│ │ ├── pages/ # GcodeLibrary, FilesPage
│ │ ├── components/ # FileBrowser, harvest components
│ │ └── hooks/ # Feature-specific hooks
│ ├── models3d/ # 3D model viewing
│ │ ├── pages/ # ModelsPage, Models3DViewerPage
│ │ ├── components/3d/ # ModelViewer3D, GCodeViewer3D
│ │ └── hooks/ # useStlUpload, etc.
│ ├── printers/ # Printer management
│ │ ├── pages/ # PrintersPage
│ │ ├── components/ # Printer cards, discovery, modals
│ │ └── hooks/ # Printer-specific hooks
│ ├── slicer/ # Slicing operations
│ │ ├── pages/ # NewSliceJobPage, SliceJobsPage
│ │ ├── components/ # Job components, profile selector
│ │ └── hooks/ # Slicer-specific hooks
│ ├── queue/ # Print job queue
│ │ ├── pages/ # QueuePage
│ │ └── components/ # Queue components
│ ├── catalog/ # Manufacturer and materials
│ │ ├── pages/ # CatalogPage, SpoolsPage
│ │ └── components/ # Filament, location, tag components
│ ├── auth/ # Authentication
│ │ ├── pages/ # LoginPage, ProfilePage
│ │ ├── components/ # Auth forms, setup wizard
│ │ └── hooks/ # useAuth
│ └── admin/ # Admin tools
│ ├── pages/ # AdminPage, LogsPage, SettingsPage
│ └── components/ # Admin components
├── common/ # Shared across features
│ ├── components/ # Reusable UI components
│ │ ├── ui/ # Button, Input, Select, etc.
│ │ ├── nav/ # Navigation components
│ │ ├── modals/ # Modal dialogs
│ │ ├── skeletons/ # Loading skeletons
│ │ └── icons/ # Icon components
│ ├── hooks/ # Shared hooks (useApi, useSignalR)
│ ├── contexts/ # React contexts (AuthContext)
│ └── utils/ # Utility functions
├── services/ # API clients and services
├── types/ # TypeScript type definitions
└── App.tsx # Main application entry
Design Principles:
- Features are self-contained with clear boundaries
- Common components are shared via
common/directory - Import paths use
@/features/*and@/common/*aliases - Each feature can have its own pages, components, and hooks
Docker Container (All services)
├── API (port 5245)
├── React (port 3000, served by Nginx)
├── Database Container (PostgreSQL/SQL Server)
└── Health checks
Docker Network
├── API Container (port 5245)
├── Frontend Container (Nginx, port 80)
├── Database Container (PostgreSQL/SQL Server)
├── OrcaSlicer Worker Container
└── Health check containers
See Deployment Guide for detailed deployment options.