Update 0.2.0
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal 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
136
README.md
@@ -4,21 +4,18 @@ Filaprint is a modern, premium web application designed to help 3D printing enth
|
||||
|
||||

|
||||
|
||||
## 🛠️ 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.
|
||||
|
||||
62
bun.lock
62
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=="],
|
||||
|
||||
|
||||
30
package.json
30
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { fade, scale } from "svelte/transition";
|
||||
import type { Snippet } from "svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { fade, scale } from "svelte/transition";
|
||||
import type { Snippet } from "svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
title: string;
|
||||
children: Snippet;
|
||||
onclose: () => void;
|
||||
}
|
||||
interface Props {
|
||||
open: boolean;
|
||||
title: string;
|
||||
children: Snippet;
|
||||
onclose: () => void;
|
||||
}
|
||||
|
||||
let { open, title, children, onclose }: Props = $props();
|
||||
let { open, title, children, onclose }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 bg-black/70" onclick={onclose}></div>
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 bg-black/70" onclick={onclose}></div>
|
||||
|
||||
<!-- Dialog Panel -->
|
||||
<div
|
||||
class="relative z-10 w-full max-w-lg rounded-xl border border-[#3f3f46] shadow-2xl"
|
||||
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"
|
||||
style="background-color: #27272a;"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-white">{title}</h3>
|
||||
<button
|
||||
onclick={onclose}
|
||||
class="rounded p-1 text-[#a1a1aa] transition-colors hover:bg-[#3f3f46] hover:text-white"
|
||||
aria-label="Close"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="mdi:close" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- Dialog Panel -->
|
||||
<div
|
||||
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 shrink-0"
|
||||
style="background-color: #27272a;"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-white">{title}</h3>
|
||||
<button
|
||||
onclick={onclose}
|
||||
class="rounded p-1 text-[#a1a1aa] transition-colors hover:bg-[#3f3f46] hover:text-white"
|
||||
aria-label="Close"
|
||||
type="button"
|
||||
>
|
||||
<Icon icon="mdi:close" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="p-6 overflow-y-auto">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,426 +1,517 @@
|
||||
<script lang="ts">
|
||||
import Card from "$lib/components/ui/Card.svelte";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import Card from "$lib/components/ui/Card.svelte";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
let { data } = $props();
|
||||
// svelte-ignore non_reactive_update
|
||||
let stats = $derived(data.stats);
|
||||
// svelte-ignore non_reactive_update
|
||||
let recentPrints = $derived(data.recentPrints || []);
|
||||
// svelte-ignore non_reactive_update
|
||||
let activePrinter = $derived(data.activePrinter);
|
||||
// svelte-ignore non_reactive_update
|
||||
let activePrintJob = $derived(data.activePrintJob);
|
||||
import LogPrintModal from "$lib/components/prints/LogPrintModal.svelte";
|
||||
|
||||
// Timer state
|
||||
let currentTime = $state(Date.now());
|
||||
let timerInterval: ReturnType<typeof setInterval>;
|
||||
let notificationSent = $state(false);
|
||||
let { data } = $props();
|
||||
// svelte-ignore non_reactive_update
|
||||
let stats = $derived(data.stats);
|
||||
// svelte-ignore non_reactive_update
|
||||
let recentPrints = $derived(data.recentPrints || []);
|
||||
// svelte-ignore non_reactive_update
|
||||
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 || []);
|
||||
|
||||
onMount(() => {
|
||||
// Request notification permission
|
||||
if (
|
||||
browser &&
|
||||
"Notification" in window &&
|
||||
Notification.permission === "default"
|
||||
) {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
let showQuickLogModal = $state(false);
|
||||
|
||||
// Update timer every 10 seconds
|
||||
timerInterval = setInterval(() => {
|
||||
currentTime = Date.now();
|
||||
// Timer state
|
||||
let currentTime = $state(Date.now());
|
||||
let timerInterval: ReturnType<typeof setInterval>;
|
||||
let notificationSent = $state(false);
|
||||
|
||||
// Check if print is complete
|
||||
if (activePrintJob && !notificationSent) {
|
||||
const progress = getProgress(
|
||||
activePrintJob.started_at,
|
||||
activePrintJob.duration_minutes
|
||||
);
|
||||
if (progress >= 100) {
|
||||
sendNotification();
|
||||
notificationSent = true;
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
// ... (rest of onMount/onDestroy/notifications - keeping as is)
|
||||
onMount(() => {
|
||||
// Request notification permission
|
||||
if (
|
||||
browser &&
|
||||
"Notification" in window &&
|
||||
Notification.permission === "default"
|
||||
) {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
});
|
||||
// Update timer every 10 seconds
|
||||
timerInterval = setInterval(() => {
|
||||
currentTime = Date.now();
|
||||
|
||||
function sendNotification() {
|
||||
if (
|
||||
browser &&
|
||||
"Notification" in window &&
|
||||
Notification.permission === "granted"
|
||||
) {
|
||||
new Notification("🎉 Print Complete!", {
|
||||
body: `Your print "${activePrintJob?.name}" has finished!`,
|
||||
icon: "/favicon.png",
|
||||
});
|
||||
}
|
||||
}
|
||||
// Check if print is complete
|
||||
if (activePrintJob && !notificationSent) {
|
||||
const progress = getProgress(
|
||||
activePrintJob.started_at,
|
||||
activePrintJob.duration_minutes,
|
||||
);
|
||||
if (progress >= 100) {
|
||||
sendNotification();
|
||||
notificationSent = true;
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
// Calculate time remaining for active print
|
||||
function getTimeRemaining(
|
||||
startedAt: string,
|
||||
durationMinutes: number
|
||||
): string {
|
||||
if (!startedAt) return "--:--";
|
||||
const start = new Date(startedAt).getTime();
|
||||
const elapsed = Math.floor((currentTime - start) / 60000); // minutes
|
||||
const remaining = Math.max(0, durationMinutes - elapsed);
|
||||
if (remaining === 0) return "Complete!";
|
||||
const hours = Math.floor(remaining / 60);
|
||||
const mins = remaining % 60;
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
}
|
||||
onDestroy(() => {
|
||||
if (timerInterval) clearInterval(timerInterval);
|
||||
});
|
||||
|
||||
function getProgress(startedAt: string, durationMinutes: number): number {
|
||||
if (!startedAt || !durationMinutes) return 0;
|
||||
const start = new Date(startedAt).getTime();
|
||||
const elapsed = (currentTime - start) / 60000;
|
||||
return Math.min(100, Math.max(0, (elapsed / durationMinutes) * 100));
|
||||
}
|
||||
function sendNotification() {
|
||||
if (
|
||||
browser &&
|
||||
"Notification" in window &&
|
||||
Notification.permission === "granted"
|
||||
) {
|
||||
new Notification("🎉 Print Complete!", {
|
||||
body: `Your print "${activePrintJob?.name}" has finished!`,
|
||||
icon: "/favicon.png",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate time remaining for active print
|
||||
function getTimeRemaining(
|
||||
startedAt: string,
|
||||
durationMinutes: number,
|
||||
): string {
|
||||
if (!startedAt) return "--:--";
|
||||
const start = new Date(startedAt).getTime();
|
||||
const elapsed = Math.floor((currentTime - start) / 60000); // minutes
|
||||
const remaining = Math.max(0, durationMinutes - elapsed);
|
||||
if (remaining === 0) return "Complete!";
|
||||
const hours = Math.floor(remaining / 60);
|
||||
const mins = remaining % 60;
|
||||
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
}
|
||||
|
||||
function getProgress(startedAt: string, durationMinutes: number): number {
|
||||
if (!startedAt || !durationMinutes) return 0;
|
||||
const start = new Date(startedAt).getTime();
|
||||
const elapsed = (currentTime - start) / 60000;
|
||||
return Math.min(100, Math.max(0, (elapsed / durationMinutes) * 100));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-8 fade-in">
|
||||
<!-- Header -->
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<Button variant="secondary" size="sm">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
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Header -->
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<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
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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">
|
||||
Active Spools
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white">{stats.spoolCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
Active Spools
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white"
|
||||
>{stats.spoolCount}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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">
|
||||
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
|
||||
></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
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
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
|
||||
></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
|
||||
></span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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">
|
||||
Printers
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white">{stats.printerCount}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
Printers
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white"
|
||||
>{stats.printerCount}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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">
|
||||
Est. Value
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white"
|
||||
>${stats.estimatedValue}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
Est. Value
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white"
|
||||
>${stats.estimatedValue}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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">
|
||||
Total Spent
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white">${stats.totalSpent}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Card class="relative overflow-hidden group">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
Total Spent
|
||||
</p>
|
||||
<div class="flex items-baseline mt-2">
|
||||
<span class="text-3xl font-bold text-white"
|
||||
>${stats.totalSpent}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 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>
|
||||
<a
|
||||
href="/prints"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>View All</a
|
||||
>
|
||||
</div>
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 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>
|
||||
<a
|
||||
href="/prints"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>View All</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if recentPrints.length === 0}
|
||||
<Card
|
||||
class="min-h-[300px] flex items-center justify-center border-dashed border-slate-700 bg-transparent/50"
|
||||
>
|
||||
<div class="text-center">
|
||||
<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" />
|
||||
</div>
|
||||
<p class="text-slate-500">No recent prints found</p>
|
||||
<a href="/prints">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="mt-2 text-blue-400 hover:text-blue-300"
|
||||
>Log a Print</Button
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each recentPrints as print}
|
||||
<Card
|
||||
class="flex items-center justify-between p-4! hover:bg-slate-800/80 transition-colors group"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center group-hover:scale-105 transition-transform
|
||||
{#if recentPrints.length === 0}
|
||||
<Card
|
||||
class="min-h-[300px] flex items-center justify-center border-dashed border-slate-700 bg-transparent/50"
|
||||
>
|
||||
<div class="text-center">
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-slate-500">No recent prints found</p>
|
||||
<a href="/prints">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="mt-2 text-blue-400 hover:text-blue-300"
|
||||
>Log a Print</Button
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each recentPrints as print}
|
||||
<Card
|
||||
class="flex items-center justify-between p-4! hover:bg-slate-800/80 transition-colors group"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center group-hover:scale-105 transition-transform
|
||||
{print.status === 'Fail'
|
||||
? 'text-red-400 bg-red-500/10'
|
||||
: print.status === 'In Progress'
|
||||
? 'text-blue-400 bg-blue-500/10'
|
||||
: 'text-blue-400 bg-blue-500/10'}"
|
||||
>
|
||||
{#if print.status === "Success"}
|
||||
<Icon
|
||||
icon="mdi:check-circle"
|
||||
class="w-5 h-5 text-green-400"
|
||||
/>
|
||||
{:else if print.status === "Fail"}
|
||||
<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" />
|
||||
{:else}
|
||||
<Icon icon="mdi:printer-3d" class="w-5 h-5" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span
|
||||
class="px-2 py-0.5 rounded text-[10px] font-medium
|
||||
? 'text-red-400 bg-red-500/10'
|
||||
: print.status === 'In Progress'
|
||||
? 'text-blue-400 bg-blue-500/10'
|
||||
: 'text-blue-400 bg-blue-500/10'}"
|
||||
>
|
||||
{#if print.status === "Success"}
|
||||
<Icon
|
||||
icon="mdi:check-circle"
|
||||
class="w-5 h-5 text-green-400"
|
||||
/>
|
||||
{:else if print.status === "Fail"}
|
||||
<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"
|
||||
/>
|
||||
{:else}
|
||||
<Icon
|
||||
icon="mdi:printer-3d"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span
|
||||
class="px-2 py-0.5 rounded text-[10px] font-medium
|
||||
{print.status === 'Success'
|
||||
? '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}
|
||||
</span>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
{new Date(print.date).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
? '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}
|
||||
</span>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
{new Date(print.date).toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
timeZone: "UTC",
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions / Status -->
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-semibold text-white/90">Printer Status</h2>
|
||||
<Card>
|
||||
{#if activePrintJob}
|
||||
<!-- 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
|
||||
>
|
||||
<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"
|
||||
>Printing</span
|
||||
>
|
||||
</div>
|
||||
<!-- Quick Actions / Status -->
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-semibold text-white/90">Printer Status</h2>
|
||||
<Card>
|
||||
{#if activePrintJob}
|
||||
<!-- 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
|
||||
>
|
||||
<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"
|
||||
>Printing</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Job Name -->
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 mb-1">Currently Printing</p>
|
||||
<p class="text-sm font-semibold text-white">
|
||||
{activePrintJob.name}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<!-- Job Name -->
|
||||
<div>
|
||||
<p class="text-xs text-slate-400 mb-1">
|
||||
Currently Printing
|
||||
</p>
|
||||
<p class="text-sm font-semibold text-white">
|
||||
{activePrintJob.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
class="flex justify-between text-xs text-slate-400 font-medium"
|
||||
>
|
||||
<span>Progress</span>
|
||||
<span class="text-slate-200"
|
||||
>{Math.round(
|
||||
getProgress(
|
||||
activePrintJob.started_at,
|
||||
activePrintJob.duration_minutes
|
||||
)
|
||||
)}%</span
|
||||
>
|
||||
</div>
|
||||
<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
|
||||
)}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Progress Bar -->
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
class="flex justify-between text-xs text-slate-400 font-medium"
|
||||
>
|
||||
<span>Progress</span>
|
||||
<span class="text-slate-200"
|
||||
>{Math.round(
|
||||
getProgress(
|
||||
activePrintJob.started_at,
|
||||
activePrintJob.duration_minutes,
|
||||
),
|
||||
)}%</span
|
||||
>
|
||||
</div>
|
||||
<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,
|
||||
)}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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="font-medium text-white">
|
||||
{getTimeRemaining(
|
||||
activePrintJob.started_at,
|
||||
activePrintJob.duration_minutes
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400">Filament</p>
|
||||
<p class="font-medium text-white">
|
||||
{activePrintJob.filament_used_g}g
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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="font-medium text-white">
|
||||
{getTimeRemaining(
|
||||
activePrintJob.started_at,
|
||||
activePrintJob.duration_minutes,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400">Filament</p>
|
||||
<p class="font-medium text-white">
|
||||
{activePrintJob.filament_used_g}g
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spool Info -->
|
||||
{#if activePrintJob.spool_id}
|
||||
<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}"
|
||||
></div>
|
||||
<span class="text-slate-300"
|
||||
>{activePrintJob.spool_id.brand}
|
||||
{activePrintJob.spool_id.material || ""}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Spool Info -->
|
||||
{#if activePrintJob.spool_id}
|
||||
<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}"
|
||||
></div>
|
||||
<span class="text-slate-300"
|
||||
>{activePrintJob.spool_id.brand}
|
||||
{activePrintJob.spool_id.material ||
|
||||
""}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
</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="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>
|
||||
<a href="/prints">
|
||||
<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
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center py-6">
|
||||
<p class="text-sm text-text-muted mb-4">No printers configured</p>
|
||||
<a href="/printers">
|
||||
<Button variant="primary" size="sm">Add Printer</Button>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
</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="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>
|
||||
<a href="/prints">
|
||||
<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
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center py-6">
|
||||
<p class="text-sm text-text-muted mb-4">
|
||||
No printers configured
|
||||
</p>
|
||||
<a href="/printers">
|
||||
<Button variant="primary" size="sm"
|
||||
>Add Printer</Button
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LogPrintModal
|
||||
open={showQuickLogModal}
|
||||
{printers}
|
||||
{spools}
|
||||
onclose={() => (showQuickLogModal = false)}
|
||||
action="/prints?/log"
|
||||
/>
|
||||
|
||||
<style>
|
||||
/* Simple entry animation */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
/* Simple entry animation */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user