feat: Wire Supply Chain BOM and 3D Print Farm modules to Postgres and REST API
This commit is contained in:
parent
79559ba6f0
commit
6f06dd3782
4
main.ts
4
main.ts
@ -2,6 +2,8 @@ import { Hono } from "hono";
|
|||||||
import { serveStatic } from "hono/deno";
|
import { serveStatic } from "hono/deno";
|
||||||
import { sql, initDb } from "./src/db/connection.ts";
|
import { sql, initDb } from "./src/db/connection.ts";
|
||||||
import { qaRoutes } from "./src/routes/qa.ts";
|
import { qaRoutes } from "./src/routes/qa.ts";
|
||||||
|
import { bomRoutes } from "./src/routes/bom.ts";
|
||||||
|
import { printFarmRoutes } from "./src/routes/printfarm.ts";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ app.get("/api/status", async (c) => {
|
|||||||
|
|
||||||
// Mount modular sub-routes
|
// Mount modular sub-routes
|
||||||
app.route("/api/qa", qaRoutes);
|
app.route("/api/qa", qaRoutes);
|
||||||
|
app.route("/api/bom", bomRoutes);
|
||||||
|
app.route("/api/print-farm", printFarmRoutes);
|
||||||
|
|
||||||
// Serve the 3D Viewer frontend
|
// Serve the 3D Viewer frontend
|
||||||
app.use("/*", serveStatic({ root: "./" }));
|
app.use("/*", serveStatic({ root: "./" }));
|
||||||
|
|||||||
50
src/db/bom.ts
Normal file
50
src/db/bom.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { sql } from "./connection.ts";
|
||||||
|
|
||||||
|
export interface BomItem {
|
||||||
|
id: string;
|
||||||
|
component_name: string;
|
||||||
|
mpn: string;
|
||||||
|
vendor: string;
|
||||||
|
unit_price: number;
|
||||||
|
quantity: number;
|
||||||
|
lead_time_days: number;
|
||||||
|
status: "in_stock" | "ordered" | "backordered";
|
||||||
|
category: "compute" | "storage" | "power" | "mechanical" | "cables" | "fasteners";
|
||||||
|
notes?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBomItems = async (category?: string): Promise<BomItem[]> => {
|
||||||
|
if (category) {
|
||||||
|
return await sql<BomItem[]>`
|
||||||
|
SELECT * FROM bom_items WHERE category = ${category} ORDER BY category, component_name ASC
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return await sql<BomItem[]>`
|
||||||
|
SELECT * FROM bom_items ORDER BY category, component_name ASC
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBomSummary = async () => {
|
||||||
|
const result = await sql`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_parts,
|
||||||
|
SUM(quantity) as total_units,
|
||||||
|
SUM(unit_price * quantity) as total_cost,
|
||||||
|
MAX(lead_time_days) as max_lead_time,
|
||||||
|
COUNT(CASE WHEN status = 'in_stock' THEN 1 END) as in_stock_count,
|
||||||
|
COUNT(CASE WHEN status = 'ordered' THEN 1 END) as ordered_count,
|
||||||
|
COUNT(CASE WHEN status = 'backordered' THEN 1 END) as backordered_count
|
||||||
|
FROM bom_items
|
||||||
|
`;
|
||||||
|
return result[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBomItemStatus = async (id: string, status: string): Promise<BomItem[]> => {
|
||||||
|
return await sql<BomItem[]>`
|
||||||
|
UPDATE bom_items
|
||||||
|
SET status = ${status}, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ${id}
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
};
|
||||||
@ -14,6 +14,7 @@ export const sql = postgres({
|
|||||||
export async function initDb(retries = 5, delayMs = 2000) {
|
export async function initDb(retries = 5, delayMs = 2000) {
|
||||||
for (let i = 0; i < retries; i++) {
|
for (let i = 0; i < retries; i++) {
|
||||||
try {
|
try {
|
||||||
|
// 1. QA Checklists
|
||||||
await sql`
|
await sql`
|
||||||
CREATE TABLE IF NOT EXISTS qa_checklists (
|
CREATE TABLE IF NOT EXISTS qa_checklists (
|
||||||
id VARCHAR(50) PRIMARY KEY,
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
@ -23,17 +24,87 @@ export async function initDb(retries = 5, delayMs = 2000) {
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const count = await sql`SELECT count(*) FROM qa_checklists`;
|
const qaCount = await sql`SELECT count(*) FROM qa_checklists`;
|
||||||
if (count[0].count === "0") {
|
if (qaCount[0].count === "0") {
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO qa_checklists (id, label, is_completed) VALUES
|
INSERT INTO qa_checklists (id, label, is_completed) VALUES
|
||||||
('qa-cap', 'Supercap mounted & isolated', false),
|
('qa-inserts', 'Thermal insertion of 56x M3/M4 brass threaded inserts into 3D facade & fan wall', false),
|
||||||
('qa-sas', 'Broadcom SAS seated in PCIe', false),
|
('qa-frame', '2020 Aluminum frame assembly and squaring (Torque M5 bolts to 4.5 N·m)', false),
|
||||||
('qa-cool', 'Waterblock pressure tested', false),
|
('qa-fans', '3x 120mm PWM fan wall installation with EPDM pneumatic perimeter gasket', false),
|
||||||
('qa-zfs', 'ZFS pool scrub baseline passed', false)
|
('qa-pcba', 'Baseboard PCBA mounting and MCIO 8i twinaxial cable routing', false),
|
||||||
|
('qa-skins', 'SendCutSend 5052 aluminum skins fastening & 6U rack ear installation', false),
|
||||||
|
('qa-supercap', 'Supercapacitor PLP holdup validation (LTC3350 PFI) & Rust firmware flashing', false)
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
console.log("✅ Database schema initialized successfully");
|
|
||||||
|
// 2. Bill of Materials (BOM) & Supply Chain
|
||||||
|
await sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS bom_items (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
component_name TEXT NOT NULL,
|
||||||
|
mpn VARCHAR(100) NOT NULL,
|
||||||
|
vendor VARCHAR(100) NOT NULL,
|
||||||
|
unit_price NUMERIC(10,2) NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL DEFAULT 1,
|
||||||
|
lead_time_days INTEGER NOT NULL DEFAULT 0,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'in_stock',
|
||||||
|
category VARCHAR(50) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const bomCount = await sql`SELECT count(*) FROM bom_items`;
|
||||||
|
if (bomCount[0].count === "0") {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO bom_items (id, component_name, mpn, vendor, unit_price, quantity, lead_time_days, status, category, notes) VALUES
|
||||||
|
('bom-cm3i', 'Radxa CM3I SoM (4G/16G)', 'CM3I-4G16G', 'Ameridroid', 38.00, 1, 3, 'in_stock', 'compute', 'Rockchip RK3568 quad-core host'),
|
||||||
|
('bom-sas3408', 'Broadcom SAS3408 Tri-Mode IOC', 'SAS3408-BGA484', 'Avnet / Arrow', 32.00, 1, 14, 'ordered', 'storage', 'PCIe 3.1 x8 to 8-PHY SAS/SATA/NVMe'),
|
||||||
|
('bom-rp2040', 'RP2040 Dual MCU', 'RP2040-QFN56', 'DigiKey', 0.95, 1, 2, 'in_stock', 'compute', 'OpenBMC & hardware supervisor'),
|
||||||
|
('bom-ltc3350', 'LTC3350 Supercap Manager IC', 'LTC3350EUHF#PBF', 'Mouser', 7.45, 1, 5, 'in_stock', 'power', 'Supercap PLP buck-boost balancer'),
|
||||||
|
('bom-supercaps', 'Eaton 10.0F 2.7V Supercapacitors', 'B0510-2R7105-R', 'Mouser', 4.40, 4, 2, 'in_stock', 'power', '4-cell series stack (10.8V holdup)'),
|
||||||
|
('bom-efuse', 'TPS25982 15A Smart eFuse', 'TPS259827ONRGER', 'DigiKey', 1.85, 2, 2, 'in_stock', 'power', 'Hot-swap inrush limiter & fault clamp'),
|
||||||
|
('bom-diode', 'LM74700 Ideal Diode Controller', 'LM74700QDBVRQ1', 'Mouser', 0.95, 2, 2, 'in_stock', 'power', 'Reverse polarity & low-loss ORing'),
|
||||||
|
('bom-mcio', 'Amphenol MCIO 8i Connectors (74-Pin)', 'G88MP08102CEEU', 'DigiKey', 7.60, 2, 4, 'in_stock', 'cables', 'PCIe Gen4 / SAS4 high-density interconnect'),
|
||||||
|
('bom-u3', 'Amphenol U.3 SFF-TA-1001 Sockets', '10148784-101LF', 'Mouser', 11.20, 8, 7, 'in_stock', 'storage', 'Tri-mode SAS/SATA/NVMe drive bays'),
|
||||||
|
('bom-pcb-base', 'Carrier Baseboard 6-Layer PCBA', 'PCB-BASE-01', 'MacroFab', 16.50, 1, 12, 'ordered', 'mechanical', 'Controlled impedance ENIG finish'),
|
||||||
|
('bom-pcb-bp', 'Passive U.3 Drive Backplane PCBA', 'PCB-BP-01', 'MacroFab', 12.50, 1, 12, 'ordered', 'mechanical', '2oz copper power distribution plane'),
|
||||||
|
('bom-2020', '2020 T-Slot Aluminum Extrusions', '20-2020-KIT', '80/20 Inc.', 14.00, 1, 3, 'in_stock', 'mechanical', 'Anodized 6063-T6 framing'),
|
||||||
|
('bom-skins', 'SendCutSend 5052 Aluminum Outer Panels', 'SMP-ALL-01', 'SendCutSend', 26.80, 1, 4, 'in_stock', 'mechanical', 'Laser cut 2.0mm 5052-H32 with countersinks'),
|
||||||
|
('bom-fasteners', 'Black Oxide M3/M4 Fastener & Insert Kit', 'FAST-KIT-01', 'McMaster-Carr', 16.00, 1, 1, 'in_stock', 'fasteners', 'Class 12.9 alloy steel hardware')
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 3D Print Farm Jobs
|
||||||
|
await sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS print_farm_jobs (
|
||||||
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
|
part_name TEXT NOT NULL,
|
||||||
|
printer_model VARCHAR(50) NOT NULL,
|
||||||
|
material VARCHAR(30) NOT NULL,
|
||||||
|
print_time_hours NUMERIC(4,1) NOT NULL,
|
||||||
|
weight_grams INTEGER NOT NULL,
|
||||||
|
cost_estimate NUMERIC(6,2) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'queued',
|
||||||
|
bed_fit VARCHAR(30) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const pfCount = await sql`SELECT count(*) FROM print_farm_jobs`;
|
||||||
|
if (pfCount[0].count === "0") {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO print_farm_jobs (id, part_name, printer_model, material, print_time_hours, weight_grams, cost_estimate, status, bed_fit, notes) VALUES
|
||||||
|
('pf-facade', '6U Hexagonal Intake Airflow Facade', 'Prusa Core One (L)', 'Prusament PETG V0', 14.5, 412, 14.20, 'completed', 'Fit (298x298mm)', 'Flame retardant UL94-V0 compliance'),
|
||||||
|
('pf-fanwall', '3x 120mm Isolated PWM Fan Duct Wall', 'Bambu Lab X1-Carbon', 'Polymaker PA6-CF', 8.2, 245, 18.50, 'printing', 'Fit (250x250mm)', 'Carbon fiber stiffness to eliminate harmonic resonance'),
|
||||||
|
('pf-caddy-1-4', 'U.3 Toolless Drive Caddies (Bays 1-4)', 'Voron 2.4 350', 'eSUN ABS+', 6.8, 180, 5.40, 'queued', 'Batch Fit (350x350mm)', 'Spring-loaded latch with light pipe inserts'),
|
||||||
|
('pf-caddy-5-8', 'U.3 Toolless Drive Caddies (Bays 5-8)', 'Voron 2.4 350', 'eSUN ABS+', 6.8, 180, 5.40, 'queued', 'Batch Fit (350x350mm)', 'Spring-loaded latch with light pipe inserts'),
|
||||||
|
('pf-shroud', 'Radxa CM3I & SAS3408 Passive Air Shroud', 'Bambu Lab X1-Carbon', 'Prusament PC-Blend', 4.1, 115, 8.90, 'queued', 'Fit (140x110mm)', '115°C HDT rating for high thermal exhaust ducting')
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ All database schemas and seed data initialized successfully");
|
||||||
return;
|
return;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.warn(`⚠️ Database init attempt ${i + 1}/${retries} failed: ${err.message}`);
|
console.warn(`⚠️ Database init attempt ${i + 1}/${retries} failed: ${err.message}`);
|
||||||
|
|||||||
37
src/db/printfarm.ts
Normal file
37
src/db/printfarm.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { sql } from "./connection.ts";
|
||||||
|
|
||||||
|
export interface PrintJob {
|
||||||
|
id: string;
|
||||||
|
part_name: string;
|
||||||
|
printer_model: string;
|
||||||
|
material: string;
|
||||||
|
print_time_hours: number;
|
||||||
|
weight_grams: number;
|
||||||
|
cost_estimate: number;
|
||||||
|
status: "completed" | "printing" | "queued";
|
||||||
|
bed_fit: string;
|
||||||
|
notes?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPrintJobs = async (): Promise<PrintJob[]> => {
|
||||||
|
return await sql<PrintJob[]>`
|
||||||
|
SELECT * FROM print_farm_jobs ORDER BY
|
||||||
|
CASE status
|
||||||
|
WHEN 'printing' THEN 1
|
||||||
|
WHEN 'queued' THEN 2
|
||||||
|
WHEN 'completed' THEN 3
|
||||||
|
ELSE 4
|
||||||
|
END,
|
||||||
|
id ASC
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePrintJobStatus = async (id: string, status: string): Promise<PrintJob[]> => {
|
||||||
|
return await sql<PrintJob[]>`
|
||||||
|
UPDATE print_farm_jobs
|
||||||
|
SET status = ${status}, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ${id}
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
};
|
||||||
38
src/routes/bom.ts
Normal file
38
src/routes/bom.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { getBomItems, getBomSummary, updateBomItemStatus } from "../db/bom.ts";
|
||||||
|
|
||||||
|
export const bomRoutes = new Hono();
|
||||||
|
|
||||||
|
bomRoutes.get("/", async (c) => {
|
||||||
|
try {
|
||||||
|
const category = c.req.query("category");
|
||||||
|
const items = await getBomItems(category);
|
||||||
|
return c.json(items);
|
||||||
|
} catch (error: any) {
|
||||||
|
return c.json({ error: error.message }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bomRoutes.get("/summary", async (c) => {
|
||||||
|
try {
|
||||||
|
const summary = await getBomSummary();
|
||||||
|
return c.json(summary);
|
||||||
|
} catch (error: any) {
|
||||||
|
return c.json({ error: error.message }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bomRoutes.put("/:id/status", async (c) => {
|
||||||
|
const id = c.req.param("id");
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
if (!body.status) {
|
||||||
|
return c.json({ error: "Missing status field" }, 400);
|
||||||
|
}
|
||||||
|
const updated = await updateBomItemStatus(id, body.status);
|
||||||
|
if (updated.length === 0) return c.json({ error: "BOM item not found" }, 404);
|
||||||
|
return c.json(updated[0]);
|
||||||
|
} catch (error: any) {
|
||||||
|
return c.json({ error: error.message }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
28
src/routes/printfarm.ts
Normal file
28
src/routes/printfarm.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { getPrintJobs, updatePrintJobStatus } from "../db/printfarm.ts";
|
||||||
|
|
||||||
|
export const printFarmRoutes = new Hono();
|
||||||
|
|
||||||
|
printFarmRoutes.get("/", async (c) => {
|
||||||
|
try {
|
||||||
|
const jobs = await getPrintJobs();
|
||||||
|
return c.json(jobs);
|
||||||
|
} catch (error: any) {
|
||||||
|
return c.json({ error: error.message }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
printFarmRoutes.put("/:id/status", async (c) => {
|
||||||
|
const id = c.req.param("id");
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
if (!body.status) {
|
||||||
|
return c.json({ error: "Missing status field" }, 400);
|
||||||
|
}
|
||||||
|
const updated = await updatePrintJobStatus(id, body.status);
|
||||||
|
if (updated.length === 0) return c.json({ error: "Job not found" }, 404);
|
||||||
|
return c.json(updated[0]);
|
||||||
|
} catch (error: any) {
|
||||||
|
return c.json({ error: error.message }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
87
viewer/bom.js
Normal file
87
viewer/bom.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// BOM & Supply Chain API Client
|
||||||
|
let liveBomData = [];
|
||||||
|
|
||||||
|
async function loadBomData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/bom');
|
||||||
|
if (!res.ok) throw new Error("Failed to load BOM data");
|
||||||
|
liveBomData = await res.json();
|
||||||
|
|
||||||
|
renderBomTable();
|
||||||
|
updateBOMCalculations();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("BOM Loading Error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBomTable() {
|
||||||
|
const tbody = document.getElementById('bom-table-body');
|
||||||
|
if (!tbody) return;
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
liveBomData.forEach(item => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
|
||||||
|
let statusBadge = '<span style="color:var(--accent-emerald); font-weight:600;"><i class="fa-solid fa-check"></i> In Stock</span>';
|
||||||
|
if (item.status === 'ordered') {
|
||||||
|
statusBadge = `<span style="color:var(--accent-amber); font-weight:600;"><i class="fa-solid fa-clock"></i> Ordered (${item.lead_time_days}d)</span>`;
|
||||||
|
} else if (item.status === 'backordered') {
|
||||||
|
statusBadge = `<span style="color:var(--accent-rose); font-weight:600;"><i class="fa-solid fa-triangle-exclamation"></i> Backorder (${item.lead_time_days}d)</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>
|
||||||
|
<strong>${item.component_name}</strong>
|
||||||
|
<div style="font-size:0.65rem; color:var(--text-muted);">${item.notes || ''}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${item.vendor}
|
||||||
|
<div>${statusBadge}</div>
|
||||||
|
</td>
|
||||||
|
<td><code style="color:var(--accent-cyan);">${item.mpn}</code></td>
|
||||||
|
<td style="font-weight:700; color:var(--accent-emerald);">$${parseFloat(item.unit_price).toFixed(2)}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBOMCalculations() {
|
||||||
|
const batchSlider = document.getElementById('batch-slider');
|
||||||
|
const batchQty = batchSlider ? parseInt(batchSlider.value) : 100;
|
||||||
|
const batchLabel = document.getElementById('batch-qty-label');
|
||||||
|
if (batchLabel) batchLabel.innerText = `${batchQty} Units`;
|
||||||
|
|
||||||
|
let discountFactor = 1.0;
|
||||||
|
if (batchQty >= 1000) discountFactor = 0.85;
|
||||||
|
else if (batchQty >= 100) discountFactor = 0.92;
|
||||||
|
else if (batchQty >= 10) discountFactor = 0.96;
|
||||||
|
|
||||||
|
const baseTotal = liveBomData.length > 0
|
||||||
|
? liveBomData.reduce((acc, i) => acc + (parseFloat(i.unit_price) * (i.quantity || 1)), 0)
|
||||||
|
: 241.20;
|
||||||
|
|
||||||
|
const unitCost = baseTotal * discountFactor;
|
||||||
|
const totalBatch = unitCost * batchQty;
|
||||||
|
|
||||||
|
const unitEl = document.getElementById('rollup-unit-cost');
|
||||||
|
const batchEl = document.getElementById('rollup-total-cost');
|
||||||
|
if (unitEl) unitEl.innerText = `$${unitCost.toFixed(2)}`;
|
||||||
|
if (batchEl) batchEl.innerText = `$${totalBatch.toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2})}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportBOMCSV() {
|
||||||
|
let csv = "Component,MPN,Supplier,Quantity,UnitPriceUSD,Status,Notes\n";
|
||||||
|
liveBomData.forEach(i => {
|
||||||
|
csv += `"${i.component_name}","${i.mpn}","${i.vendor}",${i.quantity || 1},${parseFloat(i.unit_price).toFixed(2)},"${i.status}","${i.notes || ''}"\n`;
|
||||||
|
});
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('href', url);
|
||||||
|
a.setAttribute('download', '6U_Storage_Array_Master_PO.csv');
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.loadBomData = loadBomData;
|
||||||
|
window.updateBOMCalculations = updateBOMCalculations;
|
||||||
|
window.exportBOMCSV = exportBOMCSV;
|
||||||
@ -923,6 +923,8 @@
|
|||||||
|
|
||||||
<!-- Master Interactive JavaScript Application -->
|
<!-- Master Interactive JavaScript Application -->
|
||||||
<script src="./qa.js"></script>
|
<script src="./qa.js"></script>
|
||||||
|
<script src="./bom.js"></script>
|
||||||
|
<script src="./printfarm.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let scene, camera, renderer, controls;
|
let scene, camera, renderer, controls;
|
||||||
let groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupIsolatedPart;
|
let groupSkeleton, groupSheets, groupFacade, groupDrives, groupFans, groupPcba, groupIsolatedPart;
|
||||||
@ -2223,6 +2225,8 @@
|
|||||||
|
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
loadQaChecklist();
|
loadQaChecklist();
|
||||||
|
loadBomData();
|
||||||
|
loadPrintFarmData();
|
||||||
init3D();
|
init3D();
|
||||||
updatePrintFarmStats();
|
updatePrintFarmStats();
|
||||||
updateMaterialPhysics();
|
updateMaterialPhysics();
|
||||||
|
|||||||
61
viewer/printfarm.js
Normal file
61
viewer/printfarm.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// 3D Print Farm API Client
|
||||||
|
let livePrintJobs = [];
|
||||||
|
|
||||||
|
async function loadPrintFarmData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/print-farm');
|
||||||
|
if (!res.ok) throw new Error("Failed to load Print Farm data");
|
||||||
|
livePrintJobs = await res.json();
|
||||||
|
|
||||||
|
renderPrintFarmQueue();
|
||||||
|
updatePrintFarmStats();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Print Farm Loading Error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPrintFarmQueue() {
|
||||||
|
let container = document.getElementById('pf-jobs-container');
|
||||||
|
if (!container) {
|
||||||
|
const card = document.querySelector('#print-farm .card');
|
||||||
|
if (card) {
|
||||||
|
const queueCard = document.createElement('div');
|
||||||
|
queueCard.className = 'card';
|
||||||
|
queueCard.innerHTML = `
|
||||||
|
<div class="card-header"><i class="fa-solid fa-list-check"></i> Active Print Farm Job Queue</div>
|
||||||
|
<div id="pf-jobs-container" style="display:flex; flex-direction:column; gap:0.5rem;"></div>
|
||||||
|
`;
|
||||||
|
card.parentNode.insertBefore(queueCard, card.nextSibling);
|
||||||
|
container = document.getElementById('pf-jobs-container');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
livePrintJobs.forEach(job => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.style.cssText = 'background:#0b0f17; padding:0.6rem 0.8rem; border-radius:6px; font-size:0.75rem; display:flex; justify-content:space-between; align-items:center;';
|
||||||
|
|
||||||
|
let statusBadge = '<span style="color:var(--accent-emerald); font-weight:700;"><i class="fa-solid fa-check"></i> Complete</span>';
|
||||||
|
if (job.status === 'printing') {
|
||||||
|
statusBadge = '<span style="color:var(--accent-cyan); font-weight:700;"><i class="fa-solid fa-spinner fa-spin"></i> Printing</span>';
|
||||||
|
} else if (job.status === 'queued') {
|
||||||
|
statusBadge = '<span style="color:var(--text-muted); font-weight:600;"><i class="fa-solid fa-hourglass"></i> Queued</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<strong>${job.part_name}</strong>
|
||||||
|
<div style="font-size:0.65rem; color:var(--text-muted);">${job.printer_model} • ${job.material} • ${job.weight_grams}g</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
${statusBadge}
|
||||||
|
<div style="font-size:0.65rem; color:var(--accent-amber); font-weight:600;">${job.print_time_hours}h ($${parseFloat(job.cost_estimate).toFixed(2)})</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.loadPrintFarmData = loadPrintFarmData;
|
||||||
Loading…
x
Reference in New Issue
Block a user