@mxpicture/print-backlog 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/public/assets/index-BnhoKdrs.css +1 -0
  2. package/dist/public/assets/index-j0LpYaft.js +1 -0
  3. package/dist/public/index.html +2 -2
  4. package/dist/server/api/items.d.ts +49 -0
  5. package/dist/server/api/items.d.ts.map +1 -0
  6. package/dist/server/api/items.js +552 -0
  7. package/dist/server/api/items.js.map +1 -0
  8. package/dist/server/api/queues.d.ts +12 -0
  9. package/dist/server/api/queues.d.ts.map +1 -1
  10. package/dist/server/api/queues.js +88 -0
  11. package/dist/server/api/queues.js.map +1 -1
  12. package/dist/server/app.d.ts +3 -1
  13. package/dist/server/app.d.ts.map +1 -1
  14. package/dist/server/app.js +30 -6
  15. package/dist/server/app.js.map +1 -1
  16. package/dist/server/cli.js +4 -1
  17. package/dist/server/cli.js.map +1 -1
  18. package/dist/server/multipart.d.ts +17 -0
  19. package/dist/server/multipart.d.ts.map +1 -0
  20. package/dist/server/multipart.js +72 -0
  21. package/dist/server/multipart.js.map +1 -0
  22. package/dist/server/positions.d.ts +21 -0
  23. package/dist/server/positions.d.ts.map +1 -0
  24. package/dist/server/positions.js +79 -0
  25. package/dist/server/positions.js.map +1 -0
  26. package/dist/server/storage.d.ts +79 -0
  27. package/dist/server/storage.d.ts.map +1 -0
  28. package/dist/server/storage.js +199 -0
  29. package/dist/server/storage.js.map +1 -0
  30. package/dist/server/zip.d.ts +35 -0
  31. package/dist/server/zip.d.ts.map +1 -0
  32. package/dist/server/zip.js +190 -0
  33. package/dist/server/zip.js.map +1 -0
  34. package/dist/shared/types.d.ts +99 -0
  35. package/dist/shared/types.d.ts.map +1 -1
  36. package/package.json +9 -1
  37. package/dist/public/assets/index-CLP1jBWd.js +0 -1
  38. package/dist/public/assets/index-DxpzozYw.css +0 -1
