5 Commits

Author SHA1 Message Date
Devon Bontrager fc93c2ee57 Bump version to 1.3.0 and exclude tauri/ from Electron build
The Electron build was accidentally bundling the entire tauri/src-tauri/target
Rust build directory into app.asar (1.2GB!), inflating the installer to 304MB.
Added a files exclusion so Electron and Tauri builds stay independent.
2026-06-08 11:46:58 -04:00
Devon Bontrager e5ed1d3c9f Add lightweight Tauri shell, vendor offline libs, fix bugs and race conditions
- Add tauri/ subfolder: a Tauri-based desktop shell that loads the same
  index.html as the Electron app, producing a ~2MB installer (vs ~71MB for
  Electron) by using the OS WebView2 instead of bundled Chromium. Includes
  sync-frontend.js to keep tauri/dist/ in sync with the canonical index.html
  and vendor/ at build time (no UI duplication/drift).
- Vendor lucide, jsPDF, and jspdf-autotable locally under vendor/ instead of
  loading from CDNs, so the app works fully offline in both shells.
- Fix server.js path traversal vulnerability (sanitize/contain request paths).
- Fix exportPDF() using stale/edited piece data that could produce
  Infinity/NaN in generated PDFs; add a recalculate guard.
- Fix race conditions: overlapping copy/save-defaults button revert timers,
  and FileReader callbacks for logo/CSV import that could apply out of order.
- Minor cleanups: explicit array index parsing, hasResult() helper, consistent
  event binding for NMFC search results, cached DOM lookups.
- Document the dual-shell architecture and vendoring conventions in CLAUDE.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:19:48 -04:00
Devon Bontrager 3c3ef02f20 Bump version to 1.2.0 in package.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 11:08:52 -04:00
Devon Bontrager fc88d7ab4b v1.2.0 — Multi-piece, NMFC lookup, PDF export, defaults, CSV history, density gauge + bug fixes
Features added:
- Multi-piece support: add unlimited pieces each with L x W x H x Weight, combined class from total density
- NMFC commodity lookup: 55 bundled items, live search by name or code
- PDF export: branded PDF with piece table, Cu Ft, PCF, freight class via jsPDF
- Default piece preset: save a standard pallet size in Settings > Defaults
- Export/import history as CSV
- Density gauge: color gradient bar with live marker in results panel
- Unit toggle: in/cm and lbs/kg with live conversion across all pieces
- Copy to clipboard, timestamps in history, Enter key shortcut, input validation
- Charcoal dark mode

