Update 0.2.0

This commit is contained in:
2026-01-17 20:55:48 +00:00
parent 54bb58e5b0
commit eabc821e61
12 changed files with 684 additions and 634 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sir Blob
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

136
README.md
View File

@@ -4,21 +4,18 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
![Filaprint Dashboard](https://img.shields.io/badge/Filaprint-3D%20Print%20Manager-blue?style=for-the-badge)
## 🛠️ Technology Stack
## Technology Stack
- **Framework:** SvelteKit (Svelte 5)
- **Framework:** SvelteKit
- **Language:** TypeScript
- **Styling:** Tailwind CSS v4 (Cerberus Theme)
- **State Management:** Svelte 5 Runes
- **Build Tool:** Vite
- **Styling:** Tailwind CSS
- **3D Rendering:** Three.js (STL & OBJ loaders)
- **Data Visualization:** Chart.js
- **Icons:** Iconify (@iconify/svelte)
- **Database:** MongoDB with Mongoose
- **Authentication:** JWT with bcrypt password hashing
- **Database:** MongoDB
- **Container:** Docker with Docker Compose
## Features
## Features
### 1. Dashboard
@@ -91,58 +88,7 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
- **Admin Panel:** Manage users (Admin role only).
- **Role-Based Access:** Admin and User roles with appropriate permissions.
## 🗂️ Data Models (Mongoose Schemas)
### User Schema
- `_id`: ObjectId
- `username`: String (Required, Unique)
- `password`: String (Hashed with bcrypt)
- `role`: String (Enum: User, Admin)
- `location`: String
- `electricity_rate`: Number (Default: 0.12 $/kWh)
- `currency`: String (Default: USD)
- `createdAt`: Date
### Spool Schema
- `_id`: ObjectId
- `user_id`: ObjectId (Ref: User)
- `brand`: String (Required)
- `material`: String (Required, Enum: PLA, PETG, ABS, ASA, TPU, Other)
- `color_hex`: String (Default: #ffffff)
- `weight_initial_g`: Number (Required)
- `weight_remaining_g`: Number (Required)
- `price`: Number
- `purchased_at`: Date
- `is_active`: Boolean (Default: true)
### Printer Schema
- `_id`: ObjectId
- `user_id`: ObjectId (Ref: User)
- `name`: String (Required)
- `model`: String
- `nozzle_diameter_mm`: Number (Default: 0.4)
- `power_consumption_watts`: Number (Default: 0)
### PrintJob Schema
- `_id`: ObjectId
- `user_id`: ObjectId (Ref: User)
- `printer_id`: ObjectId (Ref: Printer)
- `spool_id`: ObjectId (Ref: Spool)
- `name`: String
- `duration_minutes`: Number
- `filament_used_g`: Number
- `calculated_cost_filament`: Number (Total cost including electricity)
- `calculated_cost_energy`: Number (Electricity cost only)
- `status`: String (Enum: Success, Fail, Cancelled, In Progress)
- `started_at`: Date (For In Progress jobs)
- `stl_file`: String (Path to uploaded 3D model)
- `date`: Date (Default: Date.now)
## 🚀 Getting Started
## Getting Started
### Prerequisites
@@ -150,30 +96,6 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
- MongoDB instance (local or Atlas)
- Docker (optional, for containerized deployment)
### Local Development
```bash
# Clone the repository
git clone https://github.com/yourusername/filaprint.git
cd filaprint
# Install dependencies
bun install
# Set up environment variables
cp .env.example .env
# Edit .env with your MongoDB URI and JWT secret
# Run development server
bun run dev
# Build for production
bun run build
# Start production server
bun run start
```
### Docker Deployment
```bash
@@ -208,40 +130,7 @@ MONGO_USER=admin
MONGO_PASSWORD=changeme
```
## 📁 Project Structure
```
filaprint/
├── src/
│ ├── lib/
│ │ ├── components/
│ │ │ ├── ui/ # Base UI components (Button, Card, Input, Modal)
│ │ │ ├── prints/ # Print-specific components (LogPrintModal, EditPrintModal)
│ │ │ ├── STLViewer.svelte # 3D model viewer (STL & OBJ)
│ │ │ └── Navbar.svelte
│ │ ├── models/ # Mongoose schemas
│ │ └── server/ # Server utilities (db connection, auth)
│ ├── routes/
│ │ ├── admin/users/ # Admin user management
│ │ ├── analytics/ # Analytics dashboard
│ │ ├── api/upload-stl/ # 3D model upload endpoint
│ │ ├── library/ # 3D model library
│ │ ├── login/ # Authentication
│ │ ├── printers/ # Printer management
│ │ ├── prints/ # Print job logging
│ │ ├── register/ # User registration
│ │ ├── settings/ # User settings
│ │ └── spools/ # Filament inventory
│ └── app.css # Global styles (Cerberus theme)
├── static/
│ └── uploads/models/ # Uploaded 3D model files
├── server/ # Production server
├── Dockerfile # Container build instructions
├── docker-compose.yml # Container orchestration
└── package.json
```
## ✅ Completed Features
## Completed Features
- [x] User authentication (Login/Register)
- [x] Dashboard with live stats and active print tracking
@@ -263,18 +152,13 @@ filaprint/
- [x] Responsive design
- [x] Docker containerization
## 🔮 Future Enhancements
## Future Features
- [ ] QR/Barcode scanning for quick spool lookup
- [ ] Photo uploads for print jobs
- [ ] Export data (CSV/PDF reports)
- [ ] Multi-language support
- [ ] Dark/Light theme toggle
- [ ] Email notifications
- [ ] Print job templates
- [ ] 3D printer integration (OctoPrint, Klipper)
- [ ] Some notifications
- [ ] Thumbnail generation for 3D models
## 📄 License
## License
MIT License - See LICENSE file for details.

View File

@@ -5,10 +5,12 @@
"": {
"name": "filaprint",
"dependencies": {
"@iconify/svelte": "^5.1.0",
"@sveltejs/adapter-node": "^5.4.0",
"@iconify/svelte": "^5.2.1",
"@sveltejs/adapter-node": "^5.5.1",
"@threlte/core": "^8.3.1",
"@threlte/extras": "^9.7.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/three": "^0.182.0",
"bcryptjs": "^3.0.3",
"chart.js": "^4.5.1",
@@ -17,24 +19,24 @@
"dotenv": "^17.2.3",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.0.2",
"mongoose": "^9.1.4",
"three": "^0.182.0",
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/forms": "^0.5.10",
"@sveltejs/kit": "^2.50.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@tailwindcss/vite": "^4.1.18",
"@types/bcryptjs": "^3.0.0",
"@types/cookie": "^1.0.0",
"@types/jsonwebtoken": "^9.0.10",
"svelte": "^5.45.6",
"svelte-check": "^4.3.4",
"tailwindcss": "^4.1.17",
"svelte": "^5.46.4",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vite": "^7.3.1",
},
},
},
@@ -93,7 +95,7 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
"@iconify/svelte": ["@iconify/svelte@5.1.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">4.0.0" } }, "sha512-I14nSqo0pNXO5OKsT61ZO3XIPF4yRHA2ErgPsaZ1sPJdKXn80o7o8jOe1xpWphbb9FihdX6by9zlKKBss61mFw=="],
"@iconify/svelte": ["@iconify/svelte@5.2.1", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "svelte": ">5.0.0" } }, "sha512-zHmsIPmnIhGd5gc95bNN5FL+GifwMZv7M2rlZEpa7IXYGFJm/XGHdWf6PWQa6OBoC+R69WyiPO9NAj5wjfjbow=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
@@ -171,11 +173,11 @@
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="],
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.4.0", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ=="],
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.1", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-VpZdPNRPQuZRtgfAMETPWWKpZx9JwXmUUsgz/+eSpw/Oh7+2O1uZHlsQTuyfxydJHPrRzjfu/ItcJjY4oscCiQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.49.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.50.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Hj8sR8O27p2zshFEIJzsvfhLzxga/hWw6tRLnBjMYw70m1aS9BSYCqAUtzDBjRREtX1EvLMYgaC0mYE3Hz4KWA=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
@@ -223,18 +225,38 @@
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/cookie": ["@types/cookie@1.0.0", "", { "dependencies": { "cookie": "*" } }, "sha512-mGFXbkDQJ6kAXByHS7QAggRXgols0mAdP4MuXgloGY1tXokvzaFFM4SMqWvf7AH0oafI7zlFJwoGWzmhDqTZ9w=="],
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
"@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
"@types/three": ["@types/three@0.182.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q=="],
@@ -301,7 +323,7 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
"diet-sprite": ["diet-sprite@0.0.1", "", {}, "sha512-zSHI2WDAn1wJqJYxcmjWfJv3Iw8oL9reQIbEyx2x2/EZ4/qmUTIo8/5qOCurnAcq61EwtJJaZ0XTy2NRYqpB5A=="],
@@ -459,7 +481,7 @@
"mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.0", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og=="],
"mongoose": ["mongoose@9.0.2", "", { "dependencies": { "kareem": "3.0.0", "mongodb": "~7.0", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-+GCaqwE+X//yN9eo2M2L/n+mVti9J6vH5iQKbhD+2AArZd5iaZqK/DkmkE4S6/iYYMyVQPTXsRk7jyVOYEtJzA=="],
"mongoose": ["mongoose@9.1.4", "", { "dependencies": { "kareem": "3.0.0", "mongodb": "~7.0", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-V8JIyoWKWW+R2COlOsh6gaYw9TvczSiP/cN3Yuk1pv7ws5VNFAy5GPrK8jfz9tVYovmqdWOJRurMjL4ilYn9wA=="],
"mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="],
@@ -479,6 +501,8 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -553,7 +577,7 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="],
"svelte": ["svelte@5.46.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VJwdXrmv9L8L7ZasJeWcCjoIuMRVbhuxbss0fpVnR8yorMmjNDwcjIH08vS6wmSzzzgAG5CADQ1JuXPS2nwt9w=="],
"svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="],
@@ -599,7 +623,7 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],

View File

@@ -1,34 +1,36 @@
{
"name": "filaprint",
"private": true,
"version": "0.0.1",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite dev --host",
"build": "vite build",
"start": "node server/server.js"
"start": "bun server/server.ts"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/forms": "^0.5.10",
"@sveltejs/kit": "^2.50.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17",
"@tailwindcss/vite": "^4.1.18",
"@types/bcryptjs": "^3.0.0",
"@types/cookie": "^1.0.0",
"@types/jsonwebtoken": "^9.0.10",
"svelte": "^5.45.6",
"svelte-check": "^4.3.4",
"tailwindcss": "^4.1.17",
"svelte": "^5.46.4",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.2.6"
"vite": "^7.3.1"
},
"dependencies": {
"@iconify/svelte": "^5.1.0",
"@sveltejs/adapter-node": "^5.4.0",
"@iconify/svelte": "^5.2.1",
"@sveltejs/adapter-node": "^5.5.1",
"@threlte/core": "^8.3.1",
"@threlte/extras": "^9.7.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/three": "^0.182.0",
"bcryptjs": "^3.0.3",
"chart.js": "^4.5.1",
@@ -37,7 +39,7 @@
"dotenv": "^17.2.3",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"mongoose": "^9.0.2",
"mongoose": "^9.1.4",
"three": "^0.182.0"
}
}

View File

@@ -3,9 +3,9 @@ import dotenv from 'dotenv';
dotenv.config();
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/filaprint';
const MONGODB_URI: string = process.env.MONGODB_URI || 'mongodb://localhost:27017/filaprint';
export const connectDB = async () => {
export const connectDB = async (): Promise<void> => {
try {
await mongoose.connect(MONGODB_URI);
console.log('MongoDB connected successfully');

View File

@@ -1,14 +1,15 @@
// @ts-ignore
import { handler } from '../build/handler.js';
import dotenv from 'dotenv';
import http from 'http';
import express from 'express';
import cors from 'cors';
import { connectDB } from './db.js';
import { connectDB } from './db';
dotenv.config();
const app = express();
const server = http.Server(app);
const server = http.createServer(app);
app.use(cors());

View File

@@ -123,6 +123,23 @@
// Track if user wants to remove the model
let removeModel = $state(false);
// Elapsed time state for In Progress prints
let elapsedHours = $state(0);
let elapsedMins = $state(0);
$effect(() => {
if (print && print.status === "In Progress" && print.started_at) {
const start = new Date(print.started_at).getTime();
const now = Date.now();
const diffMins = Math.max(0, Math.floor((now - start) / 60000));
elapsedHours = Math.floor(diffMins / 60);
elapsedMins = diffMins % 60;
} else {
elapsedHours = 0;
elapsedMins = 0;
}
});
</script>
<Modal title="Edit Print Log" {open} onclose={handleClose}>
@@ -495,7 +512,7 @@
name="elapsed_hours"
placeholder="0"
min="0"
value="0"
bind:value={elapsedHours}
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-8"
/>
<span
@@ -510,7 +527,7 @@
placeholder="0"
min="0"
max="59"
value="0"
bind:value={elapsedMins}
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none pr-10"
/>
<span

View File

@@ -10,9 +10,10 @@
printers: any[];
spools: any[];
onclose: () => void;
action?: string;
}
let { open, printers, spools, onclose }: Props = $props();
let { open, printers, spools, onclose, action = "?/log" }: Props = $props();
let isSubmitting = $state(false);
let selectedStatus = $state("Success");
let stlFile = $state<File | null>(null);
@@ -85,7 +86,7 @@
<Modal title="Log a Print" {open} onclose={handleClose}>
<form
method="POST"
action="?/log"
{action}
use:enhance={async ({ formData }) => {
isSubmitting = true;
@@ -296,8 +297,11 @@
name="printer_id"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none"
>
<option value="" disabled selected>Select a printer</option>
{#each printers as p}
<option value={p._id}>{p.name}</option>
{:else}
<option value="" disabled>No printers found</option>
{/each}
</select>
</div>
@@ -311,11 +315,14 @@
name="spool_id"
class="w-full rounded-lg bg-slate-800/50 border border-slate-700 px-4 py-2.5 text-sm text-slate-100 focus:border-blue-500 focus:outline-none"
>
<option value="" disabled selected>Select a spool</option>
{#each spools as s}
<option value={s._id}
>{s.brand}
{s.material} ({s.weight_remaining_g}g left)</option
>
{:else}
<option value="" disabled>No spools found</option>
{/each}
</select>
</div>

View File

@@ -25,13 +25,13 @@
<!-- Dialog Panel -->
<div
class="relative z-10 w-full max-w-lg rounded-xl border border-[#3f3f46] shadow-2xl"
class="relative z-10 w-full max-w-lg rounded-xl border border-[#3f3f46] shadow-2xl flex flex-col max-h-[85vh] overflow-hidden"
style="background-color: #18181b;"
transition:scale={{ duration: 150, start: 0.95 }}
>
<!-- Header -->
<div
class="flex items-center justify-between border-b border-[#3f3f46] px-6 py-4"
class="flex items-center justify-between border-b border-[#3f3f46] px-6 py-4 shrink-0"
style="background-color: #27272a;"
>
<h3 class="text-lg font-semibold text-white">{title}</h3>
@@ -46,7 +46,7 @@
</div>
<!-- Content -->
<div class="p-6">
<div class="p-6 overflow-y-auto">
{@render children()}
</div>
</div>

View File

@@ -13,12 +13,13 @@ export const load: PageServerLoad = async ({ locals }) => {
const userId = locals.user.id;
// Run in parallel - all filtered by user
const [spoolCount, spools, printerCount, recentPrints, activePrinter, activePrintJob, completedPrints] = await Promise.all([
const [spoolCount, spools, printerCount, recentPrints, activePrinter, printers, activePrintJob, completedPrints] = await Promise.all([
Spool.countDocuments({ user_id: userId, is_active: true }),
Spool.find({ user_id: userId, is_active: true }).lean(),
Printer.countDocuments({ user_id: userId }),
PrintJob.find({ user_id: userId }).sort({ date: -1 }).limit(5).populate('printer_id', 'name').populate('spool_id', 'brand color_hex').lean(),
PrintJob.find({ user_id: userId }).sort({ date: -1, _id: -1 }).limit(5).populate('printer_id', 'name').populate('spool_id', 'brand color_hex').lean(),
Printer.findOne({ user_id: userId }).lean(),
Printer.find({ user_id: userId }).lean(), // Fetch all printers for dropdowns
PrintJob.findOne({ user_id: userId, status: 'In Progress' }).populate('printer_id', 'name').populate('spool_id', 'brand color_hex material').lean(),
PrintJob.find({ user_id: userId, status: { $ne: 'In Progress' } }).select('calculated_cost_filament').lean()
]);
@@ -60,6 +61,8 @@ export const load: PageServerLoad = async ({ locals }) => {
},
recentPrints: JSON.parse(JSON.stringify(recentPrints)),
activePrinter: activePrinter ? JSON.parse(JSON.stringify(activePrinter)) : null,
activePrintJob: activePrintJob ? JSON.parse(JSON.stringify(activePrintJob)) : null
activePrintJob: activePrintJob ? JSON.parse(JSON.stringify(activePrintJob)) : null,
spools: JSON.parse(JSON.stringify(spools)),
printers: JSON.parse(JSON.stringify(printers))
};
};

View File

@@ -5,6 +5,8 @@
import { onMount, onDestroy } from "svelte";
import { browser } from "$app/environment";
import LogPrintModal from "$lib/components/prints/LogPrintModal.svelte";
let { data } = $props();
// svelte-ignore non_reactive_update
let stats = $derived(data.stats);
@@ -14,12 +16,19 @@
let activePrinter = $derived(data.activePrinter);
// svelte-ignore non_reactive_update
let activePrintJob = $derived(data.activePrintJob);
// svelte-ignore non_reactive_update
let spools = $derived(data.spools || []);
// svelte-ignore non_reactive_update
let printers = $derived(data.printers || []);
let showQuickLogModal = $state(false);
// Timer state
let currentTime = $state(Date.now());
let timerInterval: ReturnType<typeof setInterval>;
let notificationSent = $state(false);
// ... (rest of onMount/onDestroy/notifications - keeping as is)
onMount(() => {
// Request notification permission
if (
@@ -38,7 +47,7 @@
if (activePrintJob && !notificationSent) {
const progress = getProgress(
activePrintJob.started_at,
activePrintJob.duration_minutes
activePrintJob.duration_minutes,
);
if (progress >= 100) {
sendNotification();
@@ -68,7 +77,7 @@
// Calculate time remaining for active print
function getTimeRemaining(
startedAt: string,
durationMinutes: number
durationMinutes: number,
): string {
if (!startedAt) return "--:--";
const start = new Date(startedAt).getTime();
@@ -90,13 +99,23 @@
<div class="space-y-8 fade-in">
<!-- Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div
class="flex flex-col md:flex-row md:items-center justify-between gap-4"
>
<div>
<h1 class="text-3xl font-bold tracking-tight text-white/90">Dashboard</h1>
<p class="text-slate-400 mt-1">Welcome back to your printing hub.</p>
<h1 class="text-3xl font-bold tracking-tight text-white/90">
Dashboard
</h1>
<p class="text-slate-400 mt-1">
Welcome back to your printing hub.
</p>
</div>
<div class="flex gap-3">
<Button variant="secondary" size="sm">Quick Log</Button>
<Button
variant="secondary"
size="sm"
onclick={() => (showQuickLogModal = true)}>Quick Log</Button
>
<a href="/prints">
<Button variant="primary" size="sm" class="shadow-blue-500/20">
<span class="mr-2 text-lg leading-none">+</span> New Print
@@ -112,11 +131,15 @@
class="absolute inset-0 bg-linear-to-br from-blue-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
></div>
<div class="relative z-10">
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
<p
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
>
Active Spools
</p>
<div class="flex items-baseline mt-2">
<span class="text-3xl font-bold text-white">{stats.spoolCount}</span>
<span class="text-3xl font-bold text-white"
>{stats.spoolCount}</span
>
</div>
</div>
</Card>
@@ -126,20 +149,24 @@
class="absolute inset-0 bg-linear-to-br from-violet-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
></div>
<div class="relative z-10">
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
<p
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
>
Filament On Hand
</p>
<div class="flex items-baseline mt-2">
{#if stats.totalWeightG >= 1000}
<span class="text-3xl font-bold text-white"
>{stats.totalWeightKg}<span
class="text-sm font-normal text-slate-500 ml-1">kg</span
class="text-sm font-normal text-slate-500 ml-1"
>kg</span
></span
>
{:else}
<span class="text-3xl font-bold text-white"
>{stats.totalWeightG}<span
class="text-sm font-normal text-slate-500 ml-1">g</span
class="text-sm font-normal text-slate-500 ml-1"
>g</span
></span
>
{/if}
@@ -152,11 +179,14 @@
class="absolute inset-0 bg-linear-to-br from-emerald-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
></div>
<div class="relative z-10">
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
<p
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
>
Printers
</p>
<div class="flex items-baseline mt-2">
<span class="text-3xl font-bold text-white">{stats.printerCount}</span
<span class="text-3xl font-bold text-white"
>{stats.printerCount}</span
>
</div>
</div>
@@ -167,7 +197,9 @@
class="absolute inset-0 bg-linear-to-br from-amber-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
></div>
<div class="relative z-10">
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
<p
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
>
Est. Value
</p>
<div class="flex items-baseline mt-2">
@@ -183,11 +215,15 @@
class="absolute inset-0 bg-linear-to-br from-rose-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
></div>
<div class="relative z-10">
<p class="text-xs font-medium text-slate-400 uppercase tracking-wider">
<p
class="text-xs font-medium text-slate-400 uppercase tracking-wider"
>
Total Spent
</p>
<div class="flex items-baseline mt-2">
<span class="text-3xl font-bold text-white">${stats.totalSpent}</span>
<span class="text-3xl font-bold text-white"
>${stats.totalSpent}</span
>
</div>
</div>
</Card>
@@ -198,7 +234,9 @@
<!-- Recent Activity -->
<div class="lg:col-span-2 space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-white/90">Recent Activity</h2>
<h2 class="text-xl font-semibold text-white/90">
Recent Activity
</h2>
<a
href="/prints"
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
@@ -214,7 +252,10 @@
<div
class="w-12 h-12 rounded-full bg-slate-800 flex items-center justify-center mx-auto mb-3 text-slate-600"
>
<Icon icon="mdi:printer-3d-nozzle" class="w-6 h-6" />
<Icon
icon="mdi:printer-3d-nozzle"
class="w-6 h-6"
/>
</div>
<p class="text-slate-500">No recent prints found</p>
<a href="/prints">
@@ -248,17 +289,31 @@
class="w-5 h-5 text-green-400"
/>
{:else if print.status === "Fail"}
<Icon icon="mdi:close-circle" class="w-5 h-5" />
<Icon
icon="mdi:close-circle"
class="w-5 h-5"
/>
{:else if print.status === "In Progress"}
<Icon icon="mdi:loading" class="w-5 h-5 animate-spin" />
<Icon
icon="mdi:loading"
class="w-5 h-5 animate-spin"
/>
{:else}
<Icon icon="mdi:printer-3d" class="w-5 h-5" />
<Icon
icon="mdi:printer-3d"
class="w-5 h-5"
/>
{/if}
</div>
<div>
<h4 class="text-sm font-semibold text-white">{print.name}</h4>
<h4
class="text-sm font-semibold text-white"
>
{print.name}
</h4>
<p class="text-xs text-slate-400 mt-0.5">
{print.printer_id?.name || "Unknown Printer"}{print.filament_used_g}g
{print.printer_id?.name ||
"Unknown Printer"}{print.filament_used_g}g
</p>
</div>
</div>
@@ -274,7 +329,12 @@
{print.status}
</span>
<p class="text-xs text-slate-500 mt-1">
{new Date(print.date).toLocaleDateString()}
{new Date(print.date).toLocaleDateString(
"en-US",
{
timeZone: "UTC",
},
)}
</p>
</div>
</Card>
@@ -291,7 +351,8 @@
<!-- Active Print Job -->
<div class="flex items-center justify-between mb-4">
<span class="font-medium text-white"
>{activePrintJob.printer_id?.name || "Unknown Printer"}</span
>{activePrintJob.printer_id?.name ||
"Unknown Printer"}</span
>
<span
class="px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-wide bg-blue-500/10 text-blue-400 border border-blue-500/20 animate-pulse"
@@ -302,7 +363,9 @@
<div class="space-y-4">
<!-- Job Name -->
<div>
<p class="text-xs text-slate-400 mb-1">Currently Printing</p>
<p class="text-xs text-slate-400 mb-1">
Currently Printing
</p>
<p class="text-sm font-semibold text-white">
{activePrintJob.name}
</p>
@@ -318,17 +381,19 @@
>{Math.round(
getProgress(
activePrintJob.started_at,
activePrintJob.duration_minutes
)
activePrintJob.duration_minutes,
),
)}%</span
>
</div>
<div class="h-2 bg-surface-800 rounded-full overflow-hidden">
<div
class="h-2 bg-surface-800 rounded-full overflow-hidden"
>
<div
class="h-full bg-blue-500 transition-all duration-1000"
style="width: {getProgress(
activePrintJob.started_at,
activePrintJob.duration_minutes
activePrintJob.duration_minutes,
)}%"
></div>
</div>
@@ -337,11 +402,13 @@
<!-- Time & Material Info -->
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="text-xs text-slate-400">Time Remaining</p>
<p class="text-xs text-slate-400">
Time Remaining
</p>
<p class="font-medium text-white">
{getTimeRemaining(
activePrintJob.started_at,
activePrintJob.duration_minutes
activePrintJob.duration_minutes,
)}
</p>
</div>
@@ -358,11 +425,13 @@
<div class="flex items-center gap-2 text-sm">
<div
class="w-3 h-3 rounded-full"
style="background-color: {activePrintJob.spool_id.color_hex}"
style="background-color: {activePrintJob
.spool_id.color_hex}"
></div>
<span class="text-slate-300"
>{activePrintJob.spool_id.brand}
{activePrintJob.spool_id.material || ""}</span
{activePrintJob.spool_id.material ||
""}</span
>
</div>
{/if}
@@ -370,38 +439,52 @@
<div class="mt-6 pt-4 border-t border-surface-700/50">
<a href="/prints" class="block">
<Button variant="secondary" size="sm" class="w-full text-xs"
>View All Prints</Button
<Button
variant="secondary"
size="sm"
class="w-full text-xs">View All Prints</Button
>
</a>
</div>
{:else if activePrinter}
<!-- Idle State -->
<div class="flex items-center justify-between mb-4">
<span class="font-medium text-white">{activePrinter.name}</span>
<span class="font-medium text-white"
>{activePrinter.name}</span
>
<span
class="px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-wide bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
>Idle</span
>
</div>
<div class="text-center py-4">
<p class="text-sm text-slate-400 mb-4">No active print job</p>
<p class="text-sm text-slate-400 mb-4">
No active print job
</p>
<a href="/prints">
<Button variant="primary" size="sm">Start a Print</Button>
<Button variant="primary" size="sm"
>Start a Print</Button
>
</a>
</div>
<div class="mt-4 pt-4 border-t border-surface-700/50">
<a href="/printers" class="block">
<Button variant="secondary" size="sm" class="w-full text-xs"
>Manage Printer</Button
<Button
variant="secondary"
size="sm"
class="w-full text-xs">Manage Printer</Button
>
</a>
</div>
{:else}
<div class="text-center py-6">
<p class="text-sm text-text-muted mb-4">No printers configured</p>
<p class="text-sm text-text-muted mb-4">
No printers configured
</p>
<a href="/printers">
<Button variant="primary" size="sm">Add Printer</Button>
<Button variant="primary" size="sm"
>Add Printer</Button
>
</a>
</div>
{/if}
@@ -410,6 +493,14 @@
</div>
</div>
<LogPrintModal
open={showQuickLogModal}
{printers}
{spools}
onclose={() => (showQuickLogModal = false)}
action="/prints?/log"
/>
<style>
/* Simple entry animation */
.fade-in {

View File

@@ -18,7 +18,7 @@ export const load: PageServerLoad = async ({ locals }) => {
const prints = await PrintJob.find({ user_id: locals.user.id })
.populate('spool_id', 'brand material color_hex')
.populate('printer_id', 'name model')
.sort({ date: -1 })
.sort({ date: -1, _id: -1 })
.lean();
// Fetch active spools and printers for the "Log Print" form - filtered by user