diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2cc1757 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 7b37a8d..b87c71e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bun.lock b/bun.lock index 96dbd55..2289163 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 7573e78..ac38fc9 100644 --- a/package.json +++ b/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/server/db.js b/server/db.ts similarity index 67% rename from server/db.js rename to server/db.ts index e962f93..7fedfcb 100644 --- a/server/db.js +++ b/server/db.ts @@ -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 => { try { await mongoose.connect(MONGODB_URI); console.log('MongoDB connected successfully'); diff --git a/server/server.js b/server/server.ts similarity index 82% rename from server/server.js rename to server/server.ts index 7f56981..7f6bcca 100644 --- a/server/server.js +++ b/server/server.ts @@ -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()); diff --git a/src/lib/components/prints/EditPrintModal.svelte b/src/lib/components/prints/EditPrintModal.svelte index e52b0dc..97551b3 100644 --- a/src/lib/components/prints/EditPrintModal.svelte +++ b/src/lib/components/prints/EditPrintModal.svelte @@ -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; + } + }); @@ -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" /> 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(null); @@ -85,7 +86,7 @@
{ 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" > + {#each printers as p} + {:else} + {/each} @@ -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" > + {#each spools as s} + {:else} + {/each} diff --git a/src/lib/components/ui/Modal.svelte b/src/lib/components/ui/Modal.svelte index fa70f6c..5d6f083 100644 --- a/src/lib/components/ui/Modal.svelte +++ b/src/lib/components/ui/Modal.svelte @@ -1,54 +1,54 @@ {#if open} -
- - - -
+
+ + + +
- -
- -
-

{title}

- -
+ +
+ +
+

{title}

+ +
- -
- {@render children()} -
-
-
+ +
+ {@render children()} +
+
+
{/if} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index b2d9cf4..7495450 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -7,18 +7,19 @@ import { redirect } from '@sveltejs/kit'; export const load: PageServerLoad = async ({ locals }) => { if (!locals.user) throw redirect(303, '/login'); - + await connectDB(); - + 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() ]); @@ -29,7 +30,7 @@ export const load: PageServerLoad = async ({ locals }) => { spools.forEach(spool => { totalWeightG += (spool.weight_remaining_g || 0); - + // Value = (Remaining / Initial) * Price if (spool.weight_initial_g > 0 && spool.price > 0) { const ratio = (spool.weight_remaining_g || 0) / spool.weight_initial_g; @@ -44,8 +45,8 @@ export const load: PageServerLoad = async ({ locals }) => { }); // Keep grams for precision, format for display - const totalWeightKg = totalWeightG >= 1000 - ? (totalWeightG / 1000).toFixed(2) + const totalWeightKg = totalWeightG >= 1000 + ? (totalWeightG / 1000).toFixed(2) : (totalWeightG / 1000).toFixed(3); const estimatedValue = totalValue.toFixed(2); @@ -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)) }; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8c1431b..4203b50 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,426 +1,517 @@
- -
-
-

Dashboard

-

Welcome back to your printing hub.

-
-
- - - - -
-
+ +
+
+

+ Dashboard +

+

+ Welcome back to your printing hub. +

+
+
+ + + + +
+
- -
- -
-
-

- Active Spools -

-
- {stats.spoolCount} -
-
-
+ +
+ +
+
+

+ Active Spools +

+
+ {stats.spoolCount} +
+
+
- -
-
-

- Filament On Hand -

-
- {#if stats.totalWeightG >= 1000} - {stats.totalWeightKg}kg - {:else} - {stats.totalWeightG}g - {/if} -
-
-
+ +
+
+

+ Filament On Hand +

+
+ {#if stats.totalWeightG >= 1000} + {stats.totalWeightKg}kg + {:else} + {stats.totalWeightG}g + {/if} +
+
+
- -
-
-

- Printers -

-
- {stats.printerCount} -
-
-
+ +
+
+

+ Printers +

+
+ {stats.printerCount} +
+
+
- -
-
-

- Est. Value -

-
- ${stats.estimatedValue} -
-
-
+ +
+
+

+ Est. Value +

+
+ ${stats.estimatedValue} +
+
+
- -
-
-

- Total Spent -

-
- ${stats.totalSpent} -
-
-
-
+ +
+
+

+ Total Spent +

+
+ ${stats.totalSpent} +
+
+
+
- -
- -
-
-

Recent Activity

- View All -
+ +
+ +
+
+

+ Recent Activity +

+ View All +
- {#if recentPrints.length === 0} - -
-
- -
-

No recent prints found

- - - -
-
- {:else} -
- {#each recentPrints as print} - -
-
+
+
+ +
+

No recent prints found

+ + + +
+ + {:else} +
+ {#each recentPrints as print} + +
+
- {#if print.status === "Success"} - - {:else if print.status === "Fail"} - - {:else if print.status === "In Progress"} - - {:else} - - {/if} -
-
-

{print.name}

-

- {print.printer_id?.name || "Unknown Printer"} • {print.filament_used_g}g -

-
-
-
- + {#if print.status === "Success"} + + {:else if print.status === "Fail"} + + {:else if print.status === "In Progress"} + + {:else} + + {/if} +
+
+

+ {print.name} +

+

+ {print.printer_id?.name || + "Unknown Printer"} • {print.filament_used_g}g +

+
+
+
+ - {print.status} - -

- {new Date(print.date).toLocaleDateString()} -

-
- - {/each} -
- {/if} -
+ ? 'bg-green-500/10 text-green-400' + : print.status === 'Fail' + ? 'bg-red-500/10 text-red-400' + : 'bg-slate-700 text-slate-400'}" + > + {print.status} + +

+ {new Date(print.date).toLocaleDateString( + "en-US", + { + timeZone: "UTC", + }, + )} +

+
+ + {/each} +
+ {/if} +
- -
-

Printer Status

- - {#if activePrintJob} - -
- {activePrintJob.printer_id?.name || "Unknown Printer"} - Printing -
+ +
+

Printer Status

+ + {#if activePrintJob} + +
+ {activePrintJob.printer_id?.name || + "Unknown Printer"} + Printing +
-
- -
-

Currently Printing

-

- {activePrintJob.name} -

-
+
+ +
+

+ Currently Printing +

+

+ {activePrintJob.name} +

+
- -
-
- Progress - {Math.round( - getProgress( - activePrintJob.started_at, - activePrintJob.duration_minutes - ) - )}% -
-
-
-
-
+ +
+
+ Progress + {Math.round( + getProgress( + activePrintJob.started_at, + activePrintJob.duration_minutes, + ), + )}% +
+
+
+
+
- -
-
-

Time Remaining

-

- {getTimeRemaining( - activePrintJob.started_at, - activePrintJob.duration_minutes - )} -

-
-
-

Filament

-

- {activePrintJob.filament_used_g}g -

-
-
+ +
+
+

+ Time Remaining +

+

+ {getTimeRemaining( + activePrintJob.started_at, + activePrintJob.duration_minutes, + )} +

+
+
+

Filament

+

+ {activePrintJob.filament_used_g}g +

+
+
- - {#if activePrintJob.spool_id} -
-
- {activePrintJob.spool_id.brand} - {activePrintJob.spool_id.material || ""} -
- {/if} -
+ + {#if activePrintJob.spool_id} +
+
+ {activePrintJob.spool_id.brand} + {activePrintJob.spool_id.material || + ""} +
+ {/if} +
- - {:else if activePrinter} - -
- {activePrinter.name} - Idle -
-
-

No active print job

- - - -
- - {:else} -
-

No printers configured

- - - -
- {/if} -
-
-
+ + {:else if activePrinter} + +
+ {activePrinter.name} + Idle +
+
+

+ No active print job +

+ + + +
+ + {:else} +
+

+ No printers configured +

+ + + +
+ {/if} + +
+
+ (showQuickLogModal = false)} + action="/prints?/log" +/> + diff --git a/src/routes/prints/+page.server.ts b/src/routes/prints/+page.server.ts index f03b266..7e713d3 100644 --- a/src/routes/prints/+page.server.ts +++ b/src/routes/prints/+page.server.ts @@ -11,14 +11,14 @@ import path from 'path'; export const load: PageServerLoad = async ({ locals }) => { if (!locals.user) throw redirect(303, '/login'); - + await connectDB(); - + // Fetch prints with populated fields (Spool and Printer) - filtered by user 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 @@ -26,7 +26,7 @@ export const load: PageServerLoad = async ({ locals }) => { Spool.find({ user_id: locals.user.id, is_active: true }).lean(), Printer.find({ user_id: locals.user.id }).lean() ]); - + return { prints: JSON.parse(JSON.stringify(prints)), spools: JSON.parse(JSON.stringify(spools)), @@ -37,7 +37,7 @@ export const load: PageServerLoad = async ({ locals }) => { export const actions: Actions = { log: async ({ request, locals }) => { if (!locals.user) return fail(401, { unauthorized: true }); - + const formData = await request.formData(); const name = formData.get('name'); const spool_id = formData.get('spool_id'); @@ -71,7 +71,7 @@ export const actions: Actions = { const weightUsed = Number(filament_used_g); const durationMins = Number(duration_minutes); - + // Calculate Filament Cost: use manual if provided, otherwise calculate let costFilament: number; if (manual_cost && String(manual_cost).trim() !== '') { @@ -96,14 +96,14 @@ export const actions: Actions = { // 2. Create Print Job const isInProgress = status === 'In Progress'; - + // Calculate started_at based on elapsed time let startedAt: Date | null = null; if (isInProgress) { const elapsedMs = Number(elapsed_minutes || 0) * 60 * 1000; startedAt = new Date(Date.now() - elapsedMs); } - + await PrintJob.create({ user_id: locals.user.id, name: name || 'Untitled Print', @@ -134,7 +134,7 @@ export const actions: Actions = { edit: async ({ request, locals }) => { if (!locals.user) return fail(401, { unauthorized: true }); - + const formData = await request.formData(); const id = formData.get('id'); const name = formData.get('name'); @@ -165,17 +165,17 @@ export const actions: Actions = { const weightUsed = Number(filament_used_g); const durationMins = Number(duration_minutes); - + // Get printer for power calculation - const printerForCalc = printer_id + const printerForCalc = printer_id ? await Printer.findById(printer_id) : printJob.printer_id; - + // Get correct spool for cost calculation (use new spool if provided, else existing) - const spoolForCalc = spool_id + const spoolForCalc = spool_id ? await Spool.findById(spool_id) : printJob.spool_id; - + // Calculate Filament Cost: use manual if provided, otherwise calculate let costFilament: number; if (manual_cost && String(manual_cost).trim() !== '') { @@ -203,7 +203,7 @@ export const actions: Actions = { // Calculate started_at based on elapsed time for In Progress const isInProgress = status === 'In Progress'; let startedAt = printJob.started_at; - + if (isInProgress && elapsed_minutes) { const elapsedMs = Number(elapsed_minutes) * 60 * 1000; startedAt = new Date(Date.now() - elapsedMs); @@ -271,7 +271,7 @@ export const actions: Actions = { delete: async ({ request, locals }) => { if (!locals.user) return fail(401, { unauthorized: true }); - + const formData = await request.formData(); const id = formData.get('id'); @@ -282,7 +282,7 @@ export const actions: Actions = { try { // Find the print first to get the model file path const printJob = await PrintJob.findOne({ _id: id, user_id: locals.user.id }); - + if (printJob?.stl_file) { // Delete the model file from disk const filePath = path.join('static', printJob.stl_file); @@ -294,7 +294,7 @@ export const actions: Actions = { } } } - + await PrintJob.findOneAndDelete({ _id: id, user_id: locals.user.id }); return { success: true }; } catch (error) { @@ -305,7 +305,7 @@ export const actions: Actions = { duplicate: async ({ request, locals }) => { if (!locals.user) return fail(401, { unauthorized: true }); - + const formData = await request.formData(); const id = formData.get('id');