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/+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
{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} 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 @@