Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/routes/dashboard/admin/print/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,23 @@ export async function load({ locals }) {
throw error(403, { message: 'oi get out' });
}

const projects = await getProjects(['t1_approved'], [], []);

const allowedStatuses: (typeof project.status._.data)[] = ['printing', 't1_approved'];
const projects = await getProjects(allowedStatuses, [], []);

const allProjects = await db
.select({
id: project.id,
name: project.name
name: project.name,
status: project.status,
userId: project.userId
})
.from(project)
.where(and(eq(project.deleted, false)));
.where(and(
eq(project.deleted, false),
inArray(project.status, ['printing', 't1_approved', 'printed']),
sql`(${project.status} != 'printed' OR ${project.userId} = ${locals.user.id})`
));

const users = await db
.select({
Expand All @@ -37,7 +45,8 @@ export async function load({ locals }) {
allProjects,
projects,
users,
currentlyPrinting
currentlyPrinting,
currentUserId: locals.user.id
};
}

Expand All @@ -51,20 +60,27 @@ export const actions = {
}

const data = await request.formData();
const statusFilter = data.getAll('status') as (typeof project.status._.data)[];
const allowedStatuses: (typeof project.status._.data)[] = ['printing', 't1_approved', 'printed'];
const statusFilter = data.getAll('status')
.map((s) => s.toString())
.filter((s): s is typeof project.status._.data => allowedStatuses.includes(s as typeof project.status._.data)) as (typeof project.status._.data)[];

const projectFilter = data.getAll('project').map((projectId) => {
const parsedInt = parseInt(projectId.toString());
if (!parsedInt) throw error(400, { message: 'malformed project filter' });
return parseInt(projectId.toString());
Comment on lines 68 to 71
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parseInt conversion is called twice on the same value. The result of the first parseInt on line 69 is already validated, so the second call on line 71 is redundant. Consider storing the result in a variable and returning it directly to improve code clarity and avoid unnecessary computation.

Copilot uses AI. Check for mistakes.
});

const userFilter = data.getAll('user').map((userId) => {
let userFilter = data.getAll('user').map((userId) => {
const parsedInt = parseInt(userId.toString());
if (!parsedInt) throw error(400, { message: 'malformed user filter' });
return parseInt(userId.toString());
Comment on lines +74 to 77
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parseInt conversion is called twice on the same value. The result of the first parseInt on line 75 is already validated, so the second call on line 77 is redundant. Consider storing the result in a variable and returning it directly to improve code clarity and avoid unnecessary computation.

Copilot uses AI. Check for mistakes.
});

if (statusFilter.length === 1 && statusFilter[0] === 'printed') {
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The userFilter override on lines 80-82 silently discards any user selections when the status filter is 'printed'. This behavior is not obvious from the UI and could be confusing to users who selected specific users in the filter. Consider either making this restriction clear in the UI or preventing users from selecting user filters when 'printed' is the only status selected.

Suggested change
if (statusFilter.length === 1 && statusFilter[0] === 'printed') {
if (statusFilter.length === 1 && statusFilter[0] === 'printed' && userFilter.length === 0) {

Copilot uses AI. Check for mistakes.
userFilter = [locals.user.id];
}

const projects = await getProjects(statusFilter, projectFilter, userFilter);

return {
Expand Down
20 changes: 14 additions & 6 deletions src/routes/dashboard/admin/print/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@
let { data, form } = $props();
type AllProject = { id: number; name: string | null; status: string; userId: number };
const allProjects: AllProject[] = data.allProjects;
const currentUserId: number = data.currentUserId;
let projectSearch = $state('');
let userSearch = $state('');
let projects = $derived(form?.projects ?? data.projects);
let filteredProjects = $derived(
data.allProjects.filter((project) =>
project.name?.toLowerCase().includes(projectSearch.toLowerCase())
)
allProjects
.filter((project) => {
if (project.status === 'printing') return true;
if (project.status === 'printed' && project.userId === currentUserId) return true;
return false;
})
.filter((project) => project.name?.toLowerCase().includes(projectSearch.toLowerCase()))
Comment on lines +20 to +26
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering logic duplicates the server-side filtering in +page.server.ts. The allProjects data already has status and userId filtering applied on the server (lines 28-32 in +page.server.ts), but this client-side filter applies the same logic again. This duplication can lead to maintenance issues if the filtering rules need to change. Consider relying on the server-side filtering or documenting why both are necessary.

Suggested change
allProjects
.filter((project) => {
if (project.status === 'printing') return true;
if (project.status === 'printed' && project.userId === currentUserId) return true;
return false;
})
.filter((project) => project.name?.toLowerCase().includes(projectSearch.toLowerCase()))
allProjects.filter((project) =>
project.name?.toLowerCase().includes(projectSearch.toLowerCase())
)

Copilot uses AI. Check for mistakes.
);
let filteredUsers = $derived(
data.users.filter((user) => user.name.toLowerCase().includes(userSearch.toLowerCase()))
Expand Down Expand Up @@ -64,9 +72,9 @@
value={form?.fields.status ?? ['t1_approved']}
multiple
>
{#each Object.entries(projectStatuses) as [status, longStatus]}
<option value={status} class="truncate">{longStatus}</option>
{/each}
<option value="t1_approved" class="truncate">{projectStatuses['t1_approved'] ?? 'On Print Queue'}</option>
<option value="printing" class="truncate">{projectStatuses['printing'] ?? 'Being printed'}</option>
<option value="printed" class="truncate">{projectStatuses['printed'] ?? 'Printed'}</option>
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: lines 75-76 use tabs while line 77 uses a tab plus spaces. All three option elements should have the same indentation level for consistency.

Suggested change
<option value="printed" class="truncate">{projectStatuses['printed'] ?? 'Printed'}</option>
<option value="printed" class="truncate">{projectStatuses['printed'] ?? 'Printed'}</option>

Copilot uses AI. Check for mistakes.
</select>
</label>

Expand Down
17 changes: 8 additions & 9 deletions src/routes/dashboard/admin/print/[id]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { eq, and, asc, sql } from 'drizzle-orm';
import type { Actions } from './$types';
import { sendSlackDM } from '$lib/server/slack.js';
import { getReviewHistory } from '../../getReviewHistory.server';
import { getCurrentlyPrinting, calculateFilamentUsage } from '../utils.server';

export async function load({ locals, params }) {
if (!locals.user) {
Expand Down Expand Up @@ -213,26 +214,24 @@ export const actions = {
}

const data = await request.formData();
const filamentUsed = data.get('filament');
const gcodeFile = data.get('gcode');
const notes = data.get('notes')?.toString();
const feedback = data.get('feedback')?.toString();

if (notes === null || feedback === null) {
if (!gcodeFile || typeof gcodeFile === 'string' || notes === null || feedback === null) {
return error(400);
}

if (
!filamentUsed ||
isNaN(parseFloat(filamentUsed.toString())) ||
parseFloat(filamentUsed.toString()) < 0
) {
return error(400, { message: 'invalid filament used' });
const gcodeText = await gcodeFile.text();
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No file size validation is performed on the uploaded G-code file before reading its contents with .text(). Large G-code files could potentially cause memory issues or timeouts. Consider adding a file size check and rejecting files that exceed a reasonable limit (e.g., 10-50 MB) before processing.

Suggested change
const gcodeText = await gcodeFile.text();
const MAX_GCODE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB
const gcodeBlob = gcodeFile as Blob;
if (typeof (gcodeBlob as any).size === 'number' && (gcodeBlob as any).size > MAX_GCODE_SIZE_BYTES) {
return error(413, { message: 'G-code file is too large' });
}
const gcodeText = await gcodeBlob.text();

Copilot uses AI. Check for mistakes.
const filamentUsed = calculateFilamentUsage(gcodeText);
if (typeof filamentUsed !== 'number' || isNaN(filamentUsed) || filamentUsed <= 0) {
return error(400, { message: 'Could not calculate valid filament usage from G-code file' });
}

await db.insert(legionReview).values({
projectId: id,
userId: locals.user.id,
filamentUsed: parseFloat(filamentUsed.toString()),
filamentUsed: filamentUsed,
notes,
feedback,
action: 'print'
Expand Down
12 changes: 6 additions & 6 deletions src/routes/dashboard/admin/print/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
<form
method="POST"
action="?/print"
enctype="multipart/form-data"
class="flex flex-col gap-3"
use:enhance={() => {
printFormPending = true;
Expand All @@ -175,15 +176,14 @@
return confirm('really submit?');
}}
>

<label class="flex flex-col gap-1">
<span class="font-medium">Filament used <span class="opacity-50">(grams)</span></span>
<span class="font-medium">G-code file <span class="opacity-50">(from slicer, required)</span></span>
<input
name="filament"
type="number"
name="gcode"
type="file"
class="themed-input-on-box"
placeholder="50"
step="0.1"
min="0"
accept=".gcode"
required
/>
Comment on lines 182 to 188
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file input lacks client-side validation for file size. Users could attempt to upload very large files, which would only fail after upload and processing. Consider adding a maxlength or size attribute, or using JavaScript to validate file size before submission to provide better user experience.

Copilot uses AI. Check for mistakes.
</label>
Expand Down
29 changes: 29 additions & 0 deletions src/routes/dashboard/admin/print/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,32 @@ export async function getCurrentlyPrinting(user: { id: number | SQLWrapper }) {

return currentlyPrinting;
}

Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks documentation explaining its purpose, parameters, return value, and the assumptions it makes about the G-code format. Consider adding a JSDoc comment that explains the calculation method, the assumed filament diameter and density values, and what G-code commands are supported (G1 for extrusion moves, G92 for position resets).

Suggested change
/**
* Estimates the amount of filament used in a print based on its G-code.
*
* The function parses the provided G-code text, tracking the extruder position (`E` value)
* on supported commands and summing only the positive increases in extrusion length.
*
* Assumptions and calculation details:
* - Filament diameter is assumed to be 1.75 mm.
* - Filament density is assumed to be 1.24 g/cm³ (typical for PLA).
* - G-code units are assumed to be millimeters.
* - Extrusion is assumed to be in absolute mode for the `E` axis.
* - Only the following commands are interpreted:
* - `G1` with an `E` parameter for extrusion moves; increases in `E` are treated as
* additional filament length extruded.
* - `G92` with an `E` parameter to reset the current extruder position.
* - All other commands and parameters are ignored for the purpose of this calculation.
*
* The total extruded filament length (in mm) is converted to volume using the
* cross-sectional area of a 1.75 mm filament, then converted to cm³ and multiplied
* by the assumed density to estimate the mass in grams.
*
* @param gcodeText - Full G-code content as a string, using millimeters and absolute `E`.
* @returns Estimated filament mass in grams.
*/

Copilot uses AI. Check for mistakes.
export function calculateFilamentUsage(gcodeText: string): number {
const FILAMENT_DIAMETER = 1.75;
const FILAMENT_DENSITY = 1.24;
Comment on lines +25 to +26
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filament diameter (1.75mm) and density (1.24 g/cm³) are hardcoded. These values may vary depending on the filament type (PLA, ABS, PETG, etc.) and manufacturer. Consider making these configurable parameters or documenting which filament type these values represent. The density appears to be for PLA, but this should be explicitly documented.

Copilot uses AI. Check for mistakes.
const lines = gcodeText.split('\n');
let totalExtruded = 0;
let currentE = 0;
for (const line of lines) {
const trimmed = line.trim().toUpperCase();
if (trimmed.startsWith('G92')) {
const eMatch = trimmed.match(/E([0-9.-]+)/);
if (eMatch) {
currentE = parseFloat(eMatch[1]);
}
} else if (trimmed.startsWith('G1')) {
const eMatch = trimmed.match(/E([0-9.-]+)/);
Comment on lines +33 to +38
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regular expression pattern allows multiple dots in numbers (e.g., "1.2.3" would match). This could lead to parseFloat returning NaN for malformed numbers, though the validation at line 227 would catch it. Consider using a more restrictive pattern like /E(-?[0-9]+.?[0-9]*)/ to ensure only valid decimal numbers are matched.

Suggested change
const eMatch = trimmed.match(/E([0-9.-]+)/);
if (eMatch) {
currentE = parseFloat(eMatch[1]);
}
} else if (trimmed.startsWith('G1')) {
const eMatch = trimmed.match(/E([0-9.-]+)/);
const eMatch = trimmed.match(/E(-?\d+(?:\.\d+)?)/);
if (eMatch) {
currentE = parseFloat(eMatch[1]);
}
} else if (trimmed.startsWith('G1')) {
const eMatch = trimmed.match(/E(-?\d+(?:\.\d+)?)/);

Copilot uses AI. Check for mistakes.
if (eMatch) {
const newE = parseFloat(eMatch[1]);
if (newE > currentE) {
totalExtruded += newE - currentE;
}
currentE = newE;
Comment on lines +41 to +44
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The G-code parser only accounts for positive extrusion differences when newE is greater than currentE. However, it doesn't handle retractions (negative E movements) properly. When newE is less than currentE (which happens during retraction), the currentE is still updated to newE, but the totalExtruded is not adjusted. This can lead to incorrect calculations if the G-code uses absolute extrusion positioning. Consider tracking retractions separately or handling all E value changes to maintain accurate state.

Copilot uses AI. Check for mistakes.
}
}
Comment on lines +30 to +46
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function processes every line in the G-code file even if most lines don't contain E (extrusion) commands. For large G-code files, this could impact performance. Consider optimizing by checking for the presence of 'E' in the line before running the regex match, or filtering lines that contain 'E' first before processing.

Copilot uses AI. Check for mistakes.
}
const area = Math.PI * (FILAMENT_DIAMETER / 2) ** 2;
const volumeCm3 = (totalExtruded * area) / 1000;
return volumeCm3 * FILAMENT_DENSITY;
}