1078 lines
92 KiB
Groff
1078 lines
92 KiB
Groff
<!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>
|
|
<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;z-index:101}.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}
|
|
@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;white-space:pre}
|
|
.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}
|
|
.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)}
|
|
@media(max-width:640px){.modal-grid{grid-template-columns:1fr!important}.modal-grid>div:first-child{border-radius:1rem 1rem 0 0!important}.modal-grid>div:first-child img{min-height:200px!important}}
|
|
</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" 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 style="display:flex;align-items:center;gap:.4rem;font-size:.7rem;color:var(--fg-muted)">
|
|
<span class="sec-dot pending" id="sec-dot-crypto" 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">
|
|
|
|
<!-- SHOP -->
|
|
<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>
|
|
|
|
<!-- AUDIT -->
|
|
<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>
|
|
|
|
<!-- ARCHITECTURE -->
|
|
<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 and vanilla JS security engine with Web Crypto API</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('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-flow" class="arch-panel" style="display:none"></div>
|
|
</div>
|
|
|
|
<!-- SECURITY -->
|
|
<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>
|
|
|
|
<!-- CHECKOUT -->
|
|
<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>
|
|
|
|
<!-- CONFIRMATION -->
|
|
<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>
|
|
/* ================================================================
|
|
SHA-256 — Web Crypto API with pure-JS fallback
|
|
================================================================ */
|
|
async function sha256(message) {
|
|
if (window.crypto && crypto.subtle) {
|
|
var buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(message));
|
|
return [...new Uint8Array(buf)].map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
}
|
|
return sha256Pure(message);
|
|
}
|
|
|
|
function sha256Pure(str) {
|
|
var rotr = function(v, n) { return (v >>> n) | (v << (32 - n)); };
|
|
var K = [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];
|
|
var H = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
|
|
var msg = new TextEncoder().encode(str);
|
|
var bitLen = msg.length * 8;
|
|
var padLen = ((56 - (msg.length + 1) % 64) + 64) % 64;
|
|
var padded = new Uint8Array(msg.length + 1 + padLen + 8);
|
|
padded.set(msg);
|
|
padded[msg.length] = 0x80;
|
|
var dv = new DataView(padded.buffer);
|
|
dv.setUint32(padded.length - 4, bitLen >>> 0, false);
|
|
for (var o = 0; o < padded.length; o += 64) {
|
|
var W = new Uint32Array(64);
|
|
for (var i = 0; i < 16; i++) W[i] = dv.getUint32(o + i * 4, false);
|
|
for (i = 16; i < 64; i++) {
|
|
var s0 = rotr(W[i-15], 7) ^ rotr(W[i-15], 18) ^ (W[i-15] >>> 3);
|
|
var 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;
|
|
}
|
|
var a=H[0],b=H[1],c=H[2],d=H[3],e=H[4],f=H[5],g=H[6],h=H[7];
|
|
for (i = 0; i < 64; i++) {
|
|
var S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
var ch = (e & f) ^ (~e & g);
|
|
var t1 = (h + S1 + ch + K[i] + W[i]) | 0;
|
|
var S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
var maj = (a & b) ^ (a & c) ^ (b & c);
|
|
var 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 [...new Uint32Array(H)].map(function(v) { return v.toString(16).padStart(8, '0'); }).join('');
|
|
}
|
|
|
|
/* ================================================================
|
|
SECURITY ENGINE — Pure vanilla JS, zero dependencies
|
|
================================================================ */
|
|
var SecurityEngine = (function() {
|
|
function SE(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();
|
|
}
|
|
|
|
SE.prototype.logAudit = async function(action, details) {
|
|
var entry = {
|
|
ts: Date.now(),
|
|
action: action,
|
|
details: details,
|
|
session: this.sessionId.slice(0, 12),
|
|
prevHash: this.prevHash
|
|
};
|
|
var 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;
|
|
};
|
|
|
|
SE.prototype.verifyChain = async function() {
|
|
var prev = 'GENESIS';
|
|
for (var i = 0; i < this.logs.length; i++) {
|
|
var check = Object.assign({}, this.logs[i]);
|
|
var storedHash = check.hash;
|
|
delete check.hash;
|
|
check.prevHash = prev;
|
|
var 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 };
|
|
};
|
|
|
|
SE.prototype.clearLogs = function() {
|
|
this.logs = [];
|
|
this.prevHash = 'GENESIS';
|
|
this._persist();
|
|
};
|
|
|
|
SE.prototype.sanitize = function(text) {
|
|
if (typeof text !== 'string') text = String(text);
|
|
var r = text;
|
|
for (var i = 0; i < this.sanitizerPatterns.length; i++) {
|
|
r = r.replace(this.sanitizerPatterns[i], '[SANITIZED]');
|
|
}
|
|
return r.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
};
|
|
|
|
SE.prototype.validateEmail = function(e) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e);
|
|
};
|
|
|
|
SE.prototype.validateCC = function(n) {
|
|
n = n.replace(/\s/g, '');
|
|
if (!/^\d{13,19}$/.test(n)) return false;
|
|
var d = []; for (var i = 0; i < n.length; i++) d.push(parseInt(n[i]));
|
|
for (i = d.length - 2; i >= 0; i -= 2) { d[i] *= 2; if (d[i] > 9) d[i] -= 9; }
|
|
var sum = 0; for (i = 0; i < d.length; i++) sum += d[i];
|
|
return sum % 10 === 0;
|
|
};
|
|
|
|
SE.prototype.generateCsrf = function() {
|
|
var buf = new Uint8Array(16);
|
|
crypto.getRandomValues(buf);
|
|
this.csrfToken = [...buf].map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
this.csrfCreated = Date.now();
|
|
return this.csrfToken;
|
|
};
|
|
|
|
SE.prototype.validateCsrf = function(token) {
|
|
if (!this.csrfToken || this.csrfToken !== token) return false;
|
|
if (Date.now() - this.csrfCreated > 3600000) return false;
|
|
return true;
|
|
};
|
|
|
|
SE.prototype.rotateCsrf = function() { return this.generateCsrf(); };
|
|
|
|
SE.prototype.checkRate = function(key, max, window) {
|
|
max = max || 15; window = window || 60000;
|
|
var now = Date.now();
|
|
if (!this.rateLimits[key]) this.rateLimits[key] = [];
|
|
this.rateLimits[key] = this.rateLimits[key].filter(function(t) { return now - t < window; });
|
|
if (this.rateLimits[key].length >= max) return false;
|
|
this.rateLimits[key].push(now);
|
|
return true;
|
|
};
|
|
|
|
SE.prototype.validateOrder = function(items, statedTotal) {
|
|
var computed = 0;
|
|
for (var i = 0; i < items.length; i++) computed += parseFloat(items[i].price) * parseInt(items[i].qty);
|
|
if (Math.abs(computed - parseFloat(statedTotal)) > 0.01) return { valid: false, error: 'Total mismatch: computed $' + computed.toFixed(2) + ' vs stated $' + statedTotal };
|
|
for (i = 0; i < items.length; i++) {
|
|
var q = parseInt(items[i].qty);
|
|
if (q < 1 || q > 99) return { valid: false, error: 'Invalid qty for ' + items[i].name };
|
|
if (parseFloat(items[i].price) < 0) return { valid: false, error: 'Negative price: ' + items[i].name };
|
|
}
|
|
return { valid: true, computedTotal: computed };
|
|
};
|
|
|
|
SE.prototype._persist = function() {
|
|
try { localStorage.setItem('fortkart_audit', JSON.stringify({ logs: this.logs, prevHash: this.prevHash })); } catch(e) {}
|
|
};
|
|
|
|
SE.prototype._restoreFromStorage = function() {
|
|
try {
|
|
var d = JSON.parse(localStorage.getItem('fortkart_audit'));
|
|
if (d && d.logs && Array.isArray(d.logs)) {
|
|
this.logs = d.logs;
|
|
this.prevHash = d.prevHash || 'GENESIS';
|
|
return this.logs.length;
|
|
}
|
|
} catch(e) {}
|
|
return 0;
|
|
};
|
|
|
|
return SE;
|
|
})();
|
|
|
|
/* ================================================================
|
|
PRODUCT DATA — 30 CNC parts with SKUs, weights, tiers, params
|
|
================================================================ */
|
|
var PRODUCTS = [
|
|
{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.',stock:12,rating:4.9,reviews:87,featured:true,specs:['20mm dia','5mm lead','C3 ground','800mm L','HRC 58-62'],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. Common Bridgeport retrofit.',stock:23,rating:4.6,reviews:156,specs:['25mm dia','10mm lead','C7 grade','1200mm L'],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. 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. Compact for Z-axis on small engravers and CO2 laser cutters.',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}]},
|
|
{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. 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. 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.',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. 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. 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'}},
|
|
{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. 80mm mount. Includes ER20 collet set.',stock:28,rating:4.6,reviews:389,featured:true,specs:['2.2kW','ER20','24k RPM','80mm mount','4-bearing'],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.',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. Includes belt pulley set, drawbar, tool release cylinder.',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. Includes 3.175mm collet.',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.',stock:9,rating:4.7,reviews:78,specs:['4kW','ER25','18k RPM','85mm mount','6-bearing'],params:{power:'4.0kW',taper:'ER25',cooling:'Water'}},
|
|
{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.',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. Hardware motion planning, probe input, MPG port.',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. 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. RS485 Modbus. 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 sockets, 4 limit inputs, spindle PWM. Web UI included.',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}]},
|
|
{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 with 4 slider blocks. Chrome-plated steel. 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. Sealed 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.',stock:11,rating:4.8,reviews:52,featured:true,specs:['HGH20CA','800mm','Wide block','Double seal'],params:{size:'HGH20CA'}},
|
|
{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. 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.',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. 50mm cut length. 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}]},
|
|
{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.',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 degree step. 1.9 Nm holding torque. 4-lead bipolar. Double-shaft.',stock:88,rating:4.5,reviews:412,specs:['NEMA 23','2.8A','1.9 Nm','1.8 deg 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.',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.',stock:37,rating:4.4,reviews:223,specs:['15L tank','1200L/hr pump','8mm hose','Coolant incl.'],params:{type:'Cooling'}}
|
|
];
|
|
|
|
var CATEGORIES = ['all'];
|
|
for (var _i = 0; _i < PRODUCTS.length; _i++) { if (CATEGORIES.indexOf(PRODUCTS[_i].category) === -1) CATEGORIES.push(PRODUCTS[_i].category); }
|
|
var 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'};
|
|
var CAT_LABEL_SHORT = {'ball-screws':'Ball Screw','lead-nuts':'Lead Nut',spindles:'Spindle',controllers:'Controller','linear-guides':'Linear Guide',tooling:'Tooling',accessories:'Accessory'};
|
|
var 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
|
|
================================================================ */
|
|
var state = {
|
|
cart: [], view: 'shop', category: 'all',
|
|
sessionId: crypto.randomUUID ? crypto.randomUUID() : 's-' + Date.now() + '-' + Math.random().toString(36).slice(2),
|
|
orderHistory: [], paramSelections: {}
|
|
};
|
|
var sec;
|
|
|
|
/* ================================================================
|
|
UTILITIES
|
|
================================================================ */
|
|
function toast(msg, type) {
|
|
type = type || 'info';
|
|
var icons = {success:'fa-check-circle',error:'fa-exclamation-circle',info:'fa-info-circle',warn:'fa-exclamation-triangle'};
|
|
var 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(function() { el.style.opacity='0'; el.style.transform='translateX(100%)'; el.style.transition='all .3s'; setTimeout(function(){el.remove()},300); }, 3500);
|
|
}
|
|
function toggleMobileNav() { document.getElementById('nav-links').classList.toggle('open'); }
|
|
function closeMobileNav() { document.getElementById('nav-links').classList.remove('open'); }
|
|
function getUnitPrice(product, qty) {
|
|
if (!product.tiers) return product.price;
|
|
var price = product.price;
|
|
for (var i = 0; i < product.tiers.length; i++) { if (qty >= product.tiers[i].min) price = product.tiers[i].p; }
|
|
return price;
|
|
}
|
|
function findProduct(id) { for (var i = 0; i < PRODUCTS.length; i++) { if (PRODUCTS[i].id === id) return PRODUCTS[i]; } return null; }
|
|
function findCartItem(id) { for (var i = 0; i < state.cart.length; i++) { if (state.cart[i].product.id === id) return state.cart[i]; } return null; }
|
|
|
|
/* ================================================================
|
|
NAVIGATION
|
|
================================================================ */
|
|
function navigate(view) {
|
|
state.view = view;
|
|
var views = document.querySelectorAll('.view');
|
|
for (var i = 0; i < views.length; i++) views[i].classList.remove('active');
|
|
var t = document.getElementById('view-' + view);
|
|
if (t) t.classList.add('active');
|
|
var links = document.querySelectorAll('.nav-link');
|
|
for (var j = 0; j < links.length; j++) links[j].classList.toggle('active', links[j].dataset.view === view);
|
|
if (view === 'audit') renderAuditLog();
|
|
if (view === 'security') renderSecurityDashboard();
|
|
window.scrollTo({top:0, behavior:'smooth'});
|
|
}
|
|
|
|
/* ================================================================
|
|
PRODUCTS — category pills, parametric filters, grid rendering
|
|
================================================================ */
|
|
function renderCategoryFilters() {
|
|
var html = '';
|
|
for (var i = 0; i < CATEGORIES.length; i++) {
|
|
var c = CATEGORIES[i];
|
|
html += '<button class="cat-pill ' + (c===state.category?'active':'') + '" onclick="setCategory(\'' + c + '\')">' + (CAT_LABELS[c]||c) + '</button>';
|
|
}
|
|
document.getElementById('cat-filters').innerHTML = html;
|
|
}
|
|
|
|
function setCategory(cat) {
|
|
state.category = cat;
|
|
state.paramSelections = {};
|
|
renderCategoryFilters();
|
|
renderParamFilters();
|
|
renderProducts();
|
|
sec.logAudit('CATEGORY_FILTER', {category: cat});
|
|
}
|
|
|
|
function renderParamFilters() {
|
|
var container = document.getElementById('param-filters');
|
|
var 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';
|
|
var html = '';
|
|
for (var d = 0; d < defs.length; d++) {
|
|
var def = defs[d];
|
|
var selected = state.paramSelections[def.key] || [];
|
|
html += '<div><div style="font-size:.68rem;color:var(--fg-muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em;margin-bottom:.35rem">' + def.label + '</div><div style="display:flex;gap:.3rem;flex-wrap:wrap">';
|
|
for (var v = 0; v < def.values.length; v++) {
|
|
html += '<button class="param-chip ' + (selected.indexOf(def.values[v])>-1?'active':'') + '" onclick="toggleParam(\'' + def.key + '\',\'' + def.values[v] + '\')">' + def.values[v] + '</button>';
|
|
}
|
|
html += '</div></div>';
|
|
}
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function toggleParam(key, value) {
|
|
if (!state.paramSelections[key]) state.paramSelections[key] = [];
|
|
var arr = state.paramSelections[key];
|
|
var 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() {
|
|
var search = (document.getElementById('search-input').value || '').toLowerCase();
|
|
var sort = document.getElementById('sort-select').value;
|
|
var items = [];
|
|
for (var i = 0; i < PRODUCTS.length; i++) {
|
|
var p = PRODUCTS[i];
|
|
if (state.category !== 'all' && p.category !== state.category) continue;
|
|
var specStr = (p.specs||[]).join(' ').toLowerCase();
|
|
if (search && p.name.toLowerCase().indexOf(search)===-1 && p.description.toLowerCase().indexOf(search)===-1 && p.sku.toLowerCase().indexOf(search)===-1 && specStr.indexOf(search)===-1 && p.category.indexOf(search)===-1) continue;
|
|
var passParams = true;
|
|
var paramKeys = Object.keys(state.paramSelections);
|
|
for (var k = 0; k < paramKeys.length; k++) {
|
|
var vals = state.paramSelections[paramKeys[k]];
|
|
if (!vals || !vals.length) continue;
|
|
if (!p.params || vals.indexOf(p.params[paramKeys[k]]) === -1) { passParams = false; break; }
|
|
}
|
|
if (!passParams) continue;
|
|
items.push(p);
|
|
}
|
|
if (sort==='price-asc') items.sort(function(a,b){return a.price-b.price});
|
|
else if (sort==='price-desc') items.sort(function(a,b){return b.price-a.price});
|
|
else if (sort==='rating') items.sort(function(a,b){return b.rating-a.rating});
|
|
else if (sort==='name') items.sort(function(a,b){return a.name.localeCompare(b.name)});
|
|
else items.sort(function(a,b){return (b.featured?1:0)-(a.featured?1:0)});
|
|
|
|
var grid = document.getElementById('product-grid');
|
|
var noRes = document.getElementById('no-results');
|
|
if (!items.length) { grid.innerHTML=''; noRes.style.display='block'; return; }
|
|
noRes.style.display = 'none';
|
|
|
|
var html = '';
|
|
for (var j = 0; j < items.length; j++) {
|
|
var p = items[j];
|
|
var inCart = findCartItem(p.id);
|
|
var ep = getUnitPrice(p, inCart ? inCart.qty : 1);
|
|
var tierHint = '';
|
|
if (p.tiers && p.tiers.length > 1) {
|
|
tierHint = '<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>';
|
|
}
|
|
var freightHtml = p.freight ? '<span class="freight-badge"><i class="fa-solid fa-truck" style="margin-right:.2rem"></i>Freight</span>' : '';
|
|
var specHtml = '';
|
|
for (var s = 0; s < Math.min(3, (p.specs||[]).length); s++) specHtml += '<span class="spec-tag">' + p.specs[s] + '</span>';
|
|
var starHtml = '';
|
|
for (var st = 0; st < Math.floor(p.rating); st++) starHtml += '<i class="fa-solid fa-star"></i>';
|
|
if (p.rating % 1) starHtml += '<i class="fa-solid fa-star-half-stroke"></i>';
|
|
starHtml += ' <span style="color:var(--fg-muted);font-size:.7rem;margin-left:.3rem">(' + p.reviews + ')</span>';
|
|
html += '<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">' + specHtml + '</div><div class="stars" style="margin-bottom:.5rem">' + starHtml + '</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)">$' + ep.toFixed(2) + '</span>' + tierHint + '</div><div style="display:flex;align-items:center;gap:.4rem">' + freightHtml + '<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>';
|
|
}
|
|
grid.innerHTML = html;
|
|
}
|
|
|
|
/* ================================================================
|
|
PRODUCT DETAIL MODAL
|
|
================================================================ */
|
|
function openProduct(id) {
|
|
var p = findProduct(id);
|
|
if (!p) return;
|
|
sec.logAudit('PRODUCT_VIEW', {product_id:p.id, sku:p.sku});
|
|
var inCart = findCartItem(p.id);
|
|
var up = getUnitPrice(p, inCart ? inCart.qty : 1);
|
|
var specHtml = '';
|
|
for (var s = 0; s < (p.specs||[]).length; s++) specHtml += '<span class="spec-tag">' + p.specs[s] + '</span>';
|
|
var starHtml = '';
|
|
for (var st = 0; st < Math.floor(p.rating); st++) starHtml += '<i class="fa-solid fa-star"></i>';
|
|
if (p.rating % 1) starHtml += '<i class="fa-solid fa-star-half-stroke"></i>';
|
|
starHtml += ' <span style="color:var(--fg-muted);font-size:.8rem;margin-left:.3rem">' + p.rating + ' (' + p.reviews + ' reviews)</span>';
|
|
var tierHtml = '';
|
|
if (p.tiers && p.tiers.length > 1) {
|
|
tierHtml = '<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>';
|
|
for (var t = 0; t < p.tiers.length; t++) {
|
|
tierHtml += '<div class="tier-row ' + (inCart && inCart.qty >= p.tiers[t].min ? 'active-tier' : '') + '"><span>' + p.tiers[t].min + '+ units</span><span>$' + p.tiers[t].p.toFixed(2) + ' ea</span></div>';
|
|
}
|
|
tierHtml += '</div>';
|
|
}
|
|
var freightBadge = p.freight ? '<span class="freight-badge" style="font-size:.7rem"><i class="fa-solid fa-truck" style="margin-right:.2rem"></i>Freight Quote Required</span>' : '';
|
|
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(/-/g,' ') + '</span>' + freightBadge + '</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">' + starHtml + '</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">' + specHtml + '</div>' +
|
|
'<div style="display:flex;align-items:baseline;gap:.75rem;margin-bottom:.25rem"><span style="font-size:2rem;font-weight:800;color:var(--accent)">$' + up.toFixed(2) + '</span><span style="font-size:.75rem;color:var(--fg-muted)">' + p.weight + ' kg</span></div>' +
|
|
tierHtml +
|
|
'<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':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> 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 48 hours') + '</span></div>' +
|
|
'</div></div>';
|
|
document.getElementById('product-modal').classList.add('open');
|
|
}
|
|
function closeProductModal() { document.getElementById('product-modal').classList.remove('open'); }
|
|
document.getElementById('product-modal').addEventListener('click', function(e) { if (e.target.id === 'product-modal') closeProductModal(); });
|
|
|
|
/* ================================================================
|
|
CART — tier pricing, stock enforcement, freight gating
|
|
================================================================ */
|
|
async function addToCart(id) {
|
|
if (!sec.checkRate('add_to_cart')) { toast('Rate limited — slow down', 'warn'); return; }
|
|
var p = findProduct(id);
|
|
if (!p) return;
|
|
if (p.freight) { toast(p.sku + ' requires a freight quote', 'warn'); await sec.logAudit('FREIGHT_BLOCKED', {sku:p.sku}); return; }
|
|
if (p.stock < 1) { toast('Out of stock', 'error'); return; }
|
|
var existing = findCartItem(id);
|
|
if (existing) {
|
|
if (existing.qty >= p.stock) { toast('Maximum stock reached', 'warn'); return; }
|
|
existing.qty++;
|
|
} else {
|
|
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) {
|
|
var idx = -1;
|
|
for (var i = 0; i < state.cart.length; i++) { if (state.cart[i].product.id === id) { idx = i; break; } }
|
|
if (idx === -1) return;
|
|
var 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) {
|
|
var item = findCartItem(id);
|
|
if (!item) return;
|
|
var 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() {
|
|
var s = 0;
|
|
for (var i = 0; i < state.cart.length; i++) s += getUnitPrice(state.cart[i].product, state.cart[i].qty) * state.cart[i].qty;
|
|
return s;
|
|
}
|
|
|
|
function renderCart() {
|
|
var items = document.getElementById('cart-items');
|
|
var countEl = document.getElementById('cart-count');
|
|
var totalItems = 0;
|
|
for (var i = 0; i < state.cart.length; i++) totalItems += state.cart[i].qty;
|
|
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;
|
|
}
|
|
var html = '';
|
|
for (var i = 0; i < state.cart.length; i++) {
|
|
var c = state.cart[i];
|
|
var up = getUnitPrice(c.product, c.qty);
|
|
html += '<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>';
|
|
}
|
|
items.innerHTML = html;
|
|
var 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 — validation, loading state, 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() {
|
|
var 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 via 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) { var v=el.value.replace(/\D/g,'').slice(0,16); el.value=v.replace(/(.{4})/g,'$1 ').trim(); }
|
|
function formatExpiry(el) { var 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() {
|
|
var btn = document.getElementById('place-order-btn');
|
|
if (!sec.checkRate('submit_order')) { toast('Rate limited — please wait', 'warn'); return; }
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner"></span> Processing...';
|
|
await new Promise(function(r){setTimeout(r,400)});
|
|
|
|
var 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()
|
|
};
|
|
var allFields = document.querySelectorAll('.field');
|
|
for (var f=0; f<allFields.length; f++) allFields[f].classList.remove('error');
|
|
document.getElementById('checkout-errors').innerHTML = '';
|
|
var errors = [];
|
|
var sanitized = {};
|
|
var keys = Object.keys(fields);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = keys[i], v = fields[k];
|
|
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');
|
|
|
|
var orderItems = [];
|
|
for (var c = 0; c < state.cart.length; c++) {
|
|
var ci = state.cart[c];
|
|
orderItems.push({name: ci.product.name, sku: ci.product.sku, price: getUnitPrice(ci.product, ci.qty), qty: ci.qty});
|
|
}
|
|
var ov = sec.validateOrder(orderItems, getCartTotal().toFixed(2));
|
|
if (!ov.valid) errors.push('Order validation: ' + (ov.error || 'unknown'));
|
|
|
|
if (errors.length) {
|
|
for (var e = 0; e < errors.length; e++) {
|
|
var 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>' + errors[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 */
|
|
for (var s = 0; s < state.cart.length; s++) {
|
|
var sp = state.cart[s].product;
|
|
for (var pi = 0; pi < PRODUCTS.length; pi++) {
|
|
if (PRODUCTS[pi].id === sp.id) { PRODUCTS[pi].stock = Math.max(0, PRODUCTS[pi].stock - state.cart[s].qty); break; }
|
|
}
|
|
}
|
|
|
|
var orderId = 'FK-' + Date.now().toString(36).toUpperCase() + '-' + Math.random().toString(36).slice(2, 6).toUpperCase();
|
|
var 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.';
|
|
var html = '<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>';
|
|
for (var i = 0; i < order.items.length; i++) {
|
|
var it = order.items[i];
|
|
html += '<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">' + it.sku + '</span>' + it.name + ' x' + it.qty + '</span><span style="font-weight:600">$' + (it.price * it.qty).toFixed(2) + '</span></div>';
|
|
}
|
|
html += '<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>';
|
|
html += '<div style="margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border);font-size:.75rem;color:var(--fg-muted)">';
|
|
html += '<p><strong>Customer:</strong> ' + order.customer.first + ' ' + order.customer.last + ' (' + order.customer.email + ')</p>';
|
|
if (order.customer.company) html += '<p style="margin-top:.25rem"><strong>Company:</strong> ' + order.customer.company + '</p>';
|
|
html += '<p style="margin-top:.25rem"><strong>Ship to:</strong> ' + order.address.street + ', ' + order.address.city + ', ' + order.address.state + ' ' + order.address.zip + '</p>';
|
|
html += '<p style="margin-top:.25rem"><strong>CSRF Valid:</strong> <span style="color:' + (order.csrfValid?'var(--success)':'var(--danger)') + '">' + (order.csrfValid?'Yes':'No') + '</span></p>';
|
|
html += '<p style="margin-top:.25rem"><strong>SHA-256 Chain:</strong> <span style="color:var(--success)">Verified</span></p></div></div>';
|
|
document.getElementById('confirm-details').innerHTML = html;
|
|
navigate('confirmation');
|
|
}
|
|
|
|
/* ================================================================
|
|
AUDIT LOG — render, verify, clear
|
|
================================================================ */
|
|
function renderAuditLog() {
|
|
var logs = sec.logs;
|
|
var tbody = document.getElementById('audit-tbody');
|
|
var empty = document.getElementById('audit-empty');
|
|
var persistNote = document.getElementById('audit-persist-note');
|
|
if (!logs.length) { tbody.innerHTML=''; empty.style.display='block'; persistNote.style.display='none'; return; }
|
|
empty.style.display = 'none';
|
|
var 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)'};
|
|
var sorted = logs.slice().reverse();
|
|
var html = '';
|
|
for (var i = 0; i < sorted.length; i++) {
|
|
var l = sorted[i];
|
|
var time = new Date(l.ts).toLocaleTimeString();
|
|
var details = typeof l.details === 'string' ? l.details : JSON.stringify(l.details);
|
|
html += '<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>';
|
|
}
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
async function verifyChain() {
|
|
var r = await sec.verifyChain();
|
|
var 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() {
|
|
var 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:'Input Sanitization',desc:'9-pattern regex XSS filter plus HTML entity encoding. Runs synchronously with zero external dependencies.',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 orders.',status:true,icon:'fa-key'},
|
|
{name:'Audit Log Persistence',desc:'Hash chain saved to localStorage on every write. Survives page refresh. Verified on load.',status:true,icon:'fa-database'},
|
|
{name:'Rate Limiting',desc:'Sliding window: 15 requests per 60 seconds per action type.',status:true,icon:'fa-gauge-high'},
|
|
{name:'Order Integrity',desc:'Server-side total recomputation prevents price manipulation. Quantity bounds 1-99. 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.',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.',status:true,icon:'fa-boxes-stacked'},
|
|
{name:'Freight Gating',desc:'Heavy items flagged freight:true are blocked from add-to-cart. Forces manual sales contact.',status:true,icon:'fa-truck'},
|
|
{name:'PHP Backend (Architecture)',desc:'Production PHP middleware: CSRF, rate limiting, sanitization, security headers, parameterized SQL.',status:true,icon:'fa-server',note:'Code in Architecture tab'},
|
|
];
|
|
var html = '';
|
|
for (var i = 0; i < features.length; i++) {
|
|
var f = features[i];
|
|
html += '<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:var(--accent-dim);display:flex;align-items:center;justify-content:center"><i class="fa-solid ' + f.icon + '" style="color:var(--accent);font-size:.9rem"></i></div><h3 style="font-size:.85rem;font-weight:700">' + f.name + '</h3></div><span class="sec-dot ' + (f.status?'on':'off') + '"></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:var(--accent);margin-top:.5rem;font-style:italic">' + f.note + '</p>':'') + '</div>';
|
|
}
|
|
document.getElementById('security-grid').innerHTML = html;
|
|
}
|
|
|
|
/* ================================================================
|
|
ARCHITECTURE TABS
|
|
================================================================ */
|
|
function switchArchTab(tab, btn) {
|
|
var panels = document.querySelectorAll('.arch-panel');
|
|
for (var i = 0; i < panels.length; i++) panels[i].style.display = 'none';
|
|
var btns = document.querySelectorAll('.tab-btn');
|
|
for (var j = 0; j < btns.length; j++) btns[j].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>\n<span class="kw">namespace</span> App\\Middleware;\n\n<span class="kw">class</span> <span class="fn">SecurityMiddleware</span>\n{\n <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>\n {\n <span class="cm">// 1. CSRF validation on POST</span>\n <span class="kw">if</span> (<span class="var">$req</span>-><span class="fn">isMethod</span>(<span class="str">\'POST\'</span>)) {\n <span class="kw">if</span> (!<span class="fn">hash_equals</span>(\n <span class="var">$req</span>-><span class="fn">session</span>()-><span class="fn">get</span>(<span class="str">\'csrf_token\'</span>),\n <span class="var">$req</span>-><span class="fn">header</span>(<span class="str">\'X-CSRF-Token\'</span>)\n )) <span class="kw">return</span> <span class="fn">response</span>()-><span class="fn">json</span>(\n [<span class="str">\'error\'</span>=><span class="str">\'CSRF failed\'</span>], <span class="num">403</span>\n );\n }\n\n <span class="cm">// 2. Sliding window rate limiter</span>\n <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>();\n <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>))\n <span class="kw">return</span> <span class="fn">response</span>()-><span class="fn">json</span>(\n [<span class="str">\'error\'</span>=><span class="str">\'Rate limited\'</span>], <span class="num">429</span>\n );\n\n <span class="cm">// 3. Recursive input sanitization</span>\n <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>()));\n\n <span class="cm">// 4. Security headers</span>\n <span class="var">$res</span> = <span class="var">$next</span>(<span class="var">$req</span>);\n <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>);\n <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>);\n <span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">\'Content-Security-Policy\'</span>,\n <span class="str">"default-src \'self\'; script-src \'self\'"</span>);\n <span class="kw">return</span> <span class="var">$res</span>;\n }\n}</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. Zero dependencies.</p></div>' +
|
|
'<div class="code-block mono"><span class="kw">class</span> <span class="fn">SecurityEngine</span> {\n <span class="fn">constructor</span>(sessionId) {\n <span class="var">this</span>.logs = [];\n <span class="var">this</span>.prevHash = <span class="str">\'GENESIS\'</span>;\n <span class="var">this</span>.sanitizerPatterns = [\n /<script[\\s\\S]*?<\\/script>/gi,\n /javascript\\s*:/gi,\n /on\\w+\\s*=/gi,\n /<iframe/gi, /<object/gi,\n /<embed/gi, /<form/gi,\n /expression\\s*\\(/gi,\n /@import/gi,\n ];\n }\n\n <span class="cm">// SHA-256 hash-chained audit log</span>\n <span class="kw">async</span> <span class="fn">logAudit</span>(action, details) {\n <span class="kw">const</span> entry = {\n ts: <span class="fn">Date</span>.<span class="fn">now</span>(), action, details,\n session: <span class="var">this</span>.sessionId.<span class="fn">slice</span>(<span class="num">0</span>,<span class="num">12</span>),\n prevHash: <span class="var">this</span>.prevHash\n };\n <span class="kw">const</span> hash = <span class="kw">await</span> <span class="fn">sha256</span>(\n <span class="fn">JSON</span>.<span class="fn">stringify</span>(entry,\n <span class="fn">Object</span>.<span class="fn">keys</span>(entry).<span class="fn">sort</span>())\n );\n entry.hash = hash.<span class="fn">slice</span>(<span class="num">0</span>, <span class="num">16</span>);\n <span class="var">this</span>.prevHash = entry.hash;\n <span class="var">this</span>.logs.<span class="fn">push</span>(entry);\n <span class="var">this</span>._persist(); <span class="cm">// localStorage</span>\n <span class="kw">return</span> entry;\n }\n\n <span class="cm">// crypto.subtle.digest — OS-level crypto</span>\n <span class="kw">async</span> <span class="fn">sha256</span>(message) {\n <span class="kw">const</span> buf = <span class="kw">await</span> crypto.subtle.<span class="fn">digest</span>(\n <span class="str">\'SHA-256\'</span>,\n <span class="kw">new</span> <span class="fn">TextEncoder</span>().<span class="fn">encode</span>(message)\n );\n <span class="kw">return</span> [...<span class="kw">new</span> <span class="fn">Uint8Array</span>(buf)]\n .<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>))\n .<span class="fn">join</span>(<span class="str">\'\'</span>);\n }\n\n <span class="cm">// CSRF via crypto.getRandomValues</span>\n <span class="fn">generateCsrf</span>() {\n <span class="kw">const</span> buf = <span class="kw">new</span> <span class="fn">Uint8Array</span>(<span class="num">16</span>);\n crypto.<span class="fn">getRandomValues</span>(buf);\n <span class="var">this</span>.csrfToken = [...buf]\n .<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>))\n .<span class="fn">join</span>(<span class="str">\'\'</span>);\n }\n\n <span class="fn">sanitize</span>(text) {\n <span class="kw">let</span> r = text;\n <span class="kw">for</span> (<span class="kw">const</span> p <span class="kw">of</span> <span class="var">this</span>.sanitizerPatterns)\n r = r.<span class="fn">replace</span>(p, <span class="str">\'[SANITIZED]\'</span>);\n <span class="kw">return</span> r.<span class="fn">replace</span>(<span class="str">/&/g</span>,<span class="str">\'&amp;\'</span>)\n .<span class="fn">replace</span>(<span class="str">/</g</span>,<span class="str">\'&lt;\'</span>)\n .<span class="fn">replace</span>(<span class="str">/>/g</span>,<span class="str">\'&gt;\'</span>);\n }\n}</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',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(function(s, i, a) {
|
|
return '<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 — verify crypto, restore audit log, render everything
|
|
================================================================ */
|
|
async function init() {
|
|
var bar = document.getElementById('load-bar');
|
|
var status = document.getElementById('load-status');
|
|
|
|
status.textContent = 'Initializing security engine...';
|
|
bar.style.width = '25%';
|
|
sec = new SecurityEngine(state.sessionId);
|
|
sec.generateCsrf();
|
|
|
|
status.textContent = 'Verifying Web Crypto API...';
|
|
bar.style.width = '60%';
|
|
var 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');
|
|
|
|
status.textContent = 'Restoring audit log...';
|
|
bar.style.width = '85%';
|
|
var restoredCount = sec._restoreFromStorage();
|
|
if (restoredCount > 0) {
|
|
document.getElementById('persist-count').textContent = restoredCount;
|
|
document.getElementById('audit-persist-note').style.display = 'block';
|
|
}
|
|
|
|
status.textContent = 'Ready';
|
|
bar.style.width = '100%';
|
|
await sec.logAudit('SESSION_START', {crypto: cryptoOk, restored: restoredCount});
|
|
|
|
renderCategoryFilters();
|
|
renderProducts();
|
|
renderCart();
|
|
renderArchitecturePanels();
|
|
|
|
await new Promise(function(r) { setTimeout(r, 300); });
|
|
document.getElementById('loader').classList.add('hidden');
|
|
document.getElementById('main-nav').style.opacity = '1';
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeProductModal();
|
|
if (document.getElementById('cart-sidebar').classList.contains('open')) toggleCart();
|
|
}
|
|
});
|
|
|
|
init().catch(function(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> |