@@ -0,0 +1,199 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { link, mkdir, open, stat, unlink } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ /**
6
+ * The write-once file store under DATA_DIR/files (docs/plan.md §3.5).
7
+ *
8
+ * - An upload streams into files/.tmp/<uuid>, hashed and counted on the way,
9
+ * fsync'd, and only then placed at files/<item_id>/<n>-<sha12>-<name>.
10
+ * - Placing is link() + unlink(), never rename(): link() fails with EEXIST
11
+ * instead of replacing a file, so a stored version can never be
12
+ * overwritten. The VPS backs files/ up with `rclone --immutable`, which
13
+ * fails loudly on a rewritten file; this keeps it from ever happening.
14
+ * - .tmp sits inside files/ so the link stays on one filesystem.
15
+ */
16
+ /** Model formats we keep. A ZIP upload is unpacked into these (zip.ts). */
17
+ export const MODEL_EXTENSIONS = ["stl", "3mf", "step", "stp", "obj"];
18
+ /** Formats Bambuddy can take (Send to Bambuddy); STEP it cannot slice. */
19
+ export const BAMBUDDY_EXTENSIONS = new Set([
20
+ "stl",
21
+ "3mf",
22
+ "obj",
23
+ ]);
24
+ export class UploadRejected extends Error {
25
+ status;
26
+ constructor(status, message) {
27
+ super(message);
28
+ this.status = status;
29
+ }
30
+ }
31
+ const MAX_NAME_CHARS = 180;
32
+ /**
33
+ * A file name that is safe on disk and in a Content-Disposition header:
34
+ * NFC, no path separators or control characters, no leading dots, at most
35
+ * 180 characters with the extension kept. Never empty.
36
+ */
37
+ export const safeFilename = (name) => {
38
+ let s = name.normalize("NFC");
39
+ // Only the last path segment (some browsers send a path).
40
+ s = s.split(/[/\\]/).pop() ?? "";
41
+ s = s.replace(/[\u0000-\u001f\u007f"<>:|?*]/g, "_").trim();
42
+ s = s.replace(/^\.+/, "");
43
+ if (s.length > MAX_NAME_CHARS) {
44
+ const dot = s.lastIndexOf(".");
45
+ const ext = dot > 0 && s.length - dot <= 10 ? s.slice(dot) : "";
46
+ s = s.slice(0, MAX_NAME_CHARS - ext.length) + ext;
47
+ }
48
+ return s || "file";
49
+ };
50
+ /**
51
+ * The model extension of a file name, or an UploadRejected that says why
52
+ * not. Sliced files belong in Bambuddy, not here.
53
+ */
54
+ export const modelExt = (filename) => {
55
+ const lower = filename.toLowerCase();
56
+ if (lower.endsWith(".gcode.3mf") || lower.endsWith(".gcode"))
57
+ throw new UploadRejected(415, `${filename}: a sliced file; upload it to Bambuddy instead`);
58
+ const dot = lower.lastIndexOf(".");
59
+ const ext = dot >= 0 ? lower.slice(dot + 1) : "";
60
+ if (MODEL_EXTENSIONS.includes(ext))
61
+ return ext;
62
+ throw new UploadRejected(415, `${filename}: not a supported model file (${MODEL_EXTENSIONS.map((e) => `.${e}`).join(", ")})`);
63
+ };
64
+ /** The title a new item gets from its first file: the name without the extension, separators as spaces. */
65
+ export const titleFromFilename = (filename) => {
66
+ const dot = filename.lastIndexOf(".");
67
+ const stem = dot > 0 ? filename.slice(0, dot) : filename;
68
+ const title = stem.replace(/[_]+/g, " ").replace(/\s+/g, " ").trim();
69
+ return title || filename;
70
+ };
71
+ export const isModel = (f) => f.ext !== "zip";
72
+ /** modelExt, plus `.zip` when allowed. */
73
+ export const uploadExt = (filename, allowZip) => {
74
+ if (allowZip && filename.toLowerCase().endsWith(".zip"))
75
+ return "zip";
76
+ return modelExt(filename);
77
+ };
78
+ export class FileStore {
79
+ maxBytes;
80
+ root;
81
+ tmpDir;
82
+ constructor(dataDir, maxBytes) {
83
+ this.maxBytes = maxBytes;
84
+ this.root = join(dataDir, "files");
85
+ this.tmpDir = join(this.root, ".tmp");
86
+ }
87
+ async init() {
88
+ await mkdir(this.tmpDir, { recursive: true });
89
+ }
90
+ /**
91
+ * Stream one upload into .tmp. Rejects (and removes the partial file) when
92
+ * the name is not a model file or the stream exceeds maxBytes; `truncated`
93
+ * is busboy's flag for a file cut off at its fileSize limit.
94
+ */
95
+ async stage(stream, rawName, truncated, opts = {}) {
96
+ const filename = safeFilename(rawName);
97
+ let ext;
98
+ try {
99
+ ext = uploadExt(filename, opts.allowZip ?? false);
100
+ }
101
+ catch (e) {
102
+ stream.resume(); // drain, so busboy moves on to the next part
103
+ throw e;
104
+ }
105
+ const tmpPath = join(this.tmpDir, randomUUID());
106
+ const hash = createHash("sha256");
107
+ let size = 0;
108
+ const fh = await open(tmpPath, "wx", 0o640);
109
+ try {
110
+ // Never break out of this loop: leaving a `for await` destroys the
111
+ // stream, and busboy then never finishes the request. Past the cap the
112
+ // rest is read and dropped, and the error comes at the end.
113
+ let tooBig = false;
114
+ for await (const chunk of stream) {
115
+ size += chunk.length;
116
+ if (tooBig || size > this.maxBytes) {
117
+ tooBig = true;
118
+ continue;
119
+ }
120
+ hash.update(chunk);
121
+ await fh.write(chunk);
122
+ }
123
+ if (tooBig || truncated())
124
+ throw new UploadRejected(413, `${filename}: larger than ${this.maxBytes} bytes`);
125
+ await fh.sync();
126
+ }
127
+ catch (e) {
128
+ stream.resume();
129
+ await fh.close();
130
+ await unlink(tmpPath).catch(() => { });
131
+ throw e;
132
+ }
133
+ await fh.close();
134
+ return { tmpPath, filename, ext, size, sha256: hash.digest("hex") };
135
+ }
136
+ /** The path, relative to the store root, a version is stored at. */
137
+ static relativePath(itemId, n, sha256, filename) {
138
+ return join(String(itemId), `${n}-${sha256.slice(0, 12)}-${filename}`);
139
+ }
140
+ /** Place a staged file at its final path, write-once (EEXIST if anything is there). */
141
+ async place(staged, relPath) {
142
+ const target = this.absolute(relPath);
143
+ await mkdir(dirname(target), { recursive: true });
144
+ await link(staged.tmpPath, target);
145
+ await unlink(staged.tmpPath);
146
+ // Make the new directory entry durable as well.
147
+ const dir = await open(dirname(target), "r");
148
+ try {
149
+ await dir.sync();
150
+ }
151
+ finally {
152
+ await dir.close();
153
+ }
154
+ }
155
+ /**
156
+ * Write a small buffer (a thumbnail) write-once at relPath: through .tmp
157
+ * and link(), like an upload, so EEXIST protects it the same way.
158
+ */
159
+ async placeBuffer(data, relPath) {
160
+ const tmpPath = join(this.tmpDir, randomUUID());
161
+ const fh = await open(tmpPath, "wx", 0o640);
162
+ try {
163
+ await fh.write(data);
164
+ await fh.sync();
165
+ }
166
+ finally {
167
+ await fh.close();
168
+ }
169
+ try {
170
+ await this.place({ tmpPath, filename: "", ext: "stl", size: data.length, sha256: "" }, relPath);
171
+ }
172
+ catch (e) {
173
+ await unlink(tmpPath).catch(() => { });
174
+ throw e;
175
+ }
176
+ }
177
+ /** Drop a staged file that will not be placed (duplicate, rejected batch, failed transaction). */
178
+ async discard(staged) {
179
+ await unlink(staged.tmpPath).catch(() => { });
180
+ }
181
+ absolute(relPath) {
182
+ const abs = join(this.root, relPath);
183
+ if (!abs.startsWith(this.root + "/"))
184
+ throw new Error(`path outside the store: ${relPath}`);
185
+ return abs;
186
+ }
187
+ async exists(relPath) {
188
+ try {
189
+ return (await stat(this.absolute(relPath))).isFile();
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ read(relPath) {
196
+ return createReadStream(this.absolute(relPath));
197
+ }
198
+ }
199
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/server/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { Readable } from "node:stream";
2
+ import { type FileStore, type StagedFile, type StagedModel } from "./storage.js";
3
+ export interface Unpacked {
4
+ models: StagedModel[];
5
+ /** entries that were not imported (not a model file, sliced, hidden) */
6
+ skipped: string[];
7
+ }
8
+ /**
9
+ * Stage every model file in a staged ZIP; the ZIP itself is discarded.
10
+ * - Each entry goes through store.stage, so the size cap holds per entry
11
+ * (a ZIP bomb stops at maxBytes) and the name is sanitised (the entry's
12
+ * path is dropped; only its base name is kept).
13
+ * - At most `maxModels` models; a ZIP without any model is refused.
14
+ * On any error the models staged so far are discarded.
15
+ */
16
+ export declare const unpackZip: (store: FileStore, zipFile: StagedFile, maxModels: number) => Promise<Unpacked>;
17
+ /**
18
+ * The preview PNG embedded in a 3MF, or null. Never throws: a 3MF without
19
+ * a usable preview (or not a valid ZIP at all) just has no thumbnail.
20
+ */
21
+ export declare const extract3mfThumbnail: (path: string) => Promise<Buffer | null>;
22
+ /** A name that is unique within one export ZIP: "a.stl", "a (2).stl", … */
23
+ export declare const uniqueName: (taken: Set<string>, name: string) => string;
24
+ export interface ExportEntry {
25
+ name: string;
26
+ relPath: string;
27
+ ext: string;
28
+ mtime: Date;
29
+ }
30
+ /**
31
+ * A ZIP of the given files, as a stream, built while it is sent. 3MF is a
32
+ * ZIP already, so it is stored; everything else is deflated.
33
+ */
34
+ export declare const exportZip: (store: FileStore, files: ExportEntry[]) => Readable;
35
+ //# sourceMappingURL=zip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.d.ts","sourceRoot":"","sources":["../../src/server/zip.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EACL,KAAK,SAAS,EAEd,KAAK,UAAU,EACf,KAAK,WAAW,EAEjB,MAAM,cAAc,CAAC;AAiFtB,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,wEAAwE;IACxE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GACpB,OAAO,SAAS,EAChB,SAAS,UAAU,EACnB,WAAW,MAAM,KAChB,OAAO,CAAC,QAAQ,CA8ClB,CAAC;AAeF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,KACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAgCvB,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,UAAU,GAAI,OAAO,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,MAAM,KAAG,MAS7D,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,IAAI,CAAC;CACb;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,SAAS,EAAE,OAAO,WAAW,EAAE,KAAG,QAWlE,CAAC"}
@@ -0,0 +1,190 @@
1
+ import { posix } from "node:path";
2
+ import yauzl from "yauzl";
3
+ import yazl from "yazl";
4
+ import { MODEL_EXTENSIONS, UploadRejected, } from "./storage.js";
5
+ /**
6
+ * ZIP in both directions (docs/plan.md §3.5, §3.6):
7
+ * - unpack a dropped ZIP into model files (one item each);
8
+ * - pull the preview PNG out of a 3MF (a 3MF is a ZIP);
9
+ * - stream a queue's current versions out as one ZIP.
10
+ */
11
+ const openZip = (path) => new Promise((resolve, reject) => yauzl.open(path,
12
+ // lazyEntries: we ask for the next entry; validateEntrySizes: a ZIP that
13
+ // lies about an entry's size errors instead of inflating past it.
14
+ {
15
+ lazyEntries: true,
16
+ autoClose: false,
17
+ validateEntrySizes: true,
18
+ strictFileNames: false,
19
+ }, (err, zip) => err || !zip
20
+ ? reject(err ?? new Error("cannot open zip"))
21
+ : resolve(zip)));
22
+ const openEntry = (zip, entry) => new Promise((resolve, reject) => zip.openReadStream(entry, (err, stream) => err || !stream
23
+ ? reject(err ?? new Error("cannot read entry"))
24
+ : resolve(stream)));
25
+ /** Every entry, in order, without reading data (the caller opens what it needs before asking for the next). */
26
+ async function* entries(zip) {
27
+ for (;;) {
28
+ const next = await new Promise((resolve, reject) => {
29
+ const onEntry = (e) => {
30
+ cleanup();
31
+ resolve(e);
32
+ };
33
+ const onEnd = () => {
34
+ cleanup();
35
+ resolve(null);
36
+ };
37
+ const onError = (e) => {
38
+ cleanup();
39
+ reject(e);
40
+ };
41
+ const cleanup = () => {
42
+ zip.off("entry", onEntry);
43
+ zip.off("end", onEnd);
44
+ zip.off("error", onError);
45
+ };
46
+ zip.on("entry", onEntry);
47
+ zip.on("end", onEnd);
48
+ zip.on("error", onError);
49
+ zip.readEntry();
50
+ });
51
+ if (!next)
52
+ return;
53
+ yield next;
54
+ }
55
+ }
56
+ /** macOS resource forks, hidden files, directories: never a model. */
57
+ const isJunk = (name) => name.endsWith("/") ||
58
+ name.split("/").some((part) => part.startsWith(".") || part === "__MACOSX");
59
+ const isModelName = (name) => {
60
+ const lower = name.toLowerCase();
61
+ if (lower.endsWith(".gcode.3mf"))
62
+ return false;
63
+ return MODEL_EXTENSIONS.includes(posix.extname(lower).slice(1));
64
+ };
65
+ /**
66
+ * Stage every model file in a staged ZIP; the ZIP itself is discarded.
67
+ * - Each entry goes through store.stage, so the size cap holds per entry
68
+ * (a ZIP bomb stops at maxBytes) and the name is sanitised (the entry's
69
+ * path is dropped; only its base name is kept).
70
+ * - At most `maxModels` models; a ZIP without any model is refused.
71
+ * On any error the models staged so far are discarded.
72
+ */
73
+ export const unpackZip = async (store, zipFile, maxModels) => {
74
+ const models = [];
75
+ const skipped = [];
76
+ let zip = null;
77
+ try {
78
+ try {
79
+ zip = await openZip(zipFile.tmpPath);
80
+ }
81
+ catch (e) {
82
+ throw new UploadRejected(415, `${zipFile.filename}: not a readable ZIP (${e instanceof Error ? e.message : String(e)})`);
83
+ }
84
+ for await (const entry of entries(zip)) {
85
+ const name = entry.fileName;
86
+ if (name.endsWith("/"))
87
+ continue;
88
+ if (isJunk(name) || !isModelName(name)) {
89
+ skipped.push(name);
90
+ continue;
91
+ }
92
+ if (models.length >= maxModels)
93
+ throw new UploadRejected(413, `${zipFile.filename}: more than ${maxModels} model files`);
94
+ const stream = await openEntry(zip, entry);
95
+ const staged = await store.stage(stream, posix.basename(name), () => false);
96
+ models.push(staged);
97
+ }
98
+ if (models.length === 0)
99
+ throw new UploadRejected(415, `${zipFile.filename}: no model file in the ZIP`);
100
+ return { models, skipped };
101
+ }
102
+ catch (e) {
103
+ await Promise.all(models.map((m) => store.discard(m)));
104
+ throw e;
105
+ }
106
+ finally {
107
+ zip?.close();
108
+ await store.discard(zipFile);
109
+ }
110
+ };
111
+ /** Preview images in a 3MF, best first (Bambu Studio / OrcaSlicer, then the 3MF core). */
112
+ const THUMBNAIL_ENTRIES = [
113
+ "Metadata/plate_1.png",
114
+ "Metadata/thumbnail.png",
115
+ "Metadata/plate_1_small.png",
116
+ "Thumbnails/thumbnail.png",
117
+ "Metadata/top_1.png",
118
+ ];
119
+ const MAX_THUMBNAIL_BYTES = 4 * 1024 * 1024;
120
+ const PNG_SIGNATURE = Buffer.from([
121
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
122
+ ]);
123
+ /**
124
+ * The preview PNG embedded in a 3MF, or null. Never throws: a 3MF without
125
+ * a usable preview (or not a valid ZIP at all) just has no thumbnail.
126
+ */
127
+ export const extract3mfThumbnail = async (path) => {
128
+ let zip = null;
129
+ try {
130
+ zip = await openZip(path);
131
+ let best = null;
132
+ let fallback = null;
133
+ for await (const entry of entries(zip)) {
134
+ const rank = THUMBNAIL_ENTRIES.findIndex((n) => n.toLowerCase() === entry.fileName.toLowerCase());
135
+ if (rank >= 0 && (!best || rank < best.rank))
136
+ best = { rank, entry };
137
+ else if (!fallback && /^metadata\/[^/]+\.png$/i.test(entry.fileName))
138
+ fallback = entry;
139
+ }
140
+ const chosen = best?.entry ?? fallback;
141
+ if (!chosen || chosen.uncompressedSize > MAX_THUMBNAIL_BYTES)
142
+ return null;
143
+ // entries() is done, so the ZIP is at its end; openReadStream works on any entry.
144
+ const stream = await openEntry(zip, chosen);
145
+ const chunks = [];
146
+ let size = 0;
147
+ for await (const chunk of stream) {
148
+ size += chunk.length;
149
+ if (size > MAX_THUMBNAIL_BYTES)
150
+ return null;
151
+ chunks.push(chunk);
152
+ }
153
+ const png = Buffer.concat(chunks);
154
+ return png.subarray(0, 8).equals(PNG_SIGNATURE) ? png : null;
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ finally {
160
+ zip?.close();
161
+ }
162
+ };
163
+ /** A name that is unique within one export ZIP: "a.stl", "a (2).stl", … */
164
+ export const uniqueName = (taken, name) => {
165
+ let candidate = name;
166
+ const dot = name.lastIndexOf(".");
167
+ const stem = dot > 0 ? name.slice(0, dot) : name;
168
+ const ext = dot > 0 ? name.slice(dot) : "";
169
+ for (let i = 2; taken.has(candidate.toLowerCase()); i++)
170
+ candidate = `${stem} (${i})${ext}`;
171
+ taken.add(candidate.toLowerCase());
172
+ return candidate;
173
+ };
174
+ /**
175
+ * A ZIP of the given files, as a stream, built while it is sent. 3MF is a
176
+ * ZIP already, so it is stored; everything else is deflated.
177
+ */
178
+ export const exportZip = (store, files) => {
179
+ const out = new yazl.ZipFile();
180
+ const taken = new Set();
181
+ for (const f of files) {
182
+ out.addReadStream(store.read(f.relPath), uniqueName(taken, f.name), {
183
+ compress: f.ext !== "3mf",
184
+ mtime: f.mtime,
185
+ });
186
+ }
187
+ out.end();
188
+ return out.outputStream;
189
+ };
190
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.js","sourceRoot":"","sources":["../../src/server/zip.ts"],"names":[],"mappings":"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"}
@@ -32,4 +32,103 @@ export interface HookAck {
32
32
  stored: true;
33
33
  id: number;
34
34
  }
35
+ /** One stored file of an item. Versions are never changed, only added. */
36
+ export interface Version {
37
+ n: number;
38
+ filename: string;
39
+ ext: string;
40
+ size: number;
41
+ sha256: string;
42
+ comment: string | null;
43
+ uploadedAt: string;
44
+ uploadedBy: string;
45
+ current: boolean;
46
+ /** a preview image (from a 3MF) at …/versions/:n/thumbnail */
47
+ hasThumbnail: boolean;
48
+ }
49
+ /** A row of the list: the item with its current version. */
50
+ export interface Item {
51
+ id: number;
52
+ queueId: number;
53
+ position: number;
54
+ title: string;
55
+ status: ItemStatus;
56
+ statusChangedAt: string;
57
+ needsConfirm: boolean;
58
+ notes: string | null;
59
+ sourceUrl: string | null;
60
+ versionCount: number;
61
+ current: Version | null;
62
+ /** whether Send to Bambuddy applies to the current version (not STEP) */
63
+ sendable: boolean;
64
+ bambuddyFileId: number | null;
65
+ bambuddySentAt: string | null;
66
+ deletedAt: string | null;
67
+ createdAt: string;
68
+ createdBy: string;
69
+ }
70
+ /** GET /api/v1/items/:id: the item with every version, newest first. */
71
+ export interface ItemDetail extends Item {
72
+ versions: Version[];
73
+ /** other items holding the same content as the current version */
74
+ alsoIn: {
75
+ id: number;
76
+ title: string;
77
+ }[];
78
+ }
79
+ /** POST /api/v1/queues/:id/items (multipart): one new item per file. */
80
+ export interface UploadResult {
81
+ items: Item[];
82
+ /** entries of dropped ZIPs that were not imported (not a model, sliced, hidden) */
83
+ skipped: string[];
84
+ }
85
+ /** POST /api/v1/items/:id/versions (multipart, one file). */
86
+ export interface VersionUploadResult {
87
+ item: ItemDetail;
88
+ /** true when the file equals an existing version: nothing new was stored */
89
+ duplicate: boolean;
90
+ version: Version;
91
+ }
92
+ /** One line of an item's history, oldest first. */
93
+ export type HistoryEntry = {
94
+ kind: "status";
95
+ at: string;
96
+ by: string;
97
+ source: "ui" | "webhook";
98
+ from: ItemStatus | null;
99
+ to: ItemStatus;
100
+ detail: Record<string, unknown> | null;
101
+ } | {
102
+ kind: "version";
103
+ at: string;
104
+ by: string;
105
+ n: number;
106
+ filename: string;
107
+ } | {
108
+ kind: "print";
109
+ at: string;
110
+ event: string;
111
+ printer: string | null;
112
+ filename: string | null;
113
+ resolution: string | null;
114
+ };
115
+ /** PATCH /api/v1/items/:id. `move` places the item right after `afterId` (null = top) in `queueId` (default: its queue). */
116
+ export interface ItemPatch {
117
+ title?: string;
118
+ notes?: string | null;
119
+ sourceUrl?: string | null;
120
+ status?: ItemStatus;
121
+ /** why the status changed (kept in the history) */
122
+ reason?: string;
123
+ move?: {
124
+ queueId?: number;
125
+ afterId: number | null;
126
+ };
127
+ }
128
+ /** POST /api/v1/queues, PATCH /api/v1/queues/:id. `afterId` reorders the queue list (null = first). */
129
+ export interface QueueWrite {
130
+ name?: string;
131
+ autoStatus?: boolean;
132
+ afterId?: number | null;
133
+ }
35
134
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AACjE,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EAK9C,CAAC;AAEF,oDAAoD;AACpD,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC;IACnB,kFAAkF;IAClF,QAAQ,EAAE,IAAI,GAAG,aAAa,GAAG,KAAK,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAqB;AACrB,MAAM,WAAW,EAAE;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,yBAAyB;AACzB,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,2BAA2B;AAC3B,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,IAAI,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxpicture/print-backlog",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A backlog for unsliced 3D models (STL, 3MF, STEP) next to Bambuddy: queues, one status per item, versions, drag & drop",
5
5
  "type": "module",
6
6
  "author": "MXPicture",
@@ -31,14 +31,22 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
+ "busboy": "^1.6.0",
34
35
  "dbmate": "2.35.1",
35
36
  "postgres": "^3.4.9",
37
+ "yauzl": "^3.4.0",
38
+ "yazl": "^3.3.1",
36
39
  "zod": "^4.3.6"
37
40
  },
38
41
  "devDependencies": {
39
42
  "@preact/preset-vite": "^2.10.6",
43
+ "@types/busboy": "^1.5.5",
40
44
  "@types/node": "^26.6.6",
45
+ "@types/sortablejs": "^1.15.9",
46
+ "@types/yauzl": "^2.10.3",
47
+ "@types/yazl": "^3.3.1",
41
48
  "preact": "^10.29.8",
49
+ "sortablejs": "^1.15.7",
42
50
  "typescript": "^5.9.3",
43
51
  "vite": "^8.1.5",
44
52
  "vitest": "^4.0.18"
@@ -1 +0,0 @@
1
- (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e,t,n,r,i,a,o,s,c,l,u,d,f,p,m={},h=[],g=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,_=Array.isArray;function v(e,t){for(var n in t)e[n]=t[n];return e}function y(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function b(t,n,r){var i,a,o,s={};for(o in n)o==`key`?i=n[o]:o==`ref`?a=n[o]:s[o]=n[o];if(arguments.length>2&&(s.children=arguments.length>3?e.call(arguments,2):r),typeof t==`function`&&t.defaultProps!=null)for(o in t.defaultProps)s[o]===void 0&&(s[o]=t.defaultProps[o]);return x(t,s,i,a,null)}function x(e,r,i,a,o){var s={type:e,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o??++n,__i:-1,__u:0};return o==null&&t.vnode!=null&&t.vnode(s),s}function S(e){return e.children}function C(e,t){this.props=e,this.context=t}function w(e,t){if(t==null)return e.__?w(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type==`function`?w(e):null}function T(e){if(e.__P&&e.__d){var n=e.__v,r=n.__e,i=[],a=[],o=v({},n);o.__v=n.__v+1,t.vnode&&t.vnode(o),I(e.__P,o,n,e.__n,e.__P.namespaceURI,32&n.__u?[r]:null,i,r??w(n),!!(32&n.__u),a),o.__v=n.__v,o.__.__k[o.__i]=o,te(i,o,a),n.__e=n.__=null,o.__e!=r&&E(o)}}function E(e){if((e=e.__)!=null&&e.__c!=null)return e.__e=e.__c.base=null,e.__k.some(function(t){if(t!=null&&t.__e!=null)return e.__e=e.__c.base=t.__e}),E(e)}function D(e){(!e.__d&&(e.__d=!0)&&r.push(e)&&!O.__r++||i!=t.debounceRendering)&&((i=t.debounceRendering)||a)(O)}function O(){try{for(var e,t=1;r.length;)r.length>t&&r.sort(o),e=r.shift(),t=r.length,T(e)}finally{r.length=O.__r=0}}function k(e,t,n,r,i,a,o,s,c,l,u){var d,f,p,g,_,v,y=r&&r.__k||h,b=t.length;for(c=A(n,t,y,c,b),d=0;d<b;d++)(p=n.__k[d])!=null&&(f=p.__i!=-1&&y[p.__i]||m,p.__i=d,v=I(e,p,f,i,a,o,s,c,l,u),g=p.__e,p.ref&&f.ref!=p.ref&&(f.ref&&R(f.ref,null,p),u.push(p.ref,p.__c||g,p)),_==null&&g!=null&&(_=g),4&p.__u?(c=j(p,c,e),f.__e&&(f.__e=null)):typeof p.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),p.__u&=-7);return n.__e=_,c}function A(e,t,n,r,i){var a,o,s,c,l,u=n.length,d=u,f=0;for(e.__k=Array(i),a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o==`string`||typeof o==`number`||typeof o==`bigint`||o.constructor==String?o=e.__k[a]=x(null,o,null,null,null):_(o)?o=e.__k[a]=x(S,{children:o},null,null,null):o.constructor===void 0&&o.__b>0?o=e.__k[a]=x(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):e.__k[a]=o,c=a+f,o.__=e,o.__b=e.__b+1,s=null,(l=o.__i=M(o,n,c,d))!=-1&&(d--,(s=n[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(i>u?f--:i<u&&f++),typeof o.type!=`function`&&(o.__u|=4)):l!=c&&(l==c-1?f--:l==c+1?f++:(l>c?f--:f++,o.__u|=4))):e.__k[a]=null;if(d)for(a=0;a<u;a++)(s=n[a])!=null&&!(2&s.__u)&&(s.__e==r&&(r=w(s)),re(s,s));return r}function j(e,t,n){var r,i;if(typeof e.type==`function`){for(r=e.__k,i=0;r&&i<r.length;i++)r[i]&&(r[i].__=e,t=j(r[i],t,n));return t}e.__e!=t&&(t&&e.type&&!t.parentNode&&(t=w(e)),t=n.insertBefore(e.__e,t||null));do t&&=t.nextSibling;while(t!=null&&t.nodeType==8);return t}function M(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l!=null&&!(2&l.__u);if(l===null&&s==null||u&&s==l.key&&c==l.type)return n;if(r>+!!u){for(i=n-1,a=n+1;i>=0||a<t.length;)if((l=t[o=i>=0?i--:a++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function N(e,t,n){t[0]==`-`?e.setProperty(t,n??``):e[t]=n==null?``:typeof n!=`number`||g.test(t)?n:n+`px`}function P(e,t,n,r,i){var a,o;n:if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||N(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||N(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)a=t!=(t=t.replace(u,`$1`)),o=t.toLowerCase(),t=o in e||t==`onFocusOut`||t==`onFocusIn`?o.slice(2):t.slice(2),e.l||={},e.l[t+a]=n,n?r?n[l]=r[l]:(n[l]=d,e.addEventListener(t,a?p:f,a)):e.removeEventListener(t,a?p:f,a);else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function F(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n[c]==null)n[c]=d++;else if(n[c]<r[l])return;return r(t.event?t.event(n):n)}}}function I(e,n,r,i,a,o,s,c,l,u){var d,f,p,m,g,b,x,T,E,D,O,A,j,M,N,P,F=n.type;if(n.constructor!==void 0)return null;128&r.__u&&(l=!!(32&r.__u),o=[c=n.__e=r.__e]),(d=t.__b)&&d(n);n:if(typeof F==`function`){f=s.length;try{if(E=n.props,D=F.prototype&&F.prototype.render,O=(d=F.contextType)&&i[d.__c],A=d?O?O.props.value:d.__:i,r.__c?T=(p=n.__c=r.__c).__=p.__E:(D?n.__c=p=new F(E,A):(n.__c=p=new C(E,A),p.constructor=F,p.render=ie),O&&O.sub(p),p.state||(p.state={}),p.__n=i,m=p.__d=!0,p.__h=[],p._sb=[]),D&&p.__s==null&&(p.__s=p.state),D&&F.getDerivedStateFromProps!=null&&(p.__s==p.state&&(p.__s=v({},p.__s)),v(p.__s,F.getDerivedStateFromProps(E,p.__s))),g=p.props,b=p.state,p.__v=n,m)D&&F.getDerivedStateFromProps==null&&p.componentWillMount!=null&&p.componentWillMount(),D&&p.componentDidMount!=null&&p.__h.push(p.componentDidMount);else{if(D&&F.getDerivedStateFromProps==null&&E!==g&&p.componentWillReceiveProps!=null&&p.componentWillReceiveProps(E,A),n.__v==r.__v||!p.__e&&p.shouldComponentUpdate!=null&&!1===p.shouldComponentUpdate(E,p.__s,A)){n.__v!=r.__v&&(p.props=E,p.state=p.__s,p.__d=!1),n.__e=r.__e,n.__k=r.__k,n.__k.some(function(e){e&&(e.__=n)}),h.push.apply(p.__h,p._sb),p._sb=[],p.__h.length&&s.push(p),c=w(r);break n}p.componentWillUpdate!=null&&p.componentWillUpdate(E,p.__s,A),D&&p.componentDidUpdate!=null&&p.__h.push(function(){p.componentDidUpdate(g,b,x)})}if(p.context=A,p.props=E,p.__P=e,p.__e=!1,j=t.__r,M=0,D)p.state=p.__s,p.__d=!1,j&&j(n),d=p.render(p.props,p.state,p.context),h.push.apply(p.__h,p._sb),p._sb=[];else do p.__d=!1,j&&j(n),d=p.render(p.props,p.state,p.context),p.state=p.__s;while(p.__d&&++M<25);p.state=p.__s,p.getChildContext!=null&&(i=v(v({},i),p.getChildContext())),D&&!m&&p.getSnapshotBeforeUpdate!=null&&(x=p.getSnapshotBeforeUpdate(g,b)),N=d!=null&&d.type===S&&d.key==null?L(d.props.children):d,c=k(e,_(N)?N:[N],n,r,i,a,o,s,c,l,u),p.base=n.__e,n.__u&=-161,p.__h.length&&s.push(p),T&&(p.__E=p.__=null)}catch(e){if(s.length=f,n.__v=null,l||o!=null){if(e.then){for(n.__u|=l?160:128;c&&c.nodeType==8&&c.nextSibling;)c=c.nextSibling;o!=null&&(o[o.indexOf(c)]=null),n.__e=c}else if(o!=null)for(P=o.length;P--;)y(o[P])}else n.__e=r.__e;n.__k??=r.__k||[],e.then||ee(n),t.__e(e,n,r)}}else o==null&&n.__v==r.__v?(n.__k=r.__k,n.__e=r.__e):c=n.__e=ne(r.__e,n,r,i,a,o,s,l,u);return(d=t.diffed)&&d(n),128&n.__u?void 0:c}function ee(e){e&&(e.__c&&(e.__c.__e=!0),e.__k&&e.__k.some(ee))}function te(e,n,r){for(var i=0;i<r.length;i++)R(r[i],r[++i],r[++i]);t.__c&&t.__c(n,e),e.some(function(n){try{e=n.__h,n.__h=[],e.some(function(e){e.call(n)})}catch(e){t.__e(e,n.__v)}})}function L(e){return typeof e!=`object`||!e||e.__b>0?e:_(e)?e.map(L):e.constructor===void 0?v({},e):null}function ne(n,r,i,a,o,s,c,l,u){var d,f,p,h,g,v,b,x=i.props||m,S=r.props,C=r.type;if(C==`svg`?o=`http://www.w3.org/2000/svg`:C==`math`?o=`http://www.w3.org/1998/Math/MathML`:o||=`http://www.w3.org/1999/xhtml`,s!=null){for(d=0;d<s.length;d++)if((g=s[d])&&`setAttribute`in g==!!C&&(C?g.localName==C:g.nodeType==3)){n=g,s[d]=null;break}}if(n==null){if(C==null)return document.createTextNode(S);n=document.createElementNS(o,C,S.is&&S),l&&=(t.__m&&t.__m(r,s),!1),s=null}if(C==null)x===S||l&&n.data==S||(n.data=S);else{if(s=C==`textarea`&&S.defaultValue!=null?null:s&&e.call(n.childNodes),!l&&s!=null)for(x={},d=0;d<n.attributes.length;d++)x[(g=n.attributes[d]).name]=g.value;for(d in x)g=x[d],d==`dangerouslySetInnerHTML`?p=g:d==`children`||d in S||d==`value`&&`defaultValue`in S||d==`checked`&&`defaultChecked`in S||P(n,d,null,g,o);for(d in S)g=S[d],d==`children`?h=g:d==`dangerouslySetInnerHTML`?f=g:d==`value`?v=g:d==`checked`?b=g:l&&typeof g!=`function`||x[d]===g||P(n,d,g,x[d],o);if(f)l||p&&(f.__html==p.__html||f.__html==n.innerHTML)||(n.innerHTML=f.__html),r.__k=[];else if(p&&(n.innerHTML=``),k(r.type==`template`?n.content:n,_(h)?h:[h],r,i,a,C==`foreignObject`?`http://www.w3.org/1999/xhtml`:o,s,c,s?s[0]:i.__k&&w(i,0),l,u),s!=null)for(d=s.length;d--;)y(s[d]);l&&C!=`textarea`||(d=`value`,C==`progress`&&v==null?n.removeAttribute(`value`):v!=null&&(v!==n[d]||C==`progress`&&!v||C==`option`&&v!=x[d])&&P(n,d,v,x[d],o),d=`checked`,b!=null&&b!=n[d]&&P(n,d,b,x[d],o))}return n}function R(e,n,r){try{if(typeof e==`function`){var i=typeof e.__u==`function`;i&&e.__u(),i&&n==null||(e.__u=e(n))}else e.current=n}catch(e){t.__e(e,r)}}function re(e,n,r){var i,a;if(t.unmount&&t.unmount(e),(i=e.ref)&&(i.current&&i.current!=e.__e||R(i,null,n)),(i=e.__c)!=null){if(i.componentWillUnmount)try{i.componentWillUnmount()}catch(e){t.__e(e,n)}i.base=i.__P=i.__n=null}if(i=e.__k)for(a=0;a<i.length;a++)i[a]&&re(i[a],n,r||typeof e.type!=`function`);r||y(e.__e),e.__c=e.__=e.__e=void 0}function ie(e,t,n){return this.constructor(e,n)}function ae(n,r,i){var a,o,s,c;r==document&&(r=document.documentElement),t.__&&t.__(n,r),o=(a=typeof i==`function`)?null:i&&i.__k||r.__k,s=[],c=[],I(r,n=(!a&&i||r).__k=b(S,null,[n]),o||m,m,r.namespaceURI,!a&&i?[i]:o?null:r.firstChild?e.call(r.childNodes):null,s,!a&&i?i:o?o.__e:r.firstChild,a,c),te(s,n,c),n.props.children=null}e=h.slice,t={__e:function(e,t,n,r){for(var i,a,o;t=t.__;)if((i=t.__c)&&!i.__)try{if((a=i.constructor)&&a.getDerivedStateFromError!=null&&(i.setState(a.getDerivedStateFromError(e)),o=i.__d),i.componentDidCatch!=null&&(i.componentDidCatch(e,r||{}),o=i.__d),o)return i.__E=i}catch(t){e=t}throw e}},n=0,C.prototype.setState=function(e,t){var n=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=v({},this.state);typeof e==`function`&&(e=e(v({},n),this.props)),e&&v(n,e),e!=null&&this.__v&&(t&&this._sb.push(t),D(this))},C.prototype.forceUpdate=function(e){this.__v&&(this.__e=!0,e&&this.__h.push(e),D(this))},C.prototype.render=S,r=[],a=typeof Promise==`function`?Promise.prototype.then.bind(Promise.resolve()):setTimeout,o=function(e,t){return e.__v.__b-t.__v.__b},O.__r=0,s=Math.random().toString(8),c=`__d`+s,l=`__a`+s,u=/(PointerCapture)$|Capture$/i,d=0,f=F(!1),p=F(!0);var z,B,V,H,U=0,W=[],G=t,K=G.__b,q=G.__r,J=G.diffed,oe=G.__c,se=G.unmount,ce=G.__;function le(e,t){G.__h&&G.__h(B,e,U||t),U=0;var n=B.__H||(B.__H={__:[],__h:[]});return e>=n.__.length&&n.__.push({}),n.__[e]}function Y(e){return U=1,ue(he,e)}function ue(e,t,n){var r=le(z++,2);if(r.t=e,!r.__c&&(r.__=[n?n(t):he(void 0,t),function(e){var t=r.__N?r.__N[0]:r.__[0],n=r.t(t,e);t!==n&&(r.__N=[n,r.__[1]],r.__c.setState({}))}],r.__c=B,!B.__f)){var i=function(e,t,n){if(!r.__c.__H)return!0;var i=!1,o=r.__c.props!==e;if(r.__c.__H.__.some(function(e){if(e.__N){i=!0;var t=e.__[0];e.__=e.__N,e.__N=void 0,t!==e.__[0]&&(o=!0)}}),a){var s=a.call(this,e,t,n);return i?s||o:s}return!i||o};B.__f=!0;var a=B.shouldComponentUpdate,o=B.componentWillUpdate;B.componentWillUpdate=function(e,t,n){if(this.__e){var r=a;a=void 0,i(e,t,n),a=r}o&&o.call(this,e,t,n)},B.shouldComponentUpdate=i}return r.__N||r.__}function de(e,t){var n=le(z++,3);!G.__s&&me(n.__H,t)&&(n.__=e,n.u=t,B.__H.__h.push(n))}function fe(){for(var e;e=W.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Z),t.__h.some(Q),t.__h=[]}catch(n){t.__h=[],G.__e(n,e.__v)}}}G.__b=function(e){B=null,K&&K(e)},G.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),ce&&ce(e,t)},G.__r=function(e){q&&q(e),z=0;var t=(B=e.__c).__H;t&&(V===B?(t.__h=[],B.__h=[],t.__.some(function(e){e.__N&&(e.__=e.__N),e.u=e.__N=void 0})):(t.__h.some(Z),t.__h.some(Q),t.__h=[],z=0)),V=B},G.diffed=function(e){J&&J(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(W.push(t)!==1&&H===G.requestAnimationFrame||((H=G.requestAnimationFrame)||pe)(fe)),t.__H.__.some(function(e){e.u&&=(e.__H=e.u,void 0)})),V=B=null},G.__c=function(e,t){t.some(function(e){try{e.__h.some(Z),e.__h=e.__h.filter(function(e){return!e.__||Q(e)})}catch(n){t.some(function(e){e.__h&&=[]}),t=[],G.__e(n,e.__v)}}),oe&&oe(e,t)},G.unmount=function(e){se&&se(e);var t,n=e.__c;n&&n.__H&&(n.__H.__.some(function(e){try{Z(e)}catch(e){t=e}}),n.__H=void 0,t&&G.__e(t,n.__v))};var X=typeof requestAnimationFrame==`function`;function pe(e){var t,n=function(){clearTimeout(r),X&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(n,35);X&&(t=requestAnimationFrame(n))}function Z(e){var t=B,n=e.__c;typeof n==`function`&&(e.__c=void 0,n()),B=t}function Q(e){var t=B;e.__c=e.__(),B=t}function me(e,t){return!e||e.length!==t.length||t.some(function(t,n){return t!==e[n]})}function he(e,t){return typeof t==`function`?t(e):t}var ge=[`queued`,`skipped`,`done`,`error`],_e=async(e,t={})=>{let n=await fetch(e,{...t,redirect:`manual`,headers:{Accept:`application/json`,...t.headers}});if(n.type===`opaqueredirect`)throw location.reload(),Error(`session expired`);let r=await n.json().catch(()=>null);if(!n.ok)throw Error(r?.error??`HTTP ${n.status}`);return r},ve={me:()=>_e(`/api/v1/me`),queues:()=>_e(`/api/v1/queues`)},ye=0;Array.isArray;function $(e,n,r,i,a,o){n||={};var s,c,l=n;if(`ref`in l)for(c in l={},n)c==`ref`?s=n[c]:l[c]=n[c];var u={type:e,props:l,key:r,ref:s,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--ye,__i:-1,__u:0,__source:a,__self:o};if(typeof e==`function`&&(s=e.defaultProps))for(c in s)l[c]===void 0&&(l[c]=s[c]);return t.vnode&&t.vnode(u),u}ae($(()=>{let[e,t]=Y(null),[n,r]=Y(null),[i,a]=Y(null);return de(()=>{Promise.all([ve.me(),ve.queues()]).then(([e,n])=>{t(e),r(n)}).catch(e=>a(e instanceof Error?e.message:String(e)))},[]),$(`main`,{children:[$(`header`,{children:[$(`h1`,{children:`Print Backlog`}),$(`span`,{class:`muted`,children:e?`${e.user} · v${e.version}`:``})]}),i&&$(`p`,{class:`error`,children:i}),n?.map(e=>$(`section`,{class:`queue`,children:[$(`h2`,{children:e.name}),$(`ul`,{class:`counts`,children:ge.map(t=>$(`li`,{class:`status-${t}`,children:[t,` `,$(`b`,{children:e.counts[t]})]},t))})]},e.id))]})},{}),document.getElementById(`app`));
@@ -1 +0,0 @@
1
- :root{--bg:#fff;--fg:#0f172a;--muted:#64748b;--line:#e2e8f0;--accent:#2563eb;--error:#dc2626;color:var(--fg);background:var(--bg);font-family:system-ui,sans-serif}@media (prefers-color-scheme:dark){:root{--bg:#0b1120;--fg:#e2e8f0;--muted:#94a3b8;--line:#1e293b;--accent:#60a5fa;--error:#f87171}}body{background:var(--bg);margin:0}main{max-width:60rem;margin:0 auto;padding:1rem}header{border-bottom:1px solid var(--line);justify-content:space-between;align-items:baseline;gap:1rem;display:flex}h1{font-size:1.25rem}h2{font-size:1rem}.muted{color:var(--muted)}.error{color:var(--error)}.counts{flex-wrap:wrap;gap:.5rem;padding:0;list-style:none;display:flex}.counts li{border:1px solid var(--line);border-radius:999px;padding:.2rem .7rem}