All ChatGPT created, I just was scrivener and Feerless Leader.

This commit is contained in:
jlpoole 2026-03-19 09:10:05 -07:00
commit 19bd89c405
7 changed files with 596 additions and 2 deletions

249
web/app.js Normal file
View file

@ -0,0 +1,249 @@
/*
20260319 ChatGPT
$Header$
*/
"use strict";
function currency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
}).format(value);
}
function ceilDivUsage(requiredUnits, packQty) {
return Math.ceil(requiredUnits / packQty);
}
function computePrintingCost(assumptions, boxCount) {
let materialCost = 0;
for (const material of assumptions.printed_materials) {
const totalGrams = material.grams_per_box * boxCount;
const totalKg = totalGrams / 1000.0;
materialCost += totalKg * material.cost_per_kg;
}
materialCost *= assumptions.waste_factor;
const totalKwh =
(assumptions.printer_average_watts / 1000.0) *
assumptions.print_hours_per_box *
boxCount;
const electricityCost = totalKwh * assumptions.electricity_cost_per_kwh;
return {
material_cost: materialCost,
electricity_cost: electricityCost,
total_printing_cost: materialCost + electricityCost
};
}
function computeHardwareScenario(bom, boxCount, includeOptional = true) {
const componentRows = [];
let totalHardwareCost = 0;
for (const component of bom.components) {
if (!includeOptional && component.optional) {
continue;
}
const requiredUnits = component.units_used_per_box * boxCount;
const packsRequired = ceilDivUsage(requiredUnits, component.pack_qty);
const purchasedUnits = packsRequired * component.pack_qty;
const leftoverUnits = purchasedUnits - requiredUnits;
const totalCost = packsRequired * component.pack_cost;
totalHardwareCost += totalCost;
componentRows.push({
id: component.id,
name: component.name,
required_units: requiredUnits,
pack_qty: component.pack_qty,
pack_cost: component.pack_cost,
packs_required: packsRequired,
purchased_units: purchasedUnits,
leftover_units: leftoverUnits,
total_cost: totalCost,
optional: component.optional,
unit_type: component.unit_type,
notes: component.notes
});
}
return {
box_count: boxCount,
total_hardware_cost: totalHardwareCost,
component_rows: componentRows
};
}
function renderAssumptions(assumptions) {
const div = document.getElementById("assumptions");
const materialLines = assumptions.printed_materials.map((m) => {
return `<li>${m.name}: ${m.grams_per_box} g/box at ${currency(m.cost_per_kg)}/kg</li>`;
}).join("");
div.innerHTML = `
<ul>
<li>Waste factor: ${assumptions.waste_factor}</li>
<li>Electricity: ${currency(assumptions.electricity_cost_per_kwh)}/kWh</li>
<li>Printer average load: ${assumptions.printer_average_watts} W</li>
<li>Print hours per box: ${assumptions.print_hours_per_box}</li>
<li>Target scenarios: ${assumptions.target_box_counts.join(", ")}</li>
</ul>
<p><strong>Printed materials</strong></p>
<ul>${materialLines}</ul>
`;
}
function renderSummaryTable(bom, assumptions) {
const tbody = document.querySelector("#summary-table tbody");
tbody.innerHTML = "";
for (const boxCount of assumptions.target_box_counts) {
const hardware = computeHardwareScenario(bom, boxCount, true);
const printing = computePrintingCost(assumptions, boxCount);
const totalCost = hardware.total_hardware_cost + printing.total_printing_cost;
const costPerBox = totalCost / boxCount;
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${boxCount}</td>
<td>${currency(hardware.total_hardware_cost)}</td>
<td>${currency(printing.total_printing_cost)}</td>
<td>${currency(totalCost)}</td>
<td>${currency(costPerBox)}</td>
`;
tbody.appendChild(tr);
}
}
function renderComponentBreakdowns(bom, assumptions) {
const container = document.getElementById("component-breakdowns");
container.innerHTML = "";
for (const boxCount of assumptions.target_box_counts) {
const hardware = computeHardwareScenario(bom, boxCount, true);
const section = document.createElement("div");
section.className = "scenario-block";
let rowsHtml = "";
for (const row of hardware.component_rows) {
rowsHtml += `
<tr>
<td>${row.name}${row.optional ? " (optional)" : ""}</td>
<td>${row.required_units}</td>
<td>${row.pack_qty}</td>
<td>${currency(row.pack_cost)}</td>
<td>${row.packs_required}</td>
<td>${currency(row.total_cost)}</td>
<td>${row.notes || ""}</td>
</tr>
`;
}
section.innerHTML = `
<h3>${boxCount} box${boxCount === 1 ? "" : "es"}</h3>
<table>
<thead>
<tr>
<th>Component</th>
<th>Required Units</th>
<th>Pack Qty</th>
<th>Pack Cost</th>
<th>Packs Required</th>
<th>Total Cost</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
${rowsHtml}
</tbody>
</table>
`;
container.appendChild(section);
}
}
function renderLeftovers(bom, assumptions) {
const container = document.getElementById("leftovers");
container.innerHTML = "";
for (const boxCount of assumptions.target_box_counts) {
const hardware = computeHardwareScenario(bom, boxCount, true);
const section = document.createElement("div");
section.className = "scenario-block";
let rowsHtml = "";
for (const row of hardware.component_rows) {
rowsHtml += `
<tr>
<td>${row.name}</td>
<td>${row.purchased_units}</td>
<td>${row.required_units}</td>
<td>${row.leftover_units}</td>
<td>${row.unit_type}</td>
</tr>
`;
}
section.innerHTML = `
<h3>${boxCount} box${boxCount === 1 ? "" : "es"}</h3>
<table>
<thead>
<tr>
<th>Component</th>
<th>Purchased Units</th>
<th>Used Units</th>
<th>Leftover Units</th>
<th>Unit Type</th>
</tr>
</thead>
<tbody>
${rowsHtml}
</tbody>
</table>
`;
container.appendChild(section);
}
}
async function loadJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load ${url}: ${response.status}`);
}
return await response.json();
}
async function main() {
try {
const bom = await loadJson("../data/bom.json");
const assumptions = await loadJson("../data/assumptions.json");
renderAssumptions(assumptions);
renderSummaryTable(bom, assumptions);
renderComponentBreakdowns(bom, assumptions);
renderLeftovers(bom, assumptions);
} catch (err) {
console.error(err);
document.body.innerHTML = `
<main style="padding: 2rem; font-family: Arial, Helvetica, sans-serif;">
<h1>Error</h1>
<p>Unable to load project data.</p>
<pre>${String(err)}</pre>
</main>
`;
}
}
main();

