From 25935bfc84a49108ab9a6cb596d6a331fe943d02 Mon Sep 17 00:00:00 2001 From: Jenin Date: Sun, 4 Jan 2026 20:10:49 -0500 Subject: [PATCH 1/2] Allow printers to claim as many items as they want, unclaims automatically after 7 days --- drizzle/0025_add_claimed_at.sql | 1 + server.js | 33 +++++++--- src/lib/server/db/schema.ts | 1 + .../api/cron/unclaim-expired/+server.ts | 64 +++++++++++++++++++ .../admin/print/[id]/+page.server.ts | 40 +++++------- .../dashboard/admin/print/[id]/+page.svelte | 4 +- 6 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 drizzle/0025_add_claimed_at.sql create mode 100644 src/routes/api/cron/unclaim-expired/+server.ts diff --git a/drizzle/0025_add_claimed_at.sql b/drizzle/0025_add_claimed_at.sql new file mode 100644 index 0000000..3a6c38f --- /dev/null +++ b/drizzle/0025_add_claimed_at.sql @@ -0,0 +1 @@ +ALTER TABLE "project" ADD COLUMN "claimedAt" timestamp; diff --git a/server.js b/server.js index c3af060..8f88a46 100644 --- a/server.js +++ b/server.js @@ -1,16 +1,29 @@ import express from 'express'; import { handler } from './build/handler.js'; -// import { CronJob } from 'cron'; +import { CronJob } from 'cron'; -// new CronJob( -// '* * * * * *', // cronTime -// function () { -// console.log('You will see this message every second'); -// }, // onTick -// null, // onComplete -// true, // start -// 'Europe/London' // timeZone -// ); +// Run daily at midnight UTC to unclaim expired print claims (older than 7 days) +new CronJob( + '0 0 * * *', // Every day at midnight + async function () { + try { + const baseUrl = process.env.PUBLIC_BASE_URL || `http://localhost:${process.env.PORT ?? 3000}`; + const response = await fetch(`${baseUrl}/api/cron/unclaim-expired`, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.APP_SECRET_KEY}` + } + }); + const result = await response.json(); + console.log(`[Cron] Unclaimed ${result.unclaimedCount} expired print claims`); + } catch (error) { + console.error('[Cron] Failed to unclaim expired prints:', error); + } + }, + null, + true, + 'UTC' +); const app = express(); diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 2f4591e..18ae15a 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -84,6 +84,7 @@ export const project = pgTable('project', { status: projectStatusEnum().notNull().default('building'), printedBy: integer().references(() => user.id), + claimedAt: timestamp(), // When the project was claimed for printing submittedToAirtable: boolean().default(false), diff --git a/src/routes/api/cron/unclaim-expired/+server.ts b/src/routes/api/cron/unclaim-expired/+server.ts new file mode 100644 index 0000000..ea3eba9 --- /dev/null +++ b/src/routes/api/cron/unclaim-expired/+server.ts @@ -0,0 +1,64 @@ +import { json, error } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { db } from '$lib/server/db/index.js'; +import { project, legionReview } from '$lib/server/db/schema.js'; +import { eq, and, lt, isNotNull } from 'drizzle-orm'; + +const CLAIM_EXPIRY_DAYS = 7; + +export async function POST({ request }) { + const authHeader = request.headers.get('Authorization'); + const expectedToken = `Bearer ${env.APP_SECRET_KEY}`; + + if (!authHeader || authHeader !== expectedToken) { + throw error(401, { message: 'Unauthorized' }); + } + + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() - CLAIM_EXPIRY_DAYS); + + // Find all projects that are in 'printing' status with claimedAt older than 7 days + const expiredClaims = await db + .select({ + id: project.id, + name: project.name, + printedBy: project.printedBy, + claimedAt: project.claimedAt + }) + .from(project) + .where( + and( + eq(project.status, 'printing'), + eq(project.deleted, false), + isNotNull(project.claimedAt), + lt(project.claimedAt, expiryDate) + ) + ); + + // Unclaim each expired project + for (const expiredProject of expiredClaims) { + if (expiredProject.printedBy) { + await db.insert(legionReview).values({ + projectId: expiredProject.id, + userId: expiredProject.printedBy, + action: 'unmark_for_printing', + notes: 'Auto-unclaimed after 7 days without printing' + }); + } + + await db + .update(project) + .set({ + status: 't1_approved', + printedBy: null, + claimedAt: null + }) + .where(eq(project.id, expiredProject.id)); + } + + return json({ + success: true, + unclaimedCount: expiredClaims.length, + unclaimed: expiredClaims.map((p) => ({ id: p.id, name: p.name })) + }); +} diff --git a/src/routes/dashboard/admin/print/[id]/+page.server.ts b/src/routes/dashboard/admin/print/[id]/+page.server.ts index a59be0a..21a5ab7 100644 --- a/src/routes/dashboard/admin/print/[id]/+page.server.ts +++ b/src/routes/dashboard/admin/print/[id]/+page.server.ts @@ -5,7 +5,6 @@ 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 } from '../utils.server'; export async function load({ locals, params }) { if (!locals.user) { @@ -80,13 +79,10 @@ export async function load({ locals, params }) { .where(and(eq(devlog.projectId, queriedProject.project.id), eq(devlog.deleted, false))) .orderBy(asc(devlog.createdAt)); - const currentlyPrinting = await getCurrentlyPrinting(locals.user); - return { project: queriedProject, devlogs, - reviews: await getReviewHistory(id), - currentlyPrinting + reviews: await getReviewHistory(id) }; } @@ -99,16 +95,8 @@ export const actions = { throw error(403, { message: 'oi get out' }); } - const currentlyPrinting = await getCurrentlyPrinting(locals.user); - const id: number = parseInt(params.id); - if (currentlyPrinting && currentlyPrinting.id !== id) { - return error(400, { - message: 'you are already printing something else right now' - }); - } - const [queriedProject] = await db .select({ id: project.id, @@ -136,7 +124,8 @@ export const actions = { .update(project) .set({ status: 'printing', - printedBy: locals.user.id + printedBy: locals.user.id, + claimedAt: new Date() }) .where(eq(project.id, id)); @@ -181,7 +170,8 @@ export const actions = { .update(project) .set({ status: 't1_approved', - printedBy: null + printedBy: null, + claimedAt: null }) .where(eq(project.id, id)); @@ -196,20 +186,13 @@ export const actions = { throw error(403, { message: 'oi get out' }); } - const currentlyPrinting = await getCurrentlyPrinting(locals.user); - const id: number = parseInt(params.id); - if (!currentlyPrinting || currentlyPrinting.id !== id) { - return error(400, { - message: "you can only print a project if you've marked it as you're printing it" - }); - } - const [queriedProject] = await db .select({ id: project.id, - status: project.status + status: project.status, + printedBy: project.printedBy }) .from(project) .where(and(eq(project.id, id), eq(project.deleted, false))) @@ -223,6 +206,12 @@ export const actions = { return error(403, { message: 'project is not marked as currently printing' }); } + if (queriedProject.printedBy !== locals.user.id) { + return error(400, { + message: "you can only print a project if you've marked it as you're printing it" + }); + } + const data = await request.formData(); const filamentUsed = data.get('filament'); const notes = data.get('notes')?.toString(); @@ -252,7 +241,8 @@ export const actions = { await db .update(project) .set({ - status: 'printed' + status: 'printed', + claimedAt: null }) .where(eq(project.id, id)); diff --git a/src/routes/dashboard/admin/print/[id]/+page.svelte b/src/routes/dashboard/admin/print/[id]/+page.svelte index cf8ad38..47ef0ec 100644 --- a/src/routes/dashboard/admin/print/[id]/+page.svelte +++ b/src/routes/dashboard/admin/print/[id]/+page.svelte @@ -112,9 +112,9 @@