Bug fixes:
- PDF and Copy now blocked before a calculation is performed
- History restore no longer misconverts values when units differ from stored unit
- History display shows correct unit labels (cm/kg) instead of hardcoded in/lbs
- PDF logo format auto-detected from data URL (fixes silent failure for JPG uploads)
- Removed dead parsedPieces variable; forEach with early return prevents NaN accumulation
- Removed no-op placeholder update in applyUnitToggleUI
- CSV export URL revocation deferred to avoid race condition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 10:54:52 -04:00
Devon Bontrager 25973cabbc Add Lucide icons, web server, and settings page cleanup
- Replace emoji icons with Lucide SVG icons (moon, sun, settings, x, upload, rotate-ccw, trash-2, save)
- Add Node.js dev server on port 3010 (server.js + npm run serve)
- Redesign settings modal: tabbed layout, styled form fields, Save/Cancel footer
- Fix dark mode: btn-danger hover now uses rgba instead of hardcoded light pink
- Fix stale pendingBranding: history item click now calls revertAndClose()
- Collapse branding header when no name/logo is set
- Move density row inline style to .density-info CSS class
- Normalise all innerText to textContent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:57:19 -04:00
25 changed files with 26174 additions and 204 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "freight-calculator",
"runtimeExecutable": "node",
"runtimeArgs": ["server.js"],
"port": 3010
}
]
}
+62
View File
@@ -0,0 +1,62 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Two desktop shells — same frontend
This repo ships **two interchangeable desktop shells** that both load the exact same `index.html`:
1. **Electron** (original, root of repo) — heavier (~70MB installer) but mature/familiar
2. **Tauri** (`tauri/` subfolder) — much lighter (~2MB installer, ~8MB exe) using the OS's built-in WebView2 instead of bundled Chromium
`index.html` (plus `vendor/`) is the **single source of truth** for the UI — never duplicate or fork it. The Tauri shell's `tauri/dist/` is a *generated* copy (gitignored, rebuilt by `tauri/sync-frontend.js` before each `dev`/`build`run) — edit `index.html` at the repo root, never inside `tauri/dist/`.
### Electron commands (run from repo root)
- `npm start` — run the app as an Electron desktop window (loads `index.html` via `main.js`)
- `npm run serve` — run a plain Node HTTP server (`server.js`) at http://localhost:3010, serving the same `index.html` for browser-based testing
- `npm run build` — package the Windows installer via `electron-builder` (NSIS, output goes to `dist/`)
### Tauri commands (run from `tauri/`)
- `npm run dev` — sync the frontend into `tauri/dist/` and launch the Tauri dev shell
- `npm run build` — sync the frontend and produce MSI + NSIS installers (output in `tauri/src-tauri/target/release/bundle/`)
- `npm run serve` — same static server as the Electron side, for browser-based testing
- Requires the Rust toolchain (`rustup`/`cargo`) plus MSVC Build Tools and WebView2 (Tauri prerequisites) — see https://tauri.app/start/prerequisites/
There is no test suite, linter, or build step for the app code itself — `index.html` is loaded directly (as a file in Electron/Tauri, or statically served). To verify a change, just reload the app/page in whichever shell you're using.
## Architecture
This is a single-page Electron desktop app (also runnable as a static page) for calculating NMFC freight class from shipment dimensions and weight. Almost all logic lives in one file:
- **`index.html`** — contains everything: markup, CSS (`<style>` block), and all application JavaScript (`<script>` block starting around line 370). There are no separate JS/CSS files or build/bundling steps; what you see in `index.html` is what runs.
- **`main.js`** — minimal Electron bootstrap (creates the `BrowserWindow`, loads `index.html`, with `nodeIntegration: true` / `contextIsolation: false`)
- **`server.js`** — minimal static file server used only for `npm run serve` (browser-based dev/testing instead of Electron)
- **`vendor/`** — local copies of third-party JS libraries (`lucide.js`, `jspdf.umd.min.js`, `jspdf.plugin.autotable.min.js`), referenced by `index.html` via relative `<script src="vendor/...">` tags so the app runs **fully offline** with no CDN dependency. Not gitignored — these files are committed and get bundled into the installer by `electron-builder` automatically. **Do not switch these back to CDN URLs** — if a library needs upgrading, download the new version into `vendor/` and update the filename/reference together.
### Structure inside `index.html`'s script
The script is organized into clearly marked sections (look for the `// ─── Section ───` banner comments) in this order:
1. **Data**`densityRanges` (density → freight class lookup table with descriptions) and `nmfcData` (searchable list of NMFC codes/descriptions/classes)
2. **State**`appState`, persisted to `localStorage` under the key `freightCalcState` via `saveState()`. Defaults are merged with `Object.assign` so new fields can be added without breaking existing saved state. Shape: `{ theme, branding: {name, color, logo}, history[], units: {dim, wt}, defaults: {L, W, H, Weight} }`. The working `pieces` array (current shipment line items being entered) is ephemeral and NOT persisted.
3. **Init**`init()`, wires up the page on load, applies branding, populates settings/defaults forms, renders the density reference table
4. **Theme** — light/dark toggle
5. **Branding** — company name/color/logo customization, applied to the page header and PDF export
6. **Defaults tab** — user-configurable default piece dimensions/weight pre-filled on new entries
7. **Modal** — settings modal open/close/revert logic (`pendingBranding` holds unsaved edits so Cancel can discard them)
8. **Tabs** — settings modal tab switching (Branding / Defaults / History)
9. **History** — calculation history stored in `appState.history` (capped at 50 entries), CSV export/import, restore-from-history (re-applies the exact units the entry was saved in)
10. **Unit toggles** — switching between in/cm and lbs/kg; conversions are applied at calculation time via `dimFactor`/`wtFactor`, not by mutating stored values
11. **Pieces** — managing the dynamic list of shipment pieces (`renderPieces`, `addPiece`)
12. **Validation & Clear** — input validation/error display, form reset
13. **Density gauge** — visual gauge showing where the calculated density falls
14. **Calculate**`calculateFreightClass()`: the core calculation. Converts all dimensions/weights to inches/lbs, computes total cubic feet and total weight, derives density, looks up the class from `densityRanges` (first range where `density >= min` wins), saves a history entry, and updates the UI/gauge
15. **Copy to clipboard** — copies the result summary as text
16. **PDF Export**`exportPDF()` builds a branded PDF report using `jsPDF` + `jspdf-autotable` (loaded from local `vendor/` copies via `<script>` tags in the `<head>` — see `vendor/` note above), including logo, company branding, pieces table, and footer
17. **NMFC Search** — live search over `nmfcData` by code or description; selecting a result highlights the matching row in the on-page density reference table
### Key conventions
- All persisted user data lives in one `appState` object in `localStorage`; always go through `saveState()` after mutating it.
- Unit conversions (`in``cm`, `lbs``kg`) happen only at calculation/display time — stored values keep their original units alongside a `units` field so they can be restored exactly.
- External libraries (`lucide` icons, `jsPDF`, `jspdf-autotable`) are vendored locally in `vendor/` and loaded via relative `<script>` tags in the `<head>`**not** from CDNs — so the app works completely offline.
+1020 -193
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -1,11 +1,12 @@
{ {
"name": "freight-calculator-pro", "name": "freight-calculator-pro",
"version": "1.0.0", "version": "1.3.0",
"description": "Professional Freight Class Calculator", "description": "Professional Freight Class Calculator",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"build": "electron-builder --win" "build": "electron-builder --win",
"serve": "node server.js"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@@ -16,6 +17,10 @@
"build": { "build": {
"appId": "com.freightcalc.pro", "appId": "com.freightcalc.pro",
"productName": "Freight Calculator Pro", "productName": "Freight Calculator Pro",
"files": [
"**/*",
"!tauri/**"
],
"win": { "win": {
"target": "nsis", "target": "nsis",
"icon": "icon.ico" "icon": "icon.ico"
+43
View File
@@ -0,0 +1,43 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 3010;
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.png': 'image/png',
'.ico': 'image/x-icon',
'.json': 'application/json',
};
const server = http.createServer((req, res) => {
// Strip query string / hash and decode URI components before resolving,
// then confirm the resolved path stays inside __dirname (no ../ escapes).
const urlPath = decodeURIComponent(req.url.split('?')[0].split('#')[0]);
const reqPath = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, '');
const filePath = path.join(__dirname, reqPath);
if (!filePath.startsWith(__dirname + path.sep) && filePath !== __dirname) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('403 Forbidden');
return;
}
const ext = path.extname(filePath);
const contentType = mimeTypes[ext] || 'application/octet-stream';
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
return;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
});
});
server.listen(PORT, () => {
console.log(`Freight Calculator Pro running at http://localhost:${PORT}`);
});
+7
View File
@@ -0,0 +1,7 @@
node_modules/
src-tauri/target/
src-tauri/gen/schemas
*.log
# Generated by sync-frontend.js — copy of ../index.html and ../vendor/, not source of truth
dist/
+16
View File
@@ -0,0 +1,16 @@
{
"name": "freight-calculator-pro-tauri",
"version": "1.3.0",
"description": "Professional Freight Class Calculator (Tauri shell)",
"private": true,
"scripts": {
"sync": "node sync-frontend.js",
"dev": "npm run sync && tauri dev",
"build": "npm run sync && tauri build",
"serve": "node ../server.js"
},
"license": "ISC",
"devDependencies": {
"@tauri-apps/cli": "^2.11.2"
}
}
+4
View File
@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas
+4941
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "freight-calculator-pro"
version = "1.3.0"
description = "Professional Freight Class Calculator"
authors = ["Devon Bontrager"]
license = "ISC"
repository = "https://gitea.netbird.zimspace.uk:5938/debont80/FreightCalculatorPro.git"
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.11.2", features = [] }
tauri-plugin-log = "2"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

+16
View File
@@ -0,0 +1,16 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}
+36
View File
@@ -0,0 +1,36 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Freight Calculator Pro",
"version": "1.3.0",
"identifier": "com.freightcalc.pro",
"build": {
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Freight Calculator Pro",
"width": 950,
"height": 950,
"minWidth": 800,
"minHeight": 700,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+27
View File
@@ -0,0 +1,27 @@
// Copies the canonical frontend (../index.html and ../vendor/) into ./dist
// before each Tauri dev/build run, so the Tauri shell always packages the
// exact same UI as the Electron version — no manual sync, no drift.
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const DIST = path.join(__dirname, 'dist');
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src)) {
copyRecursive(path.join(src, entry), path.join(dest, entry));
}
} else {
fs.copyFileSync(src, dest);
}
}
fs.rmSync(DIST, { recursive: true, force: true });
fs.mkdirSync(DIST, { recursive: true });
copyRecursive(path.join(ROOT, 'index.html'), path.join(DIST, 'index.html'));
copyRecursive(path.join(ROOT, 'vendor'), path.join(DIST, 'vendor'));
console.log(`Synced frontend (index.html + vendor/) from ${ROOT} -> ${DIST}`);
File diff suppressed because one or more lines are too long
+398
View File
File diff suppressed because one or more lines are too long
+19522
View File
File diff suppressed because it is too large Load Diff