89
web/index.html Normal file
View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<!--
20260319 ChatGPT
$Header$
Purpose:
Static cost model page for the filament drybox project.
Notes:
This page loads:
../data/bom.json
../data/assumptions.json
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Filament Drybox Cost Model</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Filament Drybox Cost Model</h1>
<p class="subtitle">
Lot-based purchasing, filament cost, and electricity for 1, 4, and 12 dryboxes
</p>
</header>
<main>
<section class="card">
<h2>Overview</h2>
<p>
This page estimates the out-of-pocket cost of building filament dryboxes,
taking into account both purchased hardware sold in lots and the printing
cost of the required parts.
</p>
</section>
<section class="card">
<h2>Assumptions</h2>
<div id="assumptions"></div>
</section>
<section class="card">
<h2>Summary</h2>
<table id="summary-table">
<thead>
<tr>
<th>Boxes</th>
<th>Total Hardware</th>
<th>Total Printing</th>
<th>Total Cost</th>
<th>Cost / Box</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
<section class="card">
<h2>Component Breakdown</h2>
<div id="component-breakdowns"></div>
</section>
<section class="card">
<h2>Inventory Leftovers</h2>
<div id="leftovers"></div>
</section>
<section class="card">
<h2>Notes</h2>
<ul>
<li>Hardware pricing is modeled using whole-pack purchases.</li>
<li>Printed material pricing is based on estimated grams per box.</li>
<li>Electricity is based on average printer wattage over total print time.</li>
<li>All initial prices and print weights should be revised with measured values.</li>
</ul>
</section>
</main>
<footer>
<p>
Project intended for publication on salemdata.net and in Forgejo for public inspection.
</p>
</footer>
<script src="app.js"></script>
</body>
</html>

75
web/style.css Normal file
View file

@ -0,0 +1,75 @@
/*
20260319 ChatGPT
$Header$
*/
body {
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 0;
background: #f4f6f8;
color: #222;
}
header,
footer {
background: #1f2933;
color: #fff;
padding: 1rem 1.5rem;
}
header h1,
footer p {
margin: 0;
}
.subtitle {
margin-top: 0.5rem;
color: #d9e2ec;
}
main {
max-width: 1100px;
margin: 1.5rem auto;
padding: 0 1rem;
}
.card {
background: #fff;
border: 1px solid #d9e2ec;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0.5rem;
}
th,
td {
border: 1px solid #cbd2d9;
padding: 0.55rem 0.7rem;
text-align: left;
vertical-align: top;
}
th {
background: #e9eff5;
}
.scenario-block {
margin-bottom: 1.5rem;
}
.small-note {
color: #52606d;
font-size: 0.92rem;
}
.mono {
font-family: "Courier New", Courier, monospace;
}