Printering area

- {#if (data.project.project.status === 't1_approved' && !data.currentlyPrinting) || (data.project.project.status === 'printing' && data.project.project.printedBy === data.user.id)} + {#if data.project.project.status === 't1_approved' || (data.project.project.status === 'printing' && data.project.project.printedBy === data.user.id)}
- {#if data.project.project.status === 't1_approved' && !data.currentlyPrinting} + {#if data.project.project.status === 't1_approved'}
Date: Sun, 4 Jan 2026 20:33:26 -0500 Subject: [PATCH 2/2] see who claimed what projs --- src/routes/dashboard/admin/print/+page.server.ts | 16 ++++++++++++++-- src/routes/dashboard/admin/print/+page.svelte | 10 ++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/routes/dashboard/admin/print/+page.server.ts b/src/routes/dashboard/admin/print/+page.server.ts index 3b91488..43b70e2 100644 --- a/src/routes/dashboard/admin/print/+page.server.ts +++ b/src/routes/dashboard/admin/print/+page.server.ts @@ -2,6 +2,7 @@ import { db } from '$lib/server/db/index.js'; import { project, user, devlog } from '$lib/server/db/schema.js'; import { error } from '@sveltejs/kit'; import { eq, and, sql, ne, inArray } from 'drizzle-orm'; +import { alias } from 'drizzle-orm/pg-core'; import type { Actions } from './$types'; import { getCurrentlyPrinting } from './utils.server'; @@ -83,6 +84,8 @@ async function getProjects( projectFilter: number[], userFilter: number[] ) { + const printer = alias(user, 'printer'); + return await db .select({ project: { @@ -91,18 +94,24 @@ async function getProjects( description: project.description, url: project.url, createdAt: project.createdAt, - status: project.status + status: project.status, + claimedAt: project.claimedAt }, user: { id: user.id, name: user.name }, + printer: { + id: printer.id, + name: printer.name + }, timeSpent: sql`COALESCE(SUM(${devlog.timeSpent}), 0)`, devlogCount: sql`COALESCE(COUNT(${devlog.id}), 0)` }) .from(project) .leftJoin(devlog, and(eq(project.id, devlog.projectId), eq(devlog.deleted, false))) .leftJoin(user, eq(user.id, project.userId)) + .leftJoin(printer, eq(printer.id, project.printedBy)) .where( and( eq(project.deleted, false), @@ -118,7 +127,10 @@ async function getProjects( project.url, project.createdAt, project.status, + project.claimedAt, user.id, - user.name + user.name, + printer.id, + printer.name ); } diff --git a/src/routes/dashboard/admin/print/+page.svelte b/src/routes/dashboard/admin/print/+page.svelte index fc8301f..b8d2b8c 100644 --- a/src/routes/dashboard/admin/print/+page.svelte +++ b/src/routes/dashboard/admin/print/+page.svelte @@ -204,6 +204,16 @@

{projectStatuses[project.project.status]}

+ {#if project.project.status === 'printing' && project.printer?.name} +

+ Claimed by {project.printer.name} + {#if project.project.claimedAt} + + {relativeDate(project.project.claimedAt)} + + {/if} +

+ {/if} {/each}