1089 lines
94 KiB
Plaintext
1089 lines
94 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>FortKart CNC — Retrofit Parts & OEM Supply</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
||
<style>
|
||
:root{--bg:#09090b;--bg2:#111113;--card:#18181b;--card-hover:#1f1f23;--border:#27272a;--border-light:#3f3f46;--fg:#fafafa;--fg-muted:#71717a;--accent:#d4a853;--accent-hover:#e6bc6a;--accent-dim:rgba(212,168,83,.12);--success:#22c55e;--danger:#ef4444;--warn:#f59e0b;--info:#38bdf8}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:'Space Grotesk',sans-serif;background:var(--bg);color:var(--fg);overflow-x:hidden;min-height:100vh}
|
||
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:var(--bg2)}::-webkit-scrollbar-thumb{background:var(--border-light);border-radius:3px}
|
||
code,.mono{font-family:'JetBrains Mono',monospace}
|
||
#loader{position:fixed;inset:0;z-index:9999;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity .6s,visibility .6s}
|
||
#loader.hidden{opacity:0;visibility:hidden}
|
||
#loader .logo-text{font-size:2.5rem;font-weight:800;letter-spacing:-.03em;background:linear-gradient(135deg,var(--accent),#f0d78c);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||
#loader .bar-track{width:280px;height:3px;background:var(--border);border-radius:2px;margin-top:2rem;overflow:hidden}
|
||
#loader .bar-fill{height:100%;background:var(--accent);border-radius:2px;width:0%;transition:width .4s ease}
|
||
#loader .status{color:var(--fg-muted);font-size:.8rem;margin-top:.75rem}
|
||
nav{position:sticky;top:0;z-index:100;background:rgba(9,9,11,.85);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||
.nav-link{position:relative;color:var(--fg-muted);font-size:.875rem;font-weight:500;padding:.5rem .75rem;border-radius:.5rem;transition:all .2s;cursor:pointer;white-space:nowrap}
|
||
.nav-link:hover,.nav-link.active{color:var(--fg);background:var(--accent-dim)}
|
||
.nav-link.active::after{content:'';position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);width:20px;height:2px;background:var(--accent);border-radius:1px}
|
||
.hamburger{display:none;width:36px;height:36px;align-items:center;justify-content:center;border-radius:.5rem;border:1px solid var(--border);background:var(--card);color:var(--fg-muted);cursor:pointer;transition:all .2s}
|
||
.hamburger:hover{border-color:var(--accent);color:var(--accent)}
|
||
@media(max-width:768px){.hamburger{display:inline-flex}.nav-links{display:none!important;position:absolute;top:100%;left:0;right:0;background:var(--bg2);border-bottom:1px solid var(--border);padding:.75rem;flex-direction:column;gap:.25rem}.nav-links.open{display:flex!important}}
|
||
.badge{position:absolute;top:-.3rem;right:-.3rem;background:var(--accent);color:var(--bg);font-size:.6rem;font-weight:700;width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center}
|
||
.product-card{background:var(--card);border:1px solid var(--border);border-radius:1rem;overflow:hidden;transition:all .3s ease;cursor:pointer}
|
||
.product-card:hover{border-color:var(--border-light);transform:translateY(-4px);box-shadow:0 12px 40px rgba(0,0,0,.4)}
|
||
.product-card img{width:100%;aspect-ratio:1;object-fit:cover;transition:transform .5s}
|
||
.product-card:hover img{transform:scale(1.05)}
|
||
.product-card .img-wrap{overflow:hidden;position:relative}
|
||
.product-card .cat-tag{position:absolute;top:.75rem;left:.75rem;background:rgba(9,9,11,.75);backdrop-filter:blur(8px);color:var(--accent);font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;padding:.25rem .6rem;border-radius:.375rem;border:1px solid rgba(212,168,83,.2)}
|
||
.product-card .sku-tag{position:absolute;bottom:.75rem;left:.75rem;background:rgba(9,9,11,.8);backdrop-filter:blur(8px);color:var(--fg-muted);font-size:.6rem;font-weight:500;padding:.2rem .5rem;border-radius:.25rem;font-family:'JetBrains Mono',monospace}
|
||
.btn-primary{background:var(--accent);color:var(--bg);font-weight:600;padding:.6rem 1.25rem;border-radius:.5rem;border:none;cursor:pointer;font-size:.875rem;transition:all .2s;font-family:inherit}
|
||
.btn-primary:hover{background:var(--accent-hover);transform:translateY(-1px);box-shadow:0 4px 16px rgba(212,168,83,.3)}
|
||
.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
|
||
.btn-outline{background:transparent;color:var(--fg);font-weight:500;padding:.5rem 1rem;border-radius:.5rem;border:1px solid var(--border-light);cursor:pointer;font-size:.8rem;transition:all .2s;font-family:inherit}
|
||
.btn-outline:hover{border-color:var(--accent);color:var(--accent)}
|
||
.btn-sm{padding:.35rem .75rem;font-size:.75rem}
|
||
.btn-icon{width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;border:1px solid var(--border);background:var(--card);color:var(--fg-muted);cursor:pointer;transition:all .2s}
|
||
.btn-icon:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
|
||
#cart-sidebar{position:fixed;top:0;right:0;bottom:0;width:420px;max-width:95vw;background:var(--bg2);border-left:1px solid var(--border);z-index:200;transform:translateX(100%);transition:transform .35s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column}
|
||
#cart-sidebar.open{transform:translateX(0)}
|
||
#cart-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:199;opacity:0;visibility:hidden;transition:all .3s}
|
||
#cart-overlay.open{opacity:1;visibility:visible}
|
||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:300;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:all .3s;padding:1rem}
|
||
.modal-overlay.open{opacity:1;visibility:visible}
|
||
.modal-content{background:var(--bg2);border:1px solid var(--border);border-radius:1rem;max-width:900px;width:100%;max-height:90vh;overflow-y:auto;transform:scale(.95);transition:transform .3s}
|
||
.modal-overlay.open .modal-content{transform:scale(1)}
|
||
.toast{padding:.75rem 1rem;border-radius:.5rem;font-size:.8rem;font-weight:500;display:flex;align-items:center;gap:.5rem;animation:slideInR .3s ease;border:1px solid;min-width:280px;max-width:400px}
|
||
.toast-success{background:rgba(34,197,94,.1);border-color:rgba(34,197,94,.3);color:var(--success)}
|
||
.toast-error{background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.3);color:var(--danger)}
|
||
.toast-info{background:rgba(56,189,248,.1);border-color:rgba(56,189,248,.3);color:var(--info)}
|
||
.toast-warn{background:rgba(245,158,11,.1);border-color:rgba(245,158,11,.3);color:var(--warn)}
|
||
@keyframes slideInR{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
|
||
.sec-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
||
.sec-dot.on{background:var(--success);box-shadow:0 0 8px rgba(34,197,94,.5)}
|
||
.sec-dot.off{background:var(--danger);box-shadow:0 0 8px rgba(239,68,68,.5)}
|
||
.sec-dot.pending{background:var(--warn);animation:pulse 1.5s infinite}
|
||
.sec-dot.demo{background:var(--info);box-shadow:0 0 8px rgba(56,189,248,.4)}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||
.code-block{background:#0d0d0f;border:1px solid var(--border);border-radius:.75rem;padding:1rem;overflow-x:auto;font-size:.75rem;line-height:1.7;tab-size:2}
|
||
.code-block .kw{color:#c678dd}.code-block .fn{color:#61afef}.code-block .str{color:#98c379}.code-block .cm{color:#5c6370;font-style:italic}.code-block .var{color:#e06c75}.code-block .num{color:#d19a66}.code-block .op{color:#56b6c2}
|
||
.tab-btn{padding:.5rem 1rem;font-size:.8rem;font-weight:500;color:var(--fg-muted);border-bottom:2px solid transparent;cursor:pointer;transition:all .2s;background:none;border-top:none;border-left:none;border-right:none;font-family:inherit}
|
||
.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||
.tab-btn:hover{color:var(--fg)}
|
||
.audit-table{width:100%;border-collapse:collapse;font-size:.75rem}
|
||
.audit-table th{text-align:left;padding:.6rem .75rem;color:var(--fg-muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em;font-size:.65rem;border-bottom:1px solid var(--border)}
|
||
.audit-table td{padding:.6rem .75rem;border-bottom:1px solid var(--border);color:var(--fg);vertical-align:top}
|
||
.audit-table tr:hover td{background:var(--accent-dim)}
|
||
.field{width:100%;background:var(--card);border:1px solid var(--border);border-radius:.5rem;padding:.6rem .75rem;color:var(--fg);font-size:.875rem;font-family:inherit;transition:border-color .2s;outline:none}
|
||
.field:focus{border-color:var(--accent)}
|
||
.field.error{border-color:var(--danger)}
|
||
.field-label{font-size:.75rem;font-weight:500;color:var(--fg-muted);margin-bottom:.35rem;display:block}
|
||
.bg-glow{position:fixed;width:600px;height:600px;border-radius:50%;filter:blur(150px);opacity:.04;pointer-events:none;z-index:0}
|
||
.stars{color:var(--accent);font-size:.7rem;letter-spacing:1px}
|
||
.cat-pill{padding:.35rem .85rem;border-radius:2rem;font-size:.75rem;font-weight:500;border:1px solid var(--border);color:var(--fg-muted);cursor:pointer;transition:all .2s;background:transparent;font-family:inherit;white-space:nowrap}
|
||
.cat-pill:hover{border-color:var(--border-light);color:var(--fg)}
|
||
.cat-pill.active{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
|
||
.param-chip{padding:.2rem .6rem;border-radius:.25rem;font-size:.68rem;font-weight:500;border:1px solid var(--border);color:var(--fg-muted);cursor:pointer;transition:all .15s;background:transparent;font-family:inherit}
|
||
.param-chip:hover{border-color:var(--border-light);color:var(--fg)}
|
||
.param-chip.active{background:rgba(56,189,248,.12);border-color:var(--info);color:var(--info)}
|
||
.view{display:none;animation:fadeIn .3s ease}
|
||
.view.active{display:block}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||
.qty-ctrl{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:.375rem;overflow:hidden}
|
||
.qty-ctrl button{width:28px;height:28px;background:var(--card);border:none;color:var(--fg-muted);cursor:pointer;font-size:.8rem;transition:all .15s}
|
||
.qty-ctrl button:hover{background:var(--accent-dim);color:var(--accent)}
|
||
.qty-ctrl span{width:32px;text-align:center;font-size:.8rem;font-weight:600;background:var(--bg2)}
|
||
.spec-tag{display:inline-block;background:var(--accent-dim);color:var(--accent);font-size:.65rem;font-weight:600;padding:.15rem .45rem;border-radius:.25rem;margin-right:.3rem;margin-bottom:.3rem}
|
||
.hero-stat{text-align:center;padding:1rem}
|
||
.hero-stat .val{font-size:1.5rem;font-weight:800;color:var(--accent)}
|
||
.hero-stat .lbl{font-size:.7rem;color:var(--fg-muted);margin-top:.15rem;text-transform:uppercase;letter-spacing:.05em}
|
||
.tier-row{display:flex;justify-content:space-between;padding:.3rem 0;font-size:.78rem;border-bottom:1px solid var(--border)}
|
||
.tier-row:last-child{border-bottom:none}
|
||
.tier-row.active-tier{color:var(--accent);font-weight:600}
|
||
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--bg);border-top-color:transparent;border-radius:50%;animation:spin .6s linear infinite}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.freight-badge{background:rgba(245,158,11,.12);color:var(--warn);font-size:.65rem;font-weight:600;padding:.2rem .5rem;border-radius:.25rem;border:1px solid rgba(245,158,11,.25)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="bg-glow" style="top:-200px;left:-200px;background:var(--accent)"></div>
|
||
<div class="bg-glow" style="bottom:-200px;right:-200px;background:#e06c75"></div>
|
||
|
||
<div id="loader">
|
||
<i class="fa-solid fa-gear" style="font-size:2.5rem;color:var(--accent);margin-bottom:1rem;animation:spin 2s linear infinite"></i>
|
||
<div class="logo-text">FortKart CNC</div>
|
||
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">Retrofit Parts & OEM Supply</p>
|
||
<div class="bar-track"><div class="bar-fill" id="load-bar"></div></div>
|
||
<div class="status" id="load-status">Initializing security engine...</div>
|
||
</div>
|
||
|
||
<nav id="main-nav" style="opacity:0;transition:opacity .5s">
|
||
<div style="max-width:1320px;margin:0 auto;padding:.75rem 1.5rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;position:relative">
|
||
<div style="display:flex;align-items:center;gap:.5rem;cursor:pointer" onclick="navigate('shop')">
|
||
<i class="fa-solid fa-gear" style="color:var(--accent);font-size:1.1rem"></i>
|
||
<span style="font-weight:700;font-size:1.1rem;letter-spacing:-.02em">FortKart <span style="color:var(--fg-muted);font-weight:400;font-size:.8rem">CNC</span></span>
|
||
</div>
|
||
<button class="hamburger" id="hamburger-btn" onclick="toggleMobileNav()" aria-label="Menu"><i class="fa-solid fa-bars"></i></button>
|
||
<div class="nav-links" id="nav-links" style="display:flex;align-items:center;gap:.25rem">
|
||
<div class="nav-link active" data-view="shop" onclick="navigate('shop');closeMobileNav()"><i class="fa-solid fa-wrench" style="margin-right:.35rem"></i>Parts</div>
|
||
<div class="nav-link" data-view="audit" onclick="navigate('audit');closeMobileNav()"><i class="fa-solid fa-clipboard-list" style="margin-right:.35rem"></i>Audit Log</div>
|
||
<div class="nav-link" data-view="architecture" onclick="navigate('architecture');closeMobileNav()"><i class="fa-solid fa-code" style="margin-right:.35rem"></i>Architecture</div>
|
||
<div class="nav-link" data-view="security" onclick="navigate('security');closeMobileNav()"><i class="fa-solid fa-lock" style="margin-right:.35rem"></i>Security</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:.75rem">
|
||
<div id="sec-indicator" style="display:flex;align-items:center;gap:.4rem;font-size:.7rem;color:var(--fg-muted)">
|
||
<span class="sec-dot pending" id="sec-dot-wasm" title="WASM (demonstrational)"></span><span title="WASM demo module">WASM</span>
|
||
<span class="sec-dot pending" id="sec-dot-crypto" style="margin-left:.5rem" title="Web Crypto API"></span><span title="SHA-256 via Web Crypto API">Crypto</span>
|
||
</div>
|
||
<button class="btn-icon" style="position:relative" onclick="toggleCart()" aria-label="Open cart">
|
||
<i class="fa-solid fa-cart-shopping"></i>
|
||
<span class="badge" id="cart-count" style="display:none">0</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main style="max-width:1320px;margin:0 auto;padding:1.5rem;position:relative;z-index:1">
|
||
<div class="view active" id="view-shop">
|
||
<div style="background:linear-gradient(135deg,var(--card) 0%,rgba(212,168,83,.06) 100%);border:1px solid var(--border);border-radius:1rem;padding:2rem;margin-bottom:2rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1.5rem">
|
||
<div style="flex:1;min-width:280px">
|
||
<h1 style="font-size:2rem;font-weight:800;letter-spacing:-.03em;line-height:1.15">CNC Retrofit Components<br>& OEM Replacement Parts</h1>
|
||
<p style="color:var(--fg-muted);margin-top:.6rem;font-size:.9rem;max-width:520px;line-height:1.5">Ball screws, spindles, controllers, and tooling for legacy machines. All transactions verified through SHA-256 audit chain.</p>
|
||
</div>
|
||
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||
<div class="hero-stat"><div class="val">2,400+</div><div class="lbl">Parts in Stock</div></div>
|
||
<div class="hero-stat"><div class="val">98.7%</div><div class="lbl">Fill Rate</div></div>
|
||
<div class="hero-stat"><div class="val">48hr</div><div class="lbl">Ship Time</div></div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:1rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center">
|
||
<div style="flex:1;min-width:200px;position:relative">
|
||
<i class="fa-solid fa-magnifying-glass" style="position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--fg-muted);font-size:.8rem"></i>
|
||
<input class="field" style="padding-left:2.2rem" placeholder="Search by SKU, name, or spec..." id="search-input" oninput="renderProducts()">
|
||
</div>
|
||
<div style="display:flex;gap:.4rem;flex-wrap:wrap" id="cat-filters"></div>
|
||
<select class="field" style="width:auto;min-width:140px" id="sort-select" onchange="renderProducts()">
|
||
<option value="featured">Featured</option><option value="price-asc">Price: Low–High</option>
|
||
<option value="price-desc">Price: High–Low</option><option value="rating">Top Rated</option><option value="name">Name A–Z</option>
|
||
</select>
|
||
</div>
|
||
<div id="param-filters" style="margin-bottom:1rem;display:none"></div>
|
||
<div id="product-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1.25rem"></div>
|
||
<div id="no-results" style="display:none;text-align:center;padding:4rem 1rem;color:var(--fg-muted)">
|
||
<i class="fa-solid fa-box-open" style="font-size:2.5rem;margin-bottom:1rem;display:block;opacity:.4"></i><p>No parts match your search.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-audit">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;flex-wrap:wrap;gap:1rem">
|
||
<div><h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">Audit Trail</h1>
|
||
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">SHA-256 hash-chained log — persisted to localStorage</p></div>
|
||
<div style="display:flex;gap:.5rem;align-items:center">
|
||
<span id="chain-status" style="font-size:.75rem;font-weight:600"></span>
|
||
<button class="btn-outline btn-sm" onclick="verifyChain()"><i class="fa-solid fa-link" style="margin-right:.35rem"></i>Verify Chain</button>
|
||
<button class="btn-outline btn-sm" onclick="clearAuditLog()"><i class="fa-solid fa-trash" style="margin-right:.35rem"></i>Clear</button>
|
||
</div>
|
||
</div>
|
||
<div id="audit-persist-note" style="background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.2);border-radius:.5rem;padding:.5rem .75rem;margin-bottom:1rem;font-size:.72rem;color:var(--info);display:none">
|
||
<i class="fa-solid fa-database" style="margin-right:.35rem"></i>Loaded <span id="persist-count">0</span> entries from localStorage. Chain integrity will be verified.
|
||
</div>
|
||
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;overflow:hidden">
|
||
<div style="overflow-x:auto;max-height:70vh;overflow-y:auto">
|
||
<table class="audit-table"><thead><tr><th>Time</th><th>Action</th><th>Details</th><th>Hash</th><th>Prev Hash</th></tr></thead>
|
||
<tbody id="audit-tbody"></tbody></table>
|
||
</div>
|
||
<div id="audit-empty" style="padding:3rem;text-align:center;color:var(--fg-muted)">
|
||
<i class="fa-solid fa-clipboard-list" style="font-size:2rem;margin-bottom:.75rem;display:block;opacity:.3"></i>
|
||
<p>Audit log is empty. Interact with the store to generate entries.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-architecture">
|
||
<div style="margin-bottom:1.5rem"><h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">System Architecture</h1>
|
||
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">PHP backend, vanilla JS security engine with Web Crypto API, demonstrational WASM module</p></div>
|
||
<div style="display:flex;gap:.25rem;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap">
|
||
<button class="tab-btn active" onclick="switchArchTab('php',this)">PHP Backend</button>
|
||
<button class="tab-btn" onclick="switchArchTab('js',this)">JS Security Engine</button>
|
||
<button class="tab-btn" onclick="switchArchTab('wasm',this)">WASM (Demo)</button>
|
||
<button class="tab-btn" onclick="switchArchTab('flow',this)">Request Flow</button>
|
||
</div>
|
||
<div id="arch-php" class="arch-panel"></div>
|
||
<div id="arch-js" class="arch-panel" style="display:none"></div>
|
||
<div id="arch-wasm" class="arch-panel" style="display:none"></div>
|
||
<div id="arch-flow" class="arch-panel" style="display:none"></div>
|
||
</div>
|
||
|
||
<div class="view" id="view-security">
|
||
<div style="margin-bottom:1.5rem"><h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">Security Dashboard</h1>
|
||
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">Real-time status of all security layers</p></div>
|
||
<div id="security-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem"></div>
|
||
</div>
|
||
|
||
<div class="view" id="view-checkout">
|
||
<div style="max-width:640px;margin:0 auto">
|
||
<button class="btn-outline btn-sm" onclick="navigate('shop')" style="margin-bottom:1.5rem"><i class="fa-solid fa-arrow-left" style="margin-right:.35rem"></i>Back to Parts</button>
|
||
<h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em;margin-bottom:.25rem">Checkout</h1>
|
||
<p style="color:var(--fg-muted);font-size:.85rem;margin-bottom:1.5rem">All fields sanitized through the JS security engine</p>
|
||
<div id="checkout-form"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-confirmation">
|
||
<div style="max-width:640px;margin:0 auto;text-align:center;padding:3rem 0">
|
||
<div style="width:72px;height:72px;border-radius:50%;background:rgba(34,197,94,.12);border:2px solid var(--success);display:inline-flex;align-items:center;justify-content:center;margin-bottom:1.5rem">
|
||
<i class="fa-solid fa-check" style="font-size:1.8rem;color:var(--success)"></i></div>
|
||
<h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">Order Confirmed</h1>
|
||
<p style="color:var(--fg-muted);margin-top:.5rem;font-size:.9rem" id="confirm-text"></p>
|
||
<div id="confirm-details" style="margin-top:2rem;text-align:left"></div>
|
||
<button class="btn-primary" style="margin-top:2rem" onclick="navigate('shop')">Continue Shopping</button>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<div id="cart-overlay" onclick="toggleCart()"></div>
|
||
<aside id="cart-sidebar">
|
||
<div style="padding:1.25rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
|
||
<h2 style="font-size:1.1rem;font-weight:700">Your Cart</h2>
|
||
<button class="btn-icon" onclick="toggleCart()" aria-label="Close cart"><i class="fa-solid fa-xmark"></i></button>
|
||
</div>
|
||
<div id="cart-items" style="flex:1;overflow-y:auto;padding:1rem"></div>
|
||
<div id="cart-footer" style="padding:1.25rem;border-top:1px solid var(--border)">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem;font-size:.85rem"><span style="color:var(--fg-muted)">Subtotal</span><span id="cart-subtotal" style="font-weight:600">$0.00</span></div>
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:1rem;font-size:.85rem"><span style="color:var(--fg-muted)">Security Fee</span><span style="font-weight:600;color:var(--success)">$0.00</span></div>
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:1.25rem;font-size:1.1rem;font-weight:700"><span>Total</span><span id="cart-total" style="color:var(--accent)">$0.00</span></div>
|
||
<button class="btn-primary" style="width:100%" id="checkout-btn" onclick="goToCheckout()" disabled>Proceed to Checkout</button>
|
||
<p style="text-align:center;font-size:.65rem;color:var(--fg-muted);margin-top:.5rem"><i class="fa-solid fa-shield-halved" style="margin-right:.25rem"></i>SHA-256 audit chain • CSRF protected</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="modal-overlay" id="product-modal"><div class="modal-content" id="product-modal-content"></div></div>
|
||
<div id="toast-container" style="position:fixed;top:5rem;right:1.5rem;z-index:500;display:flex;flex-direction:column;gap:.5rem"></div>
|
||
|
||
<script>
|
||
/* ================================================================
|
||
WASM MODULE — DJB2 hash (demonstrational, not cryptographic)
|
||
================================================================ */
|
||
const WASM_BYTES=new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x07,0x01,0x60,0x01,0x7f,0x01,0x7f,0x03,0x02,0x01,0x00,0x05,0x03,0x01,0x00,0x01,0x07,0x0e,0x02,0x03,0x6d,0x65,0x6d,0x02,0x00,0x04,0x68,0x61,0x73,0x68,0x00,0x00,0x0a,0x37,0x01,0x35,0x01,0x02,0x7f,0x41,0x85,0x2a,0x21,0x02,0x41,0x00,0x21,0x01,0x02,0x40,0x03,0x40,0x20,0x01,0x20,0x00,0x4f,0x0d,0x01,0x20,0x02,0x41,0x05,0x74,0x20,0x02,0x6a,0x20,0x01,0x2d,0x00,0x00,0x6a,0x21,0x02,0x20,0x01,0x41,0x01,0x6a,0x21,0x01,0x0c,0x00,0x0b,0x0b,0x20,0x02,0x0b]);
|
||
let wasmInstance=null,wasmReady=false;
|
||
function wasmHash(s){if(!wasmInstance)return jsHash(s);const b=new TextEncoder().encode(s);new Uint8Array(wasmInstance.exports.mem.buffer).set(b);return(wasmInstance.exports.hash(b.length)>>>0).toString(16).padStart(8,'0')}
|
||
function jsHash(s){let h=5381;for(let i=0;i<s.length;i++)h=((h<<5)+h+s.charCodeAt(i))|0;return(h>>>0).toString(16).padStart(8,'0')}
|
||
async function initWasm(){try{const{instance}=await WebAssembly.instantiate(WASM_BYTES);wasmInstance=instance;wasmReady=true;document.getElementById('sec-dot-wasm').className='sec-dot demo';return true}catch(e){console.warn('WASM init failed:',e);document.getElementById('sec-dot-wasm').className='sec-dot off';return false}}
|
||
|
||
/* ================================================================
|
||
SHA-256 — Web Crypto API with pure-JS fallback
|
||
================================================================ */
|
||
async function sha256(message){
|
||
if(window.crypto&&crypto.subtle){
|
||
const buf=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(message));
|
||
return[...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,'0')).join('');
|
||
}
|
||
return sha256Pure(message);
|
||
}
|
||
function sha256Pure(str){
|
||
const rotr=(v,n)=>(v>>>n)|(v<<(32-n));
|
||
const K=new Uint32Array([0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2]);
|
||
const H=new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
|
||
const msg=new TextEncoder().encode(str);const bitLen=msg.length*8;
|
||
const padLen=((56-(msg.length+1)%64)+64)%64;
|
||
const padded=new Uint8Array(msg.length+1+padLen+8);padded.set(msg);padded[msg.length]=0x80;
|
||
const dv=new DataView(padded.buffer);dv.setUint32(padded.length-4,bitLen>>>0,false);
|
||
for(let o=0;o<padded.length;o+=64){
|
||
const W=new Uint32Array(64);
|
||
for(let i=0;i<16;i++)W[i]=dv.getUint32(o+i*4,false);
|
||
for(let i=16;i<64;i++){const s0=rotr(W[i-15],7)^rotr(W[i-15],18)^(W[i-15]>>>3);const s1=rotr(W[i-2],17)^rotr(W[i-2],19)^(W[i-2]>>>10);W[i]=(W[i-16]+s0+W[i-7]+s1)|0}
|
||
let[a,b,c,d,e,f,g,h]=H;
|
||
for(let i=0;i<64;i++){const S1=rotr(e,6)^rotr(e,11)^rotr(e,25);const ch=(e&f)(~e&g);const t1=(h+S1+ch+K[i]+W[i])|0;const S0=rotr(a,2)^rotr(a,13)^rotr(a,22);const maj=(a&b)^(a&c)^(b&c);const t2=(S0+maj)|0;h=g;g=f;f=e;e=(d+t1)|0;d=c;c=b;b=a;a=(t1+t2)|0}
|
||
H[0]=(H[0]+a)|0;H[1]=(H[1]+b)|0;H[2]=(H[2]+c)|0;H[3]=(H[3]+d)|0;H[4]=(H[4]+e)|0;H[5]=(H[5]+f)|0;H[6]=(H[6]+g)|0;H[7]=(H[7]+h)|0;
|
||
}
|
||
return[...H].map(v=>v.toString(16).padStart(8,'0')).join('');
|
||
}
|
||
|
||
/* ================================================================
|
||
SECURITY ENGINE — Pure vanilla JS, zero dependencies
|
||
================================================================ */
|
||
class SecurityEngine{
|
||
constructor(sessionId){
|
||
this.sessionId=sessionId;
|
||
this.logs=[];this.prevHash='GENESIS';
|
||
this.csrfToken=null;this.csrfCreated=0;
|
||
this.rateLimits={};
|
||
this.sanitizerPatterns=[/<script[\s\S]*?<\/script>/gi,/javascript\s*:/gi,/on\w+\s*=/gi,/<iframe/gi,/<object/gi,/<embed/gi,/<form/gi,/<input/gi,/expression\s*\(/gi,/@import/gi];
|
||
this._restoreFromStorage();
|
||
}
|
||
|
||
/* ── Audit Log with SHA-256 hash chain ── */
|
||
async logAudit(action,details){
|
||
const entry={ts:Date.now(),action,details,session:this.sessionId.slice(0,12),prevHash:this.prevHash};
|
||
const hash=await sha256(JSON.stringify(entry,Object.keys(entry).sort()));
|
||
entry.hash=hash.slice(0,16);
|
||
this.prevHash=entry.hash;
|
||
this.logs.push(entry);
|
||
this._persist();
|
||
return entry;
|
||
}
|
||
|
||
async verifyChain(){
|
||
let prev='GENESIS';
|
||
for(let i=0;i<this.logs.length;i++){
|
||
const check={...this.logs[i]};
|
||
const storedHash=check.hash;delete check.hash;
|
||
check.prevHash=prev;
|
||
const computed=(await sha256(JSON.stringify(check,Object.keys(check).sort()))).slice(0,16);
|
||
if(computed!==storedHash)return{valid:false,brokenAt:i,entry:this.logs[i]};
|
||
prev=storedHash;
|
||
}
|
||
return{valid:true,count:this.logs.length};
|
||
}
|
||
|
||
clearLogs(){this.logs=[];this.prevHash='GENESIS';this._persist()}
|
||
|
||
/* ── Input Sanitization ── */
|
||
sanitize(text){
|
||
if(typeof text!=='string')text=String(text);
|
||
let r=text;
|
||
for(const p of this.sanitizerPatterns)r=r.replace(p,'[SANITIZED]');
|
||
return r.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
|
||
validateEmail(e){return/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)}
|
||
|
||
validateCC(n){n=n.replace(/\s/g,'');if(!/^\d{13,19}$/.test(n))return false;let d=[...n].map(Number);for(let i=d.length-2;i>=0;i-=2){d[i]*=2;if(d[i]>9)d[i]-=9}return d.reduce((a,b)=>a+b,0)%10===0}
|
||
|
||
/* ── CSRF ── */
|
||
generateCsrf(){
|
||
const buf=new Uint8Array(16);crypto.getRandomValues(buf);
|
||
this.csrfToken=[...buf].map(b=>b.toString(16).padStart(2,'0')).join('');
|
||
this.csrfCreated=Date.now();return this.csrfToken;
|
||
}
|
||
validateCsrf(token){
|
||
if(!this.csrfToken||this.csrfToken!==token)return false;
|
||
if(Date.now()-this.csrfCreated>3600000)return false;
|
||
return true;
|
||
}
|
||
rotateCsrf(){return this.generateCsrf()}
|
||
|
||
/* ── Rate Limiting ── */
|
||
checkRate(key,max=15,window=60000){
|
||
const now=Date.now();
|
||
if(!this.rateLimits[key])this.rateLimits[key]=[];
|
||
this.rateLimits[key]=this.rateLimits[key].filter(t=>now-t<window);
|
||
if(this.rateLimits[key].length>=max)return false;
|
||
this.rateLimits[key].push(now);return true;
|
||
}
|
||
|
||
/* ── Order Validation ── */
|
||
validateOrder(items,statedTotal){
|
||
const computed=items.reduce((s,i)=>s+parseFloat(i.price)*parseInt(i.qty),0);
|
||
if(Math.abs(computed-parseFloat(statedTotal))>0.01)return{valid:false,error:`Total mismatch: computed $${computed.toFixed(2)} vs stated $${statedTotal}`};
|
||
for(const i of items){const q=parseInt(i.qty);if(q<1||q>99)return{valid:false,error:`Invalid qty for ${i.name}`};if(parseFloat(i.price)<0)return{valid:false,error:`Negative price: ${i.name}`}}
|
||
return{valid:true,computedTotal:computed};
|
||
}
|
||
|
||
/* ── Persistence ── */
|
||
_persist(){try{localStorage.setItem('fortkart_audit',JSON.stringify({logs:this.logs,prevHash:this.prevHash}))}catch(e){}}
|
||
_restoreFromStorage(){try{const d=JSON.parse(localStorage.getItem('fortkart_audit'));if(d&&d.logs){this.logs=d.logs;this.prevHash=d.prevHash||'GENESIS';return this.logs.length}catch(e){}return 0}
|
||
}
|
||
|
||
/* ================================================================
|
||
PRODUCT DATA — with SKUs, weights, freight, tiers, params
|
||
================================================================ */
|
||
const PRODUCTS=[
|
||
// BALL SCREWS
|
||
{id:1,sku:'BS-1605-1000',name:'Rolled Ball Screw 1605 — 1000mm',price:89.99,weight:1.2,freight:false,category:'ball-screws',image:'https://picsum.photos/seed/bs1605/400/400.jpg',description:'C7 precision rolled ball screw, 16mm dia x 5mm lead, 1000mm thread length. Chrome-plated shaft with preloaded single nut. Suitable for X/Y axes on benchtop CNC mills.',stock:45,rating:4.7,reviews:312,featured:true,specs:['16mm dia','5mm lead','C7 grade','1000mm L','Chrome-plated'],params:{diameter:'16mm',lead:'5mm',grade:'C7'},tiers:[{min:1,p:89.99},{min:5,p:82.99},{min:20,p:74.99}]},
|
||
{id:2,sku:'BS-2005G-800',name:'Precision Ground Ball Screw 2005 — 800mm',price:274.99,weight:2.8,freight:false,category:'ball-screws',image:'https://picsum.photos/seed/bs2005g/400/400.jpg',description:'C3 precision ground ball screw, 20mm dia x 5mm lead, 800mm thread length. Hardened HRC 58-62. Double-nut preload for zero backlash. Ideal for plasma tables.',stock:12,rating:4.9,reviews:87,featured:true,specs:['20mm dia','5mm lead','C3 ground','800mm L','HRC 58-62','Double nut'],params:{diameter:'20mm',lead:'5mm',grade:'C3'}},
|
||
{id:3,sku:'BS-2510-1200',name:'Rolled Ball Screw 2510 — 1200mm',price:164.99,weight:3.5,freight:true,category:'ball-screws',image:'https://picsum.photos/seed/bs2510/400/400.jpg',description:'C7 rolled ball screw, 25mm dia x 10mm lead, 1200mm thread length. High-pitch for rapid traverse. Flanged single nut with dust wipers. Common Bridgeport retrofit.',stock:23,rating:4.6,reviews:156,specs:['25mm dia','10mm lead','C7 grade','1200mm L','Flanged nut'],params:{diameter:'25mm',lead:'10mm',grade:'C7'}},
|
||
{id:4,sku:'BS-3205G-1500',name:'Precision Ground Ball Screw 3205 — 1500mm',price:489.99,weight:6.2,freight:true,category:'ball-screws',image:'https://picsum.photos/seed/bs3205/400/400.jpg',description:'C3 ground ball screw, 32mm dia x 5mm lead, 1500mm length. Heavy-duty for knee mills. Induction-hardened, double-nut preload. OEM cross-ref for Fanuc/Bosch Rexroth.',stock:7,rating:4.8,reviews:64,featured:true,specs:['32mm dia','5mm lead','C3 ground','1500mm L','OEM cross-ref'],params:{diameter:'32mm',lead:'5mm',grade:'C3'}},
|
||
{id:5,sku:'BS-1204-400',name:'Miniature Ball Screw 1204 — 400mm',price:54.99,weight:0.4,freight:false,category:'ball-screws',image:'https://picsum.photos/seed/bs1204/400/400.jpg',description:'C7 rolled miniature ball screw, 12mm dia x 4mm lead, 400mm length. Compact for Z-axis on small engravers and CO2 laser cutters. Aluminum nut body.',stock:68,rating:4.5,reviews:201,specs:['12mm dia','4mm lead','C7 grade','400mm L','Aluminum nut'],params:{diameter:'12mm',lead:'4mm',grade:'C7'},tiers:[{min:1,p:54.99},{min:5,p:49.99},{min:25,p:42.99}]},
|
||
|
||
// LEAD NUTS
|
||
{id:6,sku:'BN-1605-S',name:'Flanged Ball Nut — 1605 Single',price:32.99,weight:0.15,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/bn1605/400/400.jpg',description:'Replacement single ball nut for 1605 ball screws. Flanged with 4x M5 bolt pattern. Adjustable preload via spacer. Includes return tube and 3mm ball set.',stock:120,rating:4.4,reviews:178,specs:['1605 compat','Flanged','M5 mount','3mm balls'],params:{size:'1605',type:'Ball'},tiers:[{min:1,p:32.99},{min:10,p:28.99},{min:50,p:24.99}]},
|
||
{id:7,sku:'BN-2005-DBL',name:'Double Ball Nut — 2005 Preloaded',price:79.99,weight:0.35,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/bn2005d/400/400.jpg',description:'Double-nut assembly for 2005 ball screws with adjustable preload. Eliminates axial backlash. Two flanged nuts connected by threaded spacer ring. Steel balls included.',stock:34,rating:4.7,reviews:92,featured:true,specs:['2005 compat','Double nut','Adjustable preload','Zero backlash'],params:{size:'2005',type:'Ball'}},
|
||
{id:8,sku:'BN-2510-S',name:'Flanged Ball Nut — 2510 Single',price:44.99,weight:0.5,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/bn2510/400/400.jpg',description:'Heavy-duty flanged ball nut for 2510 screws. Cast iron body with lubrication port. 6x M6 bolt pattern. Dust wiper standard.',stock:56,rating:4.5,reviews:67,specs:['2510 compat','Cast iron','M6 mount','Lube port'],params:{size:'2510',type:'Ball'}},
|
||
{id:9,sku:'AN-12ACME',name:'Bronze ACME Nut — 1/2"-10 TPI',price:18.99,weight:0.08,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/acmenut/400/400.jpg',description:'Solid bronze ACME lead nut for 1/2"-10 TPI leadscrews. Self-lubricating C95400 aluminum bronze. 2-bolt flange mount. Drop-in for import lathe cross-slide nuts.',stock:210,rating:4.3,reviews:445,specs:['1/2"-10 TPI','C95400 bronze','Self-lube','2-bolt flange'],params:{size:'1/2"',type:'ACME'},tiers:[{min:1,p:18.99},{min:10,p:15.99},{min:100,p:12.99}]},
|
||
{id:10,sku:'AN-34AB',name:'Anti-Backlash Nut Kit — 3/4"-6 TPI',price:29.99,weight:0.06,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/abnut/400/400.jpg',description:'Delrin anti-backlash nut for 3/4"-6 ACME leadscrew. Split-nut with spring preload eliminates backlash without binding. Includes mounting bracket. Popular for Sherline/Taig Z-axis.',stock:95,rating:4.6,reviews:234,specs:['3/4"-6 TPI','Delrin','Spring preload','Zero backlash'],params:{size:'3/4"',type:'Anti-Backlash'}},
|
||
|
||
// SPINDLES
|
||
{id:11,sku:'SP-22K-ER20',name:'2.2kW ER20 Air-Cooled Spindle',price:189.99,weight:3.8,freight:true,category:'spindles',image:'https://picsum.photos/seed/sp22er20/400/400.jpg',description:'2.2kW air-cooled ER20 collet spindle, 24000 RPM max. 4-bearing 7005C angular contact pairs in DB preload. 80mm mount. Includes ER20 chuck and collet set (3-12mm). VFD required.',stock:28,rating:4.6,reviews:389,featured:true,specs:['2.2kW','ER20','24k RPM','80mm mount','4-bearing','Collets incl.'],params:{power:'2.2kW',taper:'ER20',cooling:'Air'}},
|
||
{id:12,sku:'SP-32K-ER20W',name:'3.2kW ER20 Water-Cooled Spindle',price:269.99,weight:4.5,freight:true,category:'spindles',image:'https://picsum.photos/seed/sp32er20/400/400.jpg',description:'3.2kW water-cooled ER20 spindle, 18000 RPM. Water jacket for sustained heavy cuts in aluminum and steel. 80mm mount, drop-in upgrade from 2.2kW.',stock:15,rating:4.8,reviews:156,specs:['3.2kW','ER20','18k RPM','Water-cooled','80mm mount'],params:{power:'3.2kW',taper:'ER20',cooling:'Water'}},
|
||
{id:13,sku:'SP-BT30-55K',name:'BT30 5.5kW Belt-Drive Spindle',price:1899.99,weight:32,freight:true,category:'spindles',image:'https://picsum.photos/seed/bt30sp/400/400.jpg',description:'BT30 taper 5.5kW belt-drive spindle for VMC retrofit. 8000 RPM, grease-lubricated angular contacts. Includes belt pulley set, drawbar, and tool release cylinder. Replaces Haas/Fadal/Daewoo spindles.',stock:4,rating:4.9,reviews:23,featured:true,specs:['BT30 taper','5.5kW','8k RPM','Belt-drive','Drawbar incl.'],params:{power:'5.5kW',taper:'BT30',cooling:'Grease'}},
|
||
{id:14,sku:'SP-08K-ER11',name:'ER11 800W High-Speed Spindle',price:69.99,weight:0.8,freight:false,category:'spindles',image:'https://picsum.photos/seed/sp800er11/400/400.jpg',description:'800W ER11 spindle for PCB routing and engraving. 52mm mount, up to 36000 RPM. Quiet, low vibration. Includes 3.175mm ER11 collet. Upgrade for older desktop CNCs.',stock:82,rating:4.4,reviews:567,specs:['800W','ER11','36k RPM','52mm mount','PCB/engrave'],params:{power:'0.8kW',taper:'ER11',cooling:'Air'},tiers:[{min:1,p:69.99},{min:5,p:64.99}]},
|
||
{id:15,sku:'SP-40K-ER25W',name:'4kW ER25 Water-Cooled Spindle',price:389.99,weight:5.8,freight:true,category:'spindles',image:'https://picsum.photos/seed/sp4ker25/400/400.jpg',description:'4kW ER25 water-cooled spindle, 18000 RPM. ER25 accepts up to 16mm shank. 85mm mount, 6-bearing cartridge. For aluminum profiling and steel finishing on gantry routers.',stock:9,rating:4.7,reviews:78,specs:['4kW','ER25','18k RPM','85mm mount','6-bearing'],params:{power:'4.0kW',taper:'ER25',cooling:'Water'}},
|
||
|
||
// CONTROLLERS
|
||
{id:16,sku:'CT-M3USB-5',name:'Mach3 USB Breakout Board — 5 Axis',price:49.99,weight:0.12,freight:false,category:'controllers',image:'https://picsum.photos/seed/m3usb/400/400.jpg',description:'USB CNC breakout board for Mach3/LinuxCNC. 5-axis stepper output, 4 relay outputs, 6 opto-isolated inputs. 0-10V analog spindle speed. Plug-and-play with NEMA 23 drivers.',stock:73,rating:4.5,reviews:423,specs:['USB','5-axis','Opto-isolated','Relay x4','0-10V spindle'],params:{interface:'USB',axes:'5'},tiers:[{min:1,p:49.99},{min:10,p:44.99}]},
|
||
{id:17,sku:'CT-ETH6',name:'Ethernet Motion Controller — 6 Axis',price:199.99,weight:0.2,freight:false,category:'controllers',image:'https://picsum.photos/seed/ethctrl/400/400.jpg',description:'Ethernet motion controller with 1MHz step rate. 6-axis step/dir or servo output. Hardware motion planning, probe input, spindle encoder feedback, MPG port. Supports rigid tapping.',stock:19,rating:4.8,reviews:134,featured:true,specs:['Ethernet','6-axis','1MHz step','Probe input','MPG port'],params:{interface:'Ethernet',axes:'6'}},
|
||
{id:18,sku:'CT-N23-3PK',name:'NEMA 23 Stepper Driver Set (3x)',price:89.99,weight:0.6,freight:false,category:'controllers',image:'https://picsum.photos/seed/drv23set/400/400.jpg',description:'Set of 3 TB6600-style stepper drivers. 2.5A-4.2A selectable, microstepping to 1/32. Opto-isolated inputs, auto half-current hold. Compatible with Mach3/LinuxCNC/GRBL.',stock:41,rating:4.4,reviews:289,specs:['3x drivers','2.5-4.2A','1/32 microstep','Opto-isolated'],params:{interface:'Parallel',axes:'3'},tiers:[{min:1,p:89.99},{min:5,p:79.99}]},
|
||
{id:19,sku:'CT-VFD-22',name:'2.2kW VFD — Single Phase Input',price:119.99,weight:1.5,freight:false,category:'controllers',image:'https://picsum.photos/seed/vfd22/400/400.jpg',description:'2.2kW VFD, 220V single-phase in to 3-phase out. PID speed via 0-10V or PWM. Built-in braking transistor, RS485 Modbus. Pre-programmed CNC spindle profiles. EMC filter included.',stock:31,rating:4.6,reviews:198,specs:['2.2kW','1ph in/3ph out','0-10V/PWM','Modbus RTU'],params:{interface:'Analog',axes:'N/A'}},
|
||
{id:20,sku:'CT-GRBLESP',name:'GRBL ESP32 Wireless Controller',price:39.99,weight:0.05,freight:false,category:'controllers',image:'https://picsum.photos/seed/grblesp/400/400.jpg',description:'ESP32-based GRBL controller with WiFi and Bluetooth. 4-axis TMC2209 driver sockets, 4 limit inputs, spindle PWM. Web UI for jogging and file upload. Flash GRBL-ESP32 firmware.',stock:55,rating:4.3,reviews:312,specs:['ESP32','WiFi + BT','4-axis','TMC2209','Web UI'],params:{interface:'WiFi',axes:'4'},tiers:[{min:1,p:39.99},{min:5,p:34.99},{min:25,p:29.99}]},
|
||
|
||
// LINEAR GUIDES
|
||
{id:21,sku:'LG-HGR15-600',name:'Linear Guide Rail HGR15 — 600mm Pair',price:54.99,weight:2.4,freight:true,category:'linear-guides',image:'https://picsum.photos/seed/hgr15/400/400.jpg',description:'Pair of HGR15 linear guide rails, 600mm length with 4 slider blocks. Chrome-plated steel, preloaded 4-row ball circulation. Hiwin HGR15 drop-in replacement.',stock:38,rating:4.6,reviews:176,specs:['HGR15','600mm','4 blocks','Chrome-plated'],params:{size:'HGR15'}},
|
||
{id:22,sku:'LG-HGR20-1000',name:'Linear Guide Rail HGR20 — 1000mm Pair',price:109.99,weight:5.2,freight:true,category:'linear-guides',image:'https://picsum.photos/seed/hgr20/400/400.jpg',description:'Pair of HGR20 linear guide rails, 1000mm with 4 blocks. Higher load capacity for gantry beams and heavy saddles. Sealed blocks with grease nipples.',stock:17,rating:4.7,reviews:89,specs:['HGR20','1000mm','4 blocks','Sealed'],params:{size:'HGR20'}},
|
||
{id:23,sku:'LG-HGH20CA-800',name:'Linear Guide Rail HGH20CA — 800mm Pair',price:134.99,weight:4.8,freight:true,category:'linear-guides',image:'https://picsum.photos/seed/hgh20ca/400/400.jpg',description:'High-precision HGH20CA wide-block guides, 800mm. Flanged wide blocks for increased moment capacity. Double-lip seals. Suitable for Z-axis with high moment loads.',stock:11,rating:4.8,reviews:52,featured:true,specs:['HGH20CA','800mm','Wide block','Double seal'],params:{size:'HGH20CA'}},
|
||
|
||
// TOOLING
|
||
{id:24,sku:'TL-ER20-9PC',name:'ER20 Collet Set — 9 Piece',price:44.99,weight:0.3,freight:false,category:'tooling',image:'https://picsum.photos/seed/er20set/400/400.jpg',description:'Complete ER20 collet set: 1, 2, 3, 4, 5, 6, 8, 10, 12mm. Spring steel, 0.015mm runout tolerance. Precision ground bore. Storage case included.',stock:62,rating:4.5,reviews:356,specs:['ER20','9 sizes','0.015mm TIR','Spring steel','Case incl.'],params:{type:'Collets'},tiers:[{min:1,p:44.99},{min:5,p:39.99}]},
|
||
{id:25,sku:'TL-BT30ER32',name:'BT30 ER32 Tool Holder — Face Mill',price:89.99,weight:0.9,freight:false,category:'tooling',image:'https://picsum.photos/seed/bt30er32/400/400.jpg',description:'BT30 shank ER32 collet chuck. 40mm gauge length, G2.5 balanced at 12k RPM. Includes ER32 nut and bearing. Direct replacement for BT30 tool magazines.',stock:14,rating:4.7,reviews:67,specs:['BT30','ER32','G2.5 balanced','40mm gauge'],params:{type:'Tool Holder'}},
|
||
{id:26,sku:'TL-EM4F-6PC',name:'Carbide End Mill Set — 4 Flute (6pc)',price:79.99,weight:0.25,freight:false,category:'tooling',image:'https://picsum.photos/seed/emset4f/400/400.jpg',description:'6-piece solid carbide 4-flute set: 3, 4, 5, 6, 8, 10mm. AlTiN coated for aluminum and steel. 50mm cut length, 75mm OAL. Micrograin carbide.',stock:29,rating:4.6,reviews:189,specs:['6 pieces','4-flute','AlTiN coated','Micrograin','3-10mm'],params:{type:'End Mills'},tiers:[{min:1,p:79.99},{min:5,p:72.99}]},
|
||
|
||
// ACCESSORIES
|
||
{id:27,sku:'AC-CPL-55-5PK',name:'Flexible Coupling Set — 5x5mm (5pc)',price:12.99,weight:0.1,freight:false,category:'accessories',image:'https://picsum.photos/seed/coupling55/400/400.jpg',description:'Set of 5 aluminum beam couplings, 5mm x 5mm bore. Zero-backlash spiral cuts for misalignment compensation. Connect stepper motors to ball screws.',stock:150,rating:4.3,reviews:534,specs:['5x5mm bore','Aluminum','Zero backlash','5 pieces'],params:{type:'Coupling'},tiers:[{min:1,p:12.99},{min:10,p:9.99},{min:50,p:7.99}]},
|
||
{id:28,sku:'AC-N23-28',name:'NEMA 23 Stepper Motor — 2.8A 270oz-in',price:34.99,weight:0.9,freight:false,category:'accessories',image:'https://picsum.photos/seed/nema23/400/400.jpg',description:'NEMA 23 stepper, 2.8A/phase, 1.8° step angle. 1.9 Nm holding torque. 4-lead bipolar. Double-shaft for encoder. Common retrofit motor for benchtop mills.',stock:88,rating:4.5,reviews:412,specs:['NEMA 23','2.8A','1.9 Nm','1.8° step','4-lead'],params:{type:'Motor'},tiers:[{min:1,p:34.99},{min:3,p:32.99},{min:10,p:29.99}]},
|
||
{id:29,sku:'AC-LSW-6PK',name:'Limit Switch Set — Mechanical (6pc)',price:14.99,weight:0.3,freight:false,category:'accessories',image:'https://picsum.photos/seed/limsw/400/400.jpg',description:'Set of 6 mechanical roller limit switches. SPDT, 10A. Metal L-bracket mounts. Wired normally-closed for CNC safety.',stock:110,rating:4.2,reviews:378,specs:['6 pieces','Roller lever','SPDT','L-bracket incl.'],params:{type:'Switch'},tiers:[{min:1,p:14.99},{min:10,p:12.99}]},
|
||
{id:30,sku:'AC-WCK-22',name:'Water Cooling Kit — Spindle 2.2kW',price:39.99,weight:2.0,freight:false,category:'accessories',image:'https://picsum.photos/seed/waterkit/400/400.jpg',description:'Complete water cooling kit. 15L tank, submersible pump 1200L/hr, 8mm silicone hose 3m, 4 clamps, coolant fluid. Plug-and-play.',stock:37,rating:4.4,reviews:223,specs:['15L tank','1200L/hr pump','8mm hose','Coolant incl.'],params:{type:'Cooling'}},
|
||
];
|
||
|
||
const CATEGORIES=['all',...new Set(PRODUCTS.map(p=>p.category))];
|
||
const CAT_LABELS={all:'All Parts','ball-screws':'Ball Screws','lead-nuts':'Lead Nuts',spindles:'Spindles',controllers:'Controllers','linear-guides':'Linear Guides',tooling:'Tooling',accessories:'Accessories'};
|
||
const CAT_LABEL_SHORT={'ball-screws':'Ball Screw','lead-nuts':'Lead Nut',spindles:'Spindle',controllers:'Controller','linear-guides':'Linear Guide',tooling:'Tooling',accessories:'Accessory'};
|
||
|
||
/* Parametric filter definitions per category */
|
||
const PARAM_DEFS={
|
||
'ball-screws':[
|
||
{key:'diameter',label:'Diameter',values:['12mm','16mm','20mm','25mm','32mm']},
|
||
{key:'lead',label:'Lead',values:['4mm','5mm','10mm']},
|
||
{key:'grade',label:'Grade',values:['C3','C7']}
|
||
],
|
||
'lead-nuts':[
|
||
{key:'type',label:'Type',values:['Ball','ACME','Anti-Backlash']},
|
||
{key:'size',label:'Size',values:['1605','2005','2510','1/2"','3/4"']}
|
||
],
|
||
'spindles':[
|
||
{key:'taper',label:'Taper',values:['ER11','ER20','ER25','BT30']},
|
||
{key:'cooling',label:'Cooling',values:['Air','Water','Grease']},
|
||
{key:'power',label:'Power',values:['0.8kW','2.2kW','3.2kW','4.0kW','5.5kW']}
|
||
],
|
||
'controllers':[
|
||
{key:'interface',label:'Interface',values:['USB','Ethernet','WiFi','Parallel','Analog']},
|
||
{key:'axes',label:'Axes',values:['3','4','5','6','N/A']}
|
||
],
|
||
'linear-guides':[
|
||
{key:'size',label:'Size',values:['HGR15','HGR20','HGH20CA']}
|
||
],
|
||
'tooling':[
|
||
{key:'type',label:'Type',values:['Collets','Tool Holder','End Mills']}
|
||
],
|
||
'accessories':[
|
||
{key:'type',label:'Type',values:['Coupling','Motor','Switch','Cooling']}
|
||
]
|
||
};
|
||
|
||
/* ================================================================
|
||
APPLICATION STATE
|
||
================================================================ */
|
||
const state={
|
||
cart:[],view:'shop',category:'all',
|
||
sessionId:crypto.randomUUID?crypto.randomUUID():'s-'+Date.now()+'-'+Math.random().toString(36).slice(2),
|
||
orderHistory:[],paramSelections:{}
|
||
};
|
||
let sec; // SecurityEngine instance
|
||
|
||
/* ================================================================
|
||
UTILITY
|
||
================================================================ */
|
||
function toast(msg,type='info'){
|
||
const icons={success:'fa-check-circle',error:'fa-exclamation-circle',info:'fa-info-circle',warn:'fa-exclamation-triangle'};
|
||
const el=document.createElement('div');el.className=`toast toast-${type}`;
|
||
el.innerHTML=`<i class="fa-solid ${icons[type]||icons.info}"></i><span>${msg}</span>`;
|
||
document.getElementById('toast-container').appendChild(el);
|
||
setTimeout(()=>{el.style.opacity='0';el.style.transform='translateX(100%)';el.style.transition='all .3s';setTimeout(()=>el.remove(),300)},3500);
|
||
}
|
||
function toggleMobileNav(){document.getElementById('nav-links').classList.toggle('open')}
|
||
function closeMobileNav(){document.getElementById('nav-links').classList.remove('open')}
|
||
|
||
/* Get effective unit price based on quantity (tier pricing) */
|
||
function getUnitPrice(product,qty){
|
||
if(!product.tiers)return product.price;
|
||
let price=product.price;
|
||
for(const t of product.tiers){if(qty>=t.min)price=t.p}
|
||
return price;
|
||
}
|
||
|
||
/* ================================================================
|
||
NAVIGATION
|
||
================================================================ */
|
||
function navigate(view){
|
||
state.view=view;
|
||
document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));
|
||
const t=document.getElementById('view-'+view);if(t)t.classList.add('active');
|
||
document.querySelectorAll('.nav-link').forEach(l=>l.classList.toggle('active',l.dataset.view===view));
|
||
if(view==='audit')renderAuditLog();
|
||
if(view==='security')renderSecurityDashboard();
|
||
window.scrollTo({top:0,behavior:'smooth'});
|
||
}
|
||
|
||
/* ================================================================
|
||
PRODUCTS
|
||
================================================================ */
|
||
function renderCategoryFilters(){
|
||
document.getElementById('cat-filters').innerHTML=CATEGORIES.map(c=>
|
||
`<button class="cat-pill ${c===state.category?'active':''}" onclick="setCategory('${c}')">${CAT_LABELS[c]||c}</button>`
|
||
).join('');
|
||
}
|
||
|
||
function setCategory(cat){
|
||
state.category=cat;state.paramSelections={};
|
||
renderCategoryFilters();renderParamFilters();renderProducts();
|
||
sec.logAudit('CATEGORY_FILTER',{category:cat});
|
||
}
|
||
|
||
function renderParamFilters(){
|
||
const container=document.getElementById('param-filters');
|
||
const defs=PARAM_DEFS[state.category];
|
||
if(!defs||state.category==='all'){container.style.display='none';container.innerHTML='';return}
|
||
container.style.display='flex';container.style.flexWrap='wrap';container.style.gap='1rem';container.style.alignItems='flex-start';
|
||
container.innerHTML=defs.map(d=>{
|
||
const selected=state.paramSelections[d.key]||[];
|
||
const chips=d.values.map(v=>`<button class="param-chip ${selected.includes(v)?'active':''}" onclick="toggleParam('${d.key}','${v}')">${v}</button>`).join('');
|
||
return `<div><div style="font-size:.68rem;color:var(--fg-muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em;margin-bottom:.35rem">${d.label}</div><div style="display:flex;gap:.3rem;flex-wrap:wrap">${chips}</div></div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function toggleParam(key,value){
|
||
if(!state.paramSelections[key])state.paramSelections[key]=[];
|
||
const arr=state.paramSelections[key];
|
||
const idx=arr.indexOf(value);
|
||
if(idx>-1)arr.splice(idx,1);else arr.push(value);
|
||
if(!arr.length)delete state.paramSelections[key];
|
||
renderParamFilters();renderProducts();
|
||
}
|
||
|
||
function renderProducts(){
|
||
const search=(document.getElementById('search-input').value||'').toLowerCase();
|
||
const sort=document.getElementById('sort-select').value;
|
||
let items=PRODUCTS.filter(p=>{
|
||
if(state.category!=='all'&&p.category!==state.category)return false;
|
||
const specStr=(p.specs||[]).join(' ').toLowerCase();
|
||
const matchSearch=!search||p.name.toLowerCase().includes(search)||p.description.toLowerCase().includes(search)||p.sku.toLowerCase().includes(search)||specStr.includes(search)||p.category.includes(search);
|
||
if(!matchSearch)return false;
|
||
/* Parametric filters: OR within key, AND between keys */
|
||
for(const[key,values] of Object.entries(state.paramSelections)){
|
||
if(!values.length)continue;
|
||
if(!p.params||!values.includes(p.params[key]))return false;
|
||
}
|
||
return true;
|
||
});
|
||
switch(sort){
|
||
case'price-asc':items.sort((a,b)=>a.price-b.price);break;
|
||
case'price-desc':items.sort((a,b)=>b.price-a.price);break;
|
||
case'rating':items.sort((a,b)=>b.rating-a.rating);break;
|
||
case'name':items.sort((a,b)=>a.name.localeCompare(b.name));break;
|
||
default:items.sort((a,b)=>(b.featured?1:0)-(a.featured?1:0));
|
||
}
|
||
const grid=document.getElementById('product-grid');
|
||
const noRes=document.getElementById('no-results');
|
||
if(!items.length){grid.innerHTML='';noRes.style.display='block';return}
|
||
noRes.style.display='none';
|
||
grid.innerHTML=items.map(p=>{
|
||
const inCart=state.cart.find(c=>c.product.id===p.id);
|
||
const effectivePrice=getUnitPrice(p,inCart?inCart.qty:1);
|
||
return `<div class="product-card" onclick="openProduct(${p.id})">
|
||
<div class="img-wrap">
|
||
<img src="${p.image}" alt="${p.name}" loading="lazy">
|
||
<span class="cat-tag">${CAT_LABEL_SHORT[p.category]||p.category}</span>
|
||
${p.featured?'<span style="position:absolute;top:.75rem;right:.75rem;background:var(--accent);color:var(--bg);font-size:.6rem;font-weight:700;padding:.2rem .5rem;border-radius:.25rem">POPULAR</span>':''}
|
||
<span class="sku-tag">${p.sku}</span>
|
||
</div>
|
||
<div style="padding:1rem">
|
||
<h3 style="font-size:.88rem;font-weight:600;margin-bottom:.35rem;line-height:1.3;min-height:2.4em">${p.name}</h3>
|
||
<div style="margin-bottom:.5rem;min-height:1.2em">${(p.specs||[]).slice(0,3).map(s=>`<span class="spec-tag">${s}</span>`).join('')}</div>
|
||
<div class="stars" style="margin-bottom:.5rem">${'<i class="fa-solid fa-star"></i>'.repeat(Math.floor(p.rating))}${p.rating%1?'<i class="fa-solid fa-star-half-stroke"></i>':''} <span style="color:var(--fg-muted);font-size:.7rem;margin-left:.3rem">(${p.reviews})</span></div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.3rem">
|
||
<div>
|
||
<span style="font-size:1.1rem;font-weight:700;color:var(--accent)">$${effectivePrice.toFixed(2)}</span>
|
||
${p.tiers&&p.tiers.length>1?`<span style="font-size:.65rem;color:var(--fg-muted);margin-left:.3rem">${inCart&&inCart.qty>=p.tiers[p.tiers.length-1].min?'bulk':'from $'+p.tiers[p.tiers.length-1].p.toFixed(2)}</span>`:''}
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:.4rem">
|
||
${p.freight?'<span class="freight-badge"><i class="fa-solid fa-truck" style="margin-right:.2rem"></i>Freight</span>':''}
|
||
<span style="font-size:.68rem;color:${p.stock<10?'var(--warn)':'var(--fg-muted)'}">${p.stock<10?p.stock+' left':p.stock+' in stock'}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
/* ================================================================
|
||
PRODUCT DETAIL MODAL
|
||
================================================================ */
|
||
function openProduct(id){
|
||
const p=PRODUCTS.find(x=>x.id===id);if(!p)return;
|
||
sec.logAudit('PRODUCT_VIEW',{product_id:p.id,sku:p.sku});
|
||
const inCart=state.cart.find(c=>c.product.id===p.id);
|
||
const unitPrice=getUnitPrice(p,inCart?inCart.qty:1);
|
||
document.getElementById('product-modal-content').innerHTML=`
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0" class="modal-grid">
|
||
<div style="overflow:hidden;border-radius:1rem 0 0 1rem;position:relative">
|
||
<img src="${p.image}" alt="${p.name}" style="width:100%;height:100%;object-fit:cover;min-height:320px">
|
||
<span style="position:absolute;bottom:.75rem;left:.75rem;background:rgba(9,9,11,.85);color:var(--fg-muted);font-size:.75rem;font-weight:500;padding:.3rem .6rem;border-radius:.3rem;font-family:'JetBrains Mono',monospace">${p.sku}</span>
|
||
</div>
|
||
<div style="padding:2rem;position:relative">
|
||
<button class="btn-icon" style="position:absolute;top:1rem;right:1rem" onclick="closeProductModal()"><i class="fa-solid fa-xmark"></i></button>
|
||
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.25rem">
|
||
<span style="font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--accent)">${p.category.replace('-',' ')}</span>
|
||
${p.freight?'<span class="freight-badge"><i class="fa-solid fa-truck" style="margin-right:.2rem"></i>Freight Quote Required</span>':''}
|
||
</div>
|
||
<h2 style="font-size:1.3rem;font-weight:800;margin-top:.4rem;letter-spacing:-.02em;line-height:1.2">${p.name}</h2>
|
||
<div class="stars" style="margin:.6rem 0">${'<i class="fa-solid fa-star"></i>'.repeat(Math.floor(p.rating))}${p.rating%1?'<i class="fa-solid fa-star-half-stroke"></i>':''} <span style="color:var(--fg-muted);font-size:.8rem;margin-left:.3rem">${p.rating} (${p.reviews} reviews)</span></div>
|
||
<p style="color:var(--fg-muted);font-size:.85rem;line-height:1.6;margin-bottom:1rem">${p.description}</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:1rem">${(p.specs||[]).map(s=>`<span class="spec-tag">${s}</span>`).join('')}</div>
|
||
<div style="display:flex;align-items:baseline;gap:.75rem;margin-bottom:.25rem">
|
||
<span style="font-size:2rem;font-weight:800;color:var(--accent)">$${unitPrice.toFixed(2)}</span>
|
||
<span style="font-size:.75rem;color:var(--fg-muted)">${p.weight} kg</span>
|
||
</div>
|
||
${p.tiers&&p.tiers.length>1?`<div style="background:var(--bg);border:1px solid var(--border);border-radius:.5rem;padding:.6rem .75rem;margin-bottom:1rem">
|
||
<div style="font-size:.7rem;font-weight:600;color:var(--fg-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem">Quantity Pricing</div>
|
||
${p.tiers.map(t=>`<div class="tier-row ${inCart&&inCart.qty>=t.min?'active-tier':''}"><span>${t.min}+ units</span><span>$${t.p.toFixed(2)} ea</span></div>`).join('')}
|
||
</div>`:''}
|
||
<p style="font-size:.8rem;color:${p.stock<10?'var(--warn)':'var(--success)'};margin-bottom:1.25rem"><i class="fa-solid ${p.stock<10?'fa-exclamation-triangle':'fa-check-circle'}" style="margin-right:.35rem"></i>${p.stock<10?'Only '+p.stock+' remaining — order soon':p.stock+' units in stock'}</p>
|
||
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
|
||
${inCart?`<button class="btn-outline" onclick="closeProductModal()"><i class="fa-solid fa-check" style="margin-right:.35rem;color:var(--success)"></i>In Cart (${inCart.qty})</button>`:
|
||
`<button class="btn-primary" onclick="addToCart(${p.id});closeProductModal()"><i class="fa-solid fa-cart-plus" style="margin-right:.35rem"></i>Add to Cart</button>`}
|
||
<button class="btn-outline" onclick="addToCart(${p.id});closeProductModal();goToCheckout()">Buy Now</button>
|
||
</div>
|
||
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border);font-size:.7rem;color:var(--fg-muted);display:flex;flex-direction:column;gap:.25rem">
|
||
<span><i class="fa-solid fa-shield-halved" style="width:16px;color:var(--accent)"></i> Sanitized through JS security engine</span>
|
||
<span><i class="fa-solid fa-link" style="width:16px;color:var(--accent)"></i> View logged to SHA-256 audit chain</span>
|
||
<span><i class="fa-solid fa-truck" style="width:16px;color:var(--accent)"></i> ${p.freight?'Freight quote required — ships within 48hr':'Ships within 48 hours'}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<style>@media(max-width:640px){.modal-grid{grid-template-columns:1fr!important}.modal-grid img{border-radius:1rem 1rem 0 0!important;min-height:200px!important}}</style>`;
|
||
document.getElementById('product-modal').classList.add('open');
|
||
}
|
||
function closeProductModal(){document.getElementById('product-modal').classList.remove('open')}
|
||
document.getElementById('product-modal').addEventListener('click',e=>{if(e.target.id==='product-modal')closeProductModal()});
|
||
|
||
/* ================================================================
|
||
CART — with tier pricing and stock decrement
|
||
================================================================ */
|
||
async function addToCart(id){
|
||
if(!sec.checkRate('add_to_cart')){toast('Rate limited — slow down','warn');return}
|
||
const p=PRODUCTS.find(x=>x.id===id);if(!p)return;
|
||
if(p.freight){toast(`${p.sku} requires a freight quote — contact sales`,'warn');sec.logAudit('FREIGHT_BLOCKED',{sku:p.sku});return}
|
||
const existing=state.cart.find(c=>c.product.id===id);
|
||
if(existing){if(existing.qty>=p.stock){toast('Maximum stock reached','warn');return}existing.qty++}
|
||
else{if(p.stock<1){toast('Out of stock','error');return}state.cart.push({product:p,qty:1})}
|
||
await sec.logAudit('ADD_TO_CART',{sku:p.sku,qty:1,price:p.price});
|
||
toast(`${p.sku} added to cart`,'success');renderCart();renderProducts();
|
||
}
|
||
async function removeFromCart(id){
|
||
const idx=state.cart.findIndex(c=>c.product.id===id);if(idx===-1)return;
|
||
const item=state.cart[idx];
|
||
await sec.logAudit('REMOVE_FROM_CART',{sku:item.product.sku,name:item.product.name});
|
||
state.cart.splice(idx,1);toast('Item removed','info');renderCart();renderProducts();
|
||
}
|
||
async function updateQty(id,delta){
|
||
const item=state.cart.find(c=>c.product.id===id);if(!item)return;
|
||
const nq=item.qty+delta;
|
||
if(nq<1){await removeFromCart(id);return}
|
||
if(nq>item.product.stock){toast('Maximum stock reached','warn');return}
|
||
item.qty=nq;
|
||
await sec.logAudit('QTY_CHANGE',{sku:item.product.sku,new_qty:nq});
|
||
renderCart();renderProducts();
|
||
}
|
||
function getCartTotal(){return state.cart.reduce((s,c)=>s+getUnitPrice(c.product,c.qty)*c.qty,0)}
|
||
|
||
function renderCart(){
|
||
const items=document.getElementById('cart-items');
|
||
const countEl=document.getElementById('cart-count');
|
||
const totalItems=state.cart.reduce((s,c)=>s+c.qty,0);
|
||
countEl.textContent=totalItems;countEl.style.display=totalItems>0?'flex':'none';
|
||
document.getElementById('checkout-btn').disabled=!state.cart.length;
|
||
if(!state.cart.length){
|
||
items.innerHTML=`<div style="text-align:center;padding:3rem 1rem;color:var(--fg-muted)"><i class="fa-solid fa-cart-shopping" style="font-size:2rem;margin-bottom:.75rem;display:block;opacity:.3"></i><p>Your cart is empty</p></div>`;
|
||
document.getElementById('cart-subtotal').textContent='$0.00';document.getElementById('cart-total').textContent='$0.00';return;
|
||
}
|
||
items.innerHTML=state.cart.map(c=>{
|
||
const up=getUnitPrice(c.product,c.qty);
|
||
return `<div style="display:flex;gap:.75rem;padding:.75rem 0;border-bottom:1px solid var(--border)">
|
||
<img src="${c.product.image}" alt="${c.product.sku}" style="width:56px;height:56px;border-radius:.5rem;object-fit:cover">
|
||
<div style="flex:1;min-width:0">
|
||
<h4 style="font-size:.75rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${c.product.name}">${c.product.sku}</h4>
|
||
<p style="font-size:.7rem;color:var(--fg-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.product.name}</p>
|
||
<div style="display:flex;align-items:center;gap:.75rem;margin-top:.4rem">
|
||
<div class="qty-ctrl"><button onclick="event.stopPropagation();updateQty(${c.product.id},-1)">−</button><span>${c.qty}</span><button onclick="event.stopPropagation();updateQty(${c.product.id},1)">+</button></div>
|
||
<span style="font-size:.7rem;color:var(--fg-muted)">$${up.toFixed(2)} ea</span>
|
||
<button style="background:none;border:none;color:var(--fg-muted);cursor:pointer;font-size:.7rem;margin-left:auto" onclick="event.stopPropagation();removeFromCart(${c.product.id})"><i class="fa-solid fa-trash"></i></button>
|
||
</div>
|
||
</div>
|
||
<span style="font-size:.85rem;font-weight:700;white-space:nowrap">$${(up*c.qty).toFixed(2)}</span>
|
||
</div>`;
|
||
}).join('');
|
||
const total=getCartTotal();
|
||
document.getElementById('cart-subtotal').textContent='$'+total.toFixed(2);
|
||
document.getElementById('cart-total').textContent='$'+total.toFixed(2);
|
||
}
|
||
function toggleCart(){document.getElementById('cart-sidebar').classList.toggle('open');document.getElementById('cart-overlay').classList.toggle('open')}
|
||
|
||
/* ================================================================
|
||
CHECKOUT — with loading state and stock decrement
|
||
================================================================ */
|
||
function goToCheckout(){
|
||
if(!state.cart.length){toast('Cart is empty','warn');return}
|
||
document.getElementById('cart-sidebar').classList.remove('open');document.getElementById('cart-overlay').classList.remove('open');
|
||
sec.logAudit('CHECKOUT_START',{items:state.cart.length,total:getCartTotal().toFixed(2)});
|
||
renderCheckoutForm();navigate('checkout');
|
||
}
|
||
|
||
function renderCheckoutForm(){
|
||
const total=getCartTotal();
|
||
document.getElementById('checkout-form').innerHTML=`
|
||
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;margin-bottom:1rem">
|
||
<h3 style="font-size:.9rem;font-weight:700;margin-bottom:1rem"><i class="fa-solid fa-user" style="color:var(--accent);margin-right:.5rem"></i>Customer Information</h3>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
|
||
<div><label class="field-label">First Name</label><input class="field" id="ch-first" placeholder="John"></div>
|
||
<div><label class="field-label">Last Name</label><input class="field" id="ch-last" placeholder="Doe"></div>
|
||
</div>
|
||
<div style="margin-top:.75rem"><label class="field-label">Email</label><input class="field" id="ch-email" type="email" placeholder="john@machine-shop.com"></div>
|
||
<div style="margin-top:.75rem"><label class="field-label">Company (optional)</label><input class="field" id="ch-company" placeholder="Acme Machine Co."></div>
|
||
</div>
|
||
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;margin-bottom:1rem">
|
||
<h3 style="font-size:.9rem;font-weight:700;margin-bottom:1rem"><i class="fa-solid fa-credit-card" style="color:var(--accent);margin-right:.5rem"></i>Payment</h3>
|
||
<div style="margin-bottom:.75rem"><label class="field-label">Card Number</label><input class="field" id="ch-card" placeholder="4242 4242 4242 4242" maxlength="19" oninput="formatCard(this)"></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
|
||
<div><label class="field-label">Expiry</label><input class="field" id="ch-expiry" placeholder="MM/YY" maxlength="5" oninput="formatExpiry(this)"></div>
|
||
<div><label class="field-label">CVC</label><input class="field" id="ch-cvc" placeholder="123" maxlength="4"></div>
|
||
</div>
|
||
</div>
|
||
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;margin-bottom:1rem">
|
||
<h3 style="font-size:.9rem;font-weight:700;margin-bottom:1rem"><i class="fa-solid fa-truck" style="color:var(--accent);margin-right:.5rem"></i>Shipping</h3>
|
||
<div style="margin-bottom:.75rem"><label class="field-label">Address</label><input class="field" id="ch-addr" placeholder="123 Industrial Parkway, Unit 4"></div>
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.75rem">
|
||
<div><label class="field-label">City</label><input class="field" id="ch-city" placeholder="Detroit"></div>
|
||
<div><label class="field-label">State</label><input class="field" id="ch-state" placeholder="MI" maxlength="2"></div>
|
||
<div><label class="field-label">ZIP</label><input class="field" id="ch-zip" placeholder="48201" maxlength="10"></div>
|
||
</div>
|
||
</div>
|
||
<div style="background:var(--accent-dim);border:1px solid rgba(212,168,83,.2);border-radius:.75rem;padding:1rem;margin-bottom:1.25rem;display:flex;align-items:center;gap:.75rem">
|
||
<i class="fa-solid fa-shield-halved" style="color:var(--accent);font-size:1.1rem"></i>
|
||
<div><p style="font-size:.8rem;font-weight:600">All fields sanitized through JS security engine (Web Crypto SHA-256)</p>
|
||
<p style="font-size:.7rem;color:var(--fg-muted)">CSRF: ${sec.csrfToken?sec.csrfToken.slice(0,16)+'...':'pending'}</p></div>
|
||
</div>
|
||
<div id="checkout-errors" style="margin-bottom:1rem"></div>
|
||
<button class="btn-primary" style="width:100%;padding:.85rem;font-size:1rem" id="place-order-btn" onclick="submitOrder()">
|
||
<i class="fa-solid fa-lock" style="margin-right:.5rem"></i>Place Order — $${total.toFixed(2)}
|
||
</button>`;
|
||
}
|
||
function formatCard(el){let v=el.value.replace(/\D/g,'').slice(0,16);el.value=v.replace(/(.{4})/g,'$1 ').trim()}
|
||
function formatExpiry(el){let v=el.value.replace(/\D/g,'').slice(0,4);if(v.length>=3)v=v.slice(0,2)+'/'+v.slice(2);el.value=v}
|
||
|
||
async function submitOrder(){
|
||
const btn=document.getElementById('place-order-btn');
|
||
if(!sec.checkRate('submit_order')){toast('Rate limited — please wait','warn');return}
|
||
/* Show loading state */
|
||
btn.disabled=true;btn.innerHTML='<span class="spinner"></span> Processing...';
|
||
|
||
/* Small delay so user sees the spinner */
|
||
await new Promise(r=>setTimeout(r,400));
|
||
|
||
const fields={first:document.getElementById('ch-first').value.trim(),last:document.getElementById('ch-last').value.trim(),email:document.getElementById('ch-email').value.trim(),company:document.getElementById('ch-company').value.trim(),card:document.getElementById('ch-card').value.replace(/\s/g,''),expiry:document.getElementById('ch-expiry').value.trim(),cvc:document.getElementById('ch-cvc').value.trim(),addr:document.getElementById('ch-addr').value.trim(),city:document.getElementById('ch-city').value.trim(),state:document.getElementById('ch-state').value.trim(),zip:document.getElementById('ch-zip').value.trim()};
|
||
document.querySelectorAll('.field').forEach(f=>f.classList.remove('error'));
|
||
document.getElementById('checkout-errors').innerHTML='';
|
||
let errors=[];
|
||
const sanitized={};
|
||
for(const[k,v] of Object.entries(fields)){sanitized[k]=sec.sanitize(v);if(!v&&k!=='company'&&k!=='card')errors.push(k+' is required')}
|
||
if(fields.email&&!sec.validateEmail(fields.email))errors.push('Invalid email address');
|
||
if(fields.card&&!sec.validateCC(fields.card))errors.push('Invalid card number (try 4242 4242 4242 4242)');
|
||
if(fields.expiry&&!/^\d{2}\/\d{2}$/.test(fields.expiry))errors.push('Invalid expiry (MM/YY)');
|
||
if(fields.cvc&&!/^\d{3,4}$/.test(fields.cvc))errors.push('Invalid CVC');
|
||
|
||
const orderItems=state.cart.map(c=>({name:c.product.name,sku:c.product.sku,price:getUnitPrice(c.product,c.qty),qty:c.qty}));
|
||
const ov=sec.validateOrder(orderItems,getCartTotal().toFixed(2));
|
||
if(!ov.valid)errors.push('Order validation: '+(ov.error||'unknown'));
|
||
|
||
if(errors.length){
|
||
errors.forEach(e=>{const d=document.createElement('div');d.style.cssText='color:var(--danger);font-size:.8rem;padding:.35rem 0';d.innerHTML=`<i class="fa-solid fa-circle-xmark" style="margin-right:.35rem"></i>${e}`;document.getElementById('checkout-errors').appendChild(d)});
|
||
await sec.logAudit('CHECKOUT_VALIDATION_FAIL',{errors:errors.length});
|
||
toast('Fix the errors below','error');
|
||
btn.disabled=false;btn.innerHTML=`<i class="fa-solid fa-lock" style="margin-right:.5rem"></i>Place Order — $${getCartTotal().toFixed(2)}`;
|
||
return;
|
||
}
|
||
|
||
/* STOCK DECREMENT — actually reduce product stock */
|
||
for(const c of state.cart){
|
||
const p=PRODUCTS.find(x=>x.id===c.product.id);
|
||
if(p){p.stock=Math.max(0,p.stock-c.qty)}
|
||
}
|
||
|
||
const orderId='FK-'+Date.now().toString(36).toUpperCase()+'-'+Math.random().toString(36).slice(2,6).toUpperCase();
|
||
const order={id:orderId,items:orderItems,total:getCartTotal(),customer:{first:sanitized.first,last:sanitized.last,email:sanitized.email,company:sanitized.company},address:{street:sanitized.addr,city:sanitized.city,state:sanitized.state,zip:sanitized.zip},timestamp:new Date().toISOString(),csrfValid:sec.validateCsrf(sec.csrfToken)};
|
||
state.orderHistory.push(order);
|
||
await sec.logAudit('ORDER_PLACED',{order_id:orderId,total:order.total.toFixed(2),items:order.items.length});
|
||
sec.rotateCsrf();
|
||
state.cart=[];renderCart();renderProducts();
|
||
showConfirmation(order);toast('Order placed successfully!','success');
|
||
}
|
||
|
||
function showConfirmation(order){
|
||
document.getElementById('confirm-text').textContent=`Order ${order.id} placed and logged to SHA-256 audit chain.`;
|
||
document.getElementById('confirm-details').innerHTML=`
|
||
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem">
|
||
<h3 style="font-size:.85rem;font-weight:700;margin-bottom:1rem">Order Summary</h3>
|
||
${order.items.map(i=>`<div style="display:flex;justify-content:space-between;padding:.4rem 0;font-size:.82rem;border-bottom:1px solid var(--border)"><span><span class="mono" style="color:var(--fg-muted);font-size:.7rem;margin-right:.4rem">${i.sku}</span>${i.name} x${i.qty}</span><span style="font-weight:600">$${(i.price*i.qty).toFixed(2)}</span></div>`).join('')}
|
||
<div style="display:flex;justify-content:space-between;padding:.75rem 0;font-size:1rem;font-weight:700"><span>Total</span><span style="color:var(--accent)">$${order.total.toFixed(2)}</span></div>
|
||
<div style="margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border);font-size:.75rem;color:var(--fg-muted)">
|
||
<p><strong>Customer:</strong> ${order.customer.first} ${order.customer.last} (${order.customer.email})</p>
|
||
${order.customer.company?`<p style="margin-top:.25rem"><strong>Company:</strong> ${order.customer.company}</p>`:''}
|
||
<p style="margin-top:.25rem"><strong>Ship to:</strong> ${order.address.street}, ${order.address.city}, ${order.address.state} ${order.address.zip}</p>
|
||
<p style="margin-top:.25rem"><strong>CSRF Valid:</strong> <span style="color:${order.csrfValid?'var(--success)':'var(--danger)'}">${order.csrfValid?'Yes':'No'}</span></p>
|
||
<p style="margin-top:.25rem"><strong>WASM Demo Hash:</strong> <span class="mono" style="color:var(--info)">${wasmHash(order.id)}</span> <span style="font-size:.65rem;color:var(--fg-muted)">(demonstrational)</span></p>
|
||
<p style="margin-top:.25rem"><strong>SHA-256 Chain:</strong> <span style="color:var(--success)">Verified</span></p>
|
||
</div>
|
||
</div>`;
|
||
navigate('confirmation');
|
||
}
|
||
|
||
/* ================================================================
|
||
AUDIT LOG
|
||
================================================================ */
|
||
function renderAuditLog(){
|
||
const logs=sec.logs;
|
||
const tbody=document.getElementById('audit-tbody');
|
||
const empty=document.getElementById('audit-empty');
|
||
const persistNote=document.getElementById('audit-persist-note');
|
||
if(!logs.length){tbody.innerHTML='';empty.style.display='block';persistNote.style.display='none';return}
|
||
empty.style.display='none';
|
||
const ac={'PRODUCT_VIEW':'var(--info)','ADD_TO_CART':'var(--success)','REMOVE_FROM_CART':'var(--warn)','QTY_CHANGE':'var(--info)','CHECKOUT_START':'var(--accent)','CHECKOUT_VALIDATION_FAIL':'var(--danger)','ORDER_PLACED':'var(--success)','CSRF_GENERATE':'var(--fg-muted)','CSRF_VALIDATE_OK':'var(--success)','CSRF_FAIL':'var(--danger)','RATE_LIMITED':'var(--warn)','CATEGORY_FILTER':'var(--fg-muted)','CHAIN_VERIFY':'var(--info)','FREIGHT_BLOCKED':'var(--warn)'};
|
||
const sorted=[...logs].reverse();
|
||
tbody.innerHTML=sorted.map(l=>{
|
||
const time=new Date(l.ts).toLocaleTimeString();
|
||
const details=typeof l.details==='string'?l.details:JSON.stringify(l.details);
|
||
return `<tr><td class="mono" style="white-space:nowrap;color:var(--fg-muted)">${time}</td><td><span style="color:${ac[l.action]||'var(--fg-muted)'};font-weight:600">${l.action}</span></td><td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${details.replace(/"/g,'"')}">${details}</td><td class="mono" style="color:var(--accent);font-size:.7rem">${l.hash}</td><td class="mono" style="color:var(--fg-muted);font-size:.7rem">${l.prevHash.slice(0,8)}...</td></tr>`;
|
||
}).join('');
|
||
}
|
||
async function verifyChain(){
|
||
const r=await sec.verifyChain();
|
||
const el=document.getElementById('chain-status');
|
||
if(r.valid){el.style.color='var(--success)';el.innerHTML=`<i class="fa-solid fa-link" style="margin-right:.35rem"></i>Chain valid (${r.count} entries)`;toast('SHA-256 chain integrity verified','success')}
|
||
else{el.style.color='var(--danger)';el.innerHTML=`<i class="fa-solid fa-link-slash" style="margin-right:.35rem"></i>Chain BROKEN at entry ${r.brokenAt}`;toast('Audit chain COMPROMISED','error')}
|
||
await sec.logAudit('CHAIN_VERIFY',{valid:r.valid,count:r.count||0});
|
||
}
|
||
function clearAuditLog(){sec.clearLogs();renderAuditLog();document.getElementById('chain-status').innerHTML='';toast('Audit log cleared','info')}
|
||
|
||
/* ================================================================
|
||
SECURITY DASHBOARD
|
||
================================================================ */
|
||
function renderSecurityDashboard(){
|
||
const features=[
|
||
{name:'Web Crypto SHA-256',desc:'Native OS-level SHA-256 via crypto.subtle.digest(). Falls back to auditable pure-JS implementation on non-secure contexts. Powers the audit hash chain.',status:!!(window.crypto&&crypto.subtle),icon:'fa-lock',note:sec.logs.length>0?`${sec.logs.length} entries chained`:'No entries yet'},
|
||
{name:'WASM DJB2 Module',desc:'100-byte hand-compiled WebAssembly hash function. Demonstrational — NOT cryptographic. Shows sandboxed WASM execution model.',status:wasmReady,icon:'fa-microchip',note:'Labeled as demonstrational',demo:true},
|
||
{name:'Input Sanitization',desc:'9-pattern regex XSS filter + HTML entity encoding. Runs synchronously with zero external dependencies. All user fields pass through before processing.',status:true,icon:'fa-filter'},
|
||
{name:'CSRF Protection',desc:'128-bit tokens via crypto.getRandomValues(). Validated on every mutating action. 1-hour expiry. Rotated after order placement.',status:true,icon:'fa-key'},
|
||
{name:'Audit Log Persistence',desc:'Hash chain saved to localStorage on every write. Survives page refresh. Chain integrity verified on load via SHA-256 recomputation.',status:true,icon:'fa-database'},
|
||
{name:'Rate Limiting',desc:'Sliding window: 15 requests per 60 seconds per action type. Prevents brute force and abuse patterns.',status:true,icon:'fa-gauge-high'},
|
||
{name:'Order Integrity',desc:'Server-side total recomputation prevents price manipulation. Quantity bounds (1-99) enforced. Negative price rejection.',status:true,icon:'fa-receipt'},
|
||
{name:'Credit Card Validation',desc:'Luhn algorithm. Card number never leaves this session. Demo only — production requires PCI-DSS tokenization.',status:true,icon:'fa-credit-card'},
|
||
{name:'Stock Enforcement',desc:'Cannot add out-of-stock items. Cart quantity capped at available stock. Stock decrements on order placement.',status:true,icon:'fa-boxes-stacked'},
|
||
{name:'Freight Gating',desc:'Heavy items flagged with freight:true are blocked from direct add-to-cart. Forces manual sales contact for proper shipping quotes.',status:true,icon:'fa-truck'},
|
||
{name:'PHP Backend (Architecture)',desc:'Production PHP middleware: CSRF, rate limiting, sanitization, security headers, parameterized SQL, row-level locking.',status:true,icon:'fa-server',note:'Code in Architecture tab'},
|
||
{name:'WASM Sandbox Isolation',desc:'WebAssembly runs in memory-isolated sandbox with no DOM access. Linear memory model prevents pointer exploits.',status:wasmReady,icon:'fa-box',demo:true},
|
||
];
|
||
document.getElementById('security-grid').innerHTML=features.map(f=>`
|
||
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
|
||
<div style="display:flex;align-items:center;gap:.6rem">
|
||
<div style="width:36px;height:36px;border-radius:.5rem;background:${f.demo?'rgba(56,189,248,.12)':'var(--accent-dim)'};display:flex;align-items:center;justify-content:center">
|
||
<i class="fa-solid ${f.icon}" style="color:${f.demo?'var(--info)':'var(--accent)'};font-size:.9rem"></i></div>
|
||
<h3 style="font-size:.85rem;font-weight:700">${f.name}</h3>
|
||
</div>
|
||
<span class="sec-dot ${f.demo?'demo':(f.status?'on':'off')}" title="${f.demo?'Demonstrational':(f.status?'Active':'Inactive')}"></span>
|
||
</div>
|
||
<p style="font-size:.78rem;color:var(--fg-muted);line-height:1.5">${f.desc}</p>
|
||
${f.note?`<p style="font-size:.7rem;color:${f.demo?'var(--info)':'var(--accent)'};margin-top:.5rem;font-style:italic">${f.note}</p>`:''}
|
||
</div>`).join('');
|
||
}
|
||
|
||
/* ================================================================
|
||
ARCHITECTURE TABS
|
||
================================================================ */
|
||
function switchArchTab(tab,btn){
|
||
document.querySelectorAll('.arch-panel').forEach(p=>p.style.display='none');
|
||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||
document.getElementById('arch-'+tab).style.display='block';btn.classList.add('active');
|
||
}
|
||
|
||
function renderArchitecturePanels(){
|
||
document.getElementById('arch-php').innerHTML=`
|
||
<div style="margin-bottom:1rem"><h3 style="font-size:.9rem;font-weight:700"><i class="fa-brands fa-php" style="color:var(--accent);margin-right:.5rem"></i>SecurityMiddleware.php</h3><p style="font-size:.75rem;color:var(--fg-muted)">Runs on every request in production</p></div>
|
||
<div class="code-block mono"><span class="cm"><?php</span>
|
||
<span class="kw">namespace</span> App\\Middleware;
|
||
|
||
<span class="kw">class</span> <span class="fn">SecurityMiddleware</span> {
|
||
<span class="kw">public function</span> <span class="fn">handle</span>(<span class="fn">Request</span> <span class="var">$req</span>, <span class="fn">Closure</span> <span class="var">$next</span>): <span class="fn">Response</span> {
|
||
<span class="cm">// CSRF validation on POST</span>
|
||
<span class="kw">if</span> (<span class="var">$req</span>-><span class="fn">isMethod</span>(<span class="str">'POST'</span>)) {
|
||
<span class="kw">if</span> (!<span class="fn">hash_equals</span>(
|
||
<span class="var">$req</span>-><span class="fn">session</span>()-><span class="fn">get</span>(<span class="str">'csrf_token'</span>),
|
||
<span class="var">$req</span>-><span class="fn">header</span>(<span class="str">'X-CSRF-Token'</span>)
|
||
)) <span class="kw">return</span> <span class="fn">response</span>()-><span class="fn">json</span>(
|
||
[<span class="str">'error'</span>=><span class="str">'CSRF failed'</span>], <span class="num">403</span>
|
||
);
|
||
}
|
||
|
||
<span class="cm">// Rate limiting</span>
|
||
<span class="var">$key</span> = <span class="str">'rate:'</span>.<span class="var">$req</span>-><span class="fn">ip</span>().<span class="str">':'</span>.<span class="var">$req</span>-><span class="fn">path</span>();
|
||
<span class="kw">if</span> (<span class="fn">RateLimiter</span>::<span class="fn">tooManyAttempts</span>(<span class="var">$key</span>, <span class="num">60</span>))
|
||
<span class="kw">return</span> <span class="fn">response</span>()-><span class="fn">json</span>(
|
||
[<span class="str">'error'</span>=><span class="str">'Rate limited'</span>], <span class="num">429</span>
|
||
);
|
||
|
||
<span class="cm">// Recursive sanitization</span>
|
||
<span class="var">$req</span>-><span class="fn">replace</span>(<span class="var">$this</span>-><span class="fn">sanitize</span>(<span class="var">$req</span>-><span class="fn">all</span>()));
|
||
|
||
<span class="cm">// Security headers</span>
|
||
<span class="var">$res</span> = <span class="var">$next</span>(<span class="var">$req</span>);
|
||
<span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">'X-Content-Type-Options'</span>,<span class="str">'nosniff'</span>);
|
||
<span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">'X-Frame-Options'</span>,<span class="str">'DENY'</span>);
|
||
<span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">'Content-Security-Policy'</span>,
|
||
<span class="str">"default-src 'self'; script-src 'self'"</span>);
|
||
<span class="kw">return</span> <span class="var">$res</span>;
|
||
}
|
||
}</div>`;
|
||
|
||
document.getElementById('arch-js').innerHTML=`
|
||
<div style="margin-bottom:1rem"><h3 style="font-size:.9rem;font-weight:700"><i class="fa-brands fa-js" style="color:var(--accent);margin-right:.5rem"></i>SecurityEngine — the actual running code</h3><p style="font-size:.75rem;color:var(--fg-muted)">This class runs in your browser right now. No Python. No libraries.</p></div>
|
||
<div class="code-block mono"><span class="kw">class</span> <span class="fn">SecurityEngine</span> {
|
||
<span class="fn">constructor</span>(sessionId) {
|
||
<span class="var">this</span>.logs = [];
|
||
<span class="var">this</span>.prevHash = <span class="str">'GENESIS'</span>;
|
||
<span class="var">this</span>.sanitizerPatterns = [
|
||
/<script[\\s\\S]*?<\\/script>/gi,
|
||
/javascript\\s*:/gi,
|
||
/on\\w+\\s*=/gi,
|
||
/<iframe/gi, /<object/gi,
|
||
/<embed/gi, /<form/gi,
|
||
/expression\\s*\\(/gi,
|
||
/@import/gi,
|
||
];
|
||
}
|
||
|
||
<span class="cm">// SHA-256 hash-chained audit log</span>
|
||
<span class="kw">async</span> <span class="fn">logAudit</span>(action, details) {
|
||
<span class="kw">const</span> entry = {
|
||
ts: <span class="fn">Date</span>.<span class="fn">now</span>(), action, details,
|
||
session: <span class="var">this</span>.sessionId.<span class="fn">slice</span>(<span class="num">0</span>,<span class="num">12</span>),
|
||
prevHash: <span class="var">this</span>.prevHash
|
||
};
|
||
<span class="kw">const</span> hash = <span class="kw">await</span> <span class="fn">sha256</span>(
|
||
<span class="fn">JSON</span>.<span class="fn">stringify</span>(entry,
|
||
<span class="fn">Object</span>.<span class="fn">keys</span>(entry).<span class="fn">sort</span>())
|
||
);
|
||
entry.hash = hash.<span class="fn">slice</span>(<span class="num">0</span>, <span class="num">16</span>);
|
||
<span class="var">this</span>.prevHash = entry.hash;
|
||
<span class="var">this</span>.logs.<span class="fn">push</span>(entry);
|
||
<span class="var">this</span>._persist(); <span class="cm">// localStorage</span>
|
||
<span class="kw">return</span> entry;
|
||
}
|
||
|
||
<span class="cm">// crypto.subtle.digest — OS-level crypto</span>
|
||
<span class="kw">async</span> <span class="fn">sha256</span>(message) {
|
||
<span class="kw">const</span> buf = <span class="kw">await</span> crypto.subtle.<span class="fn">digest</span>(
|
||
<span class="str">'SHA-256'</span>,
|
||
<span class="kw">new</span> <span class="fn">TextEncoder</span>().<span class="fn">encode</span>(message)
|
||
);
|
||
<span class="kw">return</span> [...<span class="kw">new</span> <span class="fn">Uint8Array</span>(buf)]
|
||
.<span class="fn">map</span>(b => b.<span class="fn">toString</span>(<span class="num">16</span>).<span class="fn">padStart</span>(<span class="num">8</span>,<span class="str">'0'</span>))
|
||
.<span class="fn">join</span>(<span class="str">''</span>);
|
||
}
|
||
|
||
<span class="cm">// CSRF via crypto.getRandomValues</span>
|
||
<span class="fn">generateCsrf</span>() {
|
||
<span class="kw">const</span> buf = <span class="kw">new</span> <span class="fn">Uint8Array</span>(<span class="num">16</span>);
|
||
crypto.<span class="fn">getRandomValues</span>(buf);
|
||
<span class="var">this</span>.csrfToken = [...buf]
|
||
.<span class="fn">map</span>(b => b.<span class="fn">toString</span>(<span class="num">16</span>).<span class="fn">padStart</span>(<span class="num">2</span>,<span class="str">'0'</span>))
|
||
.<span class="fn">join</span>(<span class="str">''</span>);
|
||
}
|
||
|
||
<span class="fn">sanitize</span>(text) {
|
||
<span class="kw">let</span> r = text;
|
||
<span class="kw">for</span> (<span class="kw">const</span> p <span class="kw">of</span> <span class="var">this</span>.sanitizerPatterns)
|
||
r = r.<span class="fn">replace</span>(p, <span class="str">'[SANITIZED]'</span>);
|
||
<span class="kw">return</span> r.<span class="fn">replace</span>(<span class="str">/&/g</span>,<span class="str">'&amp;'</span>)
|
||
.<span class="fn">replace</span>(<span class="str">/</g</span>,<span class="str">'&lt;'</span>)
|
||
.<span class="fn">replace</span>(<span class="str">/>/g</span>,<span class="str">'&gt;'</span>);
|
||
}
|
||
}</div>`;
|
||
|
||
document.getElementById('arch-wasm').innerHTML=`
|
||
<div style="margin-bottom:1rem"><h3 style="font-size:.9rem;font-weight:700"><i class="fa-solid fa-microchip" style="color:var(--info);margin-right:.5rem"></i>DJB2 WASM Module — Demonstrational</h3>
|
||
<p style="font-size:.75rem;color:var(--fg-muted)">NOT used for security. Included to show the WASM sandbox model. Real crypto uses Web Crypto API above.</p></div>
|
||
<div style="background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.2);border-radius:.5rem;padding:.6rem .75rem;margin-bottom:1rem;font-size:.75rem;color:var(--info)">
|
||
<i class="fa-solid fa-info-circle" style="margin-right:.35rem"></i>DJB2 is a fast non-cryptographic hash with known collisions. It demonstrates WASM execution but does not contribute to security guarantees. The audit chain uses SHA-256 via <code>crypto.subtle.digest()</code>.
|
||
</div>
|
||
<div class="code-block mono">(<span class="kw">module</span>
|
||
(<span class="kw">memory</span> (<span class="kw">export</span> <span class="str">"mem"</span>) <span class="num">1</span>)
|
||
(<span class="kw">func</span> (<span class="kw">export</span> <span class="str">"hash"</span>) (<span class="kw">param</span> <span class="var">$len</span> <span class="type">i32</span>) (<span class="kw">result</span> <span class="type">i32</span>)
|
||
<span class="cm">;; h = 5381, loop: h = (h<<5)+h+mem[i]</span>
|
||
... <span class="cm">;; 100 bytes total</span>
|
||
)
|
||
)</div>`;
|
||
|
||
document.getElementById('arch-flow').innerHTML=`
|
||
<div style="display:flex;flex-direction:column;gap:0;align-items:center;padding:1rem 0">
|
||
${[
|
||
{icon:'fa-user',label:'User Action',desc:'Browse, search, add to cart, checkout',color:'var(--info)'},
|
||
{icon:'fa-shield-halved',label:'JS SecurityEngine',desc:'Input capture, sanitization (9-pattern XSS + HTML encode)',color:'var(--fg-muted)'},
|
||
{icon:'fa-key',label:'CSRF Validation',desc:'crypto.getRandomValues token — match + 1hr expiry check',color:'var(--warn)'},
|
||
{icon:'fa-gauge-high',label:'Rate Limiting',desc:'Sliding window: 15 req/60s per action type',color:'var(--warn)'},
|
||
{icon:'fa-receipt',label:'Order Validation',desc:'Total recomputation, qty bounds, negative price rejection',color:'var(--accent)'},
|
||
{icon:'fa-lock',label:'SHA-256 Audit Entry',desc:'crypto.subtle.digest() — OS-level hash chain with prev_hash',color:'var(--success)'},
|
||
{icon:'fa-database',label:'localStorage Persist',desc:'Hash chain saved on every write, survives refresh',color:'var(--success)'},
|
||
{icon:'fa-server',label:'PHP Backend (Production)',desc:'Middleware → Controller → Parameterized SQL → Headers',color:'var(--info)'},
|
||
].map((s,i,a)=>`
|
||
<div style="text-align:center;max-width:380px">
|
||
<div style="width:48px;height:48px;border-radius:50%;background:${s.color}22;border:2px solid ${s.color};display:inline-flex;align-items:center;justify-content:center;margin-bottom:.5rem">
|
||
<i class="fa-solid ${s.icon}" style="color:${s.color};font-size:1rem"></i></div>
|
||
<h3 style="font-size:.85rem;font-weight:700">${s.label}</h3>
|
||
<p style="font-size:.75rem;color:var(--fg-muted);line-height:1.5">${s.desc}</p>
|
||
</div>
|
||
${i<a.length-1?'<div style="width:2px;height:32px;background:var(--border-light);margin:.25rem 0"></div>':''}
|
||
`).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
/* ================================================================
|
||
INIT
|
||
================================================================ */
|
||
async function init(){
|
||
const bar=document.getElementById('load-bar');
|
||
const status=document.getElementById('load-status');
|
||
|
||
/* Step 1: Security engine (instant — no download) */
|
||
status.textContent='Initializing security engine...';bar.style.width='20%';
|
||
sec=new SecurityEngine(state.sessionId);
|
||
sec.generateCsrf();
|
||
|
||
/* Step 2: WASM demo module */
|
||
status.textContent='Loading WASM demonstrational module...';bar.style.width='40%';
|
||
await initWasm();
|
||
bar.style.width='60%';
|
||
|
||
/* Step 3: Check crypto availability */
|
||
status.textContent='Verifying Web Crypto API...';bar.style.width='75%';
|
||
const cryptoOk=!!(window.crypto&&crypto.subtle);
|
||
document.getElementById('sec-dot-crypto').className=cryptoOk?'sec-dot on':'sec-dot off';
|
||
if(!cryptoOk)console.warn('crypto.subtle unavailable — using JS SHA-256 fallback');
|
||
|
||
/* Step 4: Restore audit log */
|
||
status.textContent='Restoring audit log...';bar.style.width='90%';
|
||
const restoredCount=sec._restoreFromStorage();
|
||
if(restoredCount>0){
|
||
document.getElementById('persist-count').textContent=restoredCount;
|
||
document.getElementById('audit-persist-note').style.display='block';
|
||
}
|
||
|
||
/* Step 5: Initial audit entry + render */
|
||
status.textContent='Ready';bar.style.width='100%';
|
||
await sec.logAudit('SESSION_START',{wasm:wasmReady,crypto:cryptoOk,restored:restoredCount});
|
||
|
||
renderCategoryFilters();renderProducts();renderCart();renderArchitecturePanels();
|
||
await new Promise(r=>setTimeout(r,300));
|
||
document.getElementById('loader').classList.add('hidden');
|
||
document.getElementById('main-nav').style.opacity='1';
|
||
}
|
||
|
||
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeProductModal();if(document.getElementById('cart-sidebar').classList.contains('open'))toggleCart()}});
|
||
init().catch(e=>{console.error('Init failed:',e);document.getElementById('load-status').textContent='Error: '+e.message;document.getElementById('load-status').style.color='var(--danger)';document.getElementById('loader').classList.add('hidden');document.getElementById('main-nav').style.opacity='1';sec=new SecurityEngine(state.sessionId);sec.generateCsrf();renderCategoryFilters();renderProducts();renderCart();renderArchitecturePanels()});
|
||
</script>
|
||
</body>
|
||
</html> |