Hugdrigo commited on
Commit
496f0f3
·
verified ·
1 Parent(s): 4c8e3eb

Upload 3 files

Browse files
Files changed (3) hide show
  1. LICENSE +21 -0
  2. README.md +37 -6
  3. index.html +174 -18
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hugdrigo / Rodrigo Pantoja Navajas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,43 @@
1
  ---
2
- title: Privacymap Data Processing Risk
3
- emoji: 🏢
4
- colorFrom: yellow
5
- colorTo: gray
6
  sdk: static
 
7
  pinned: false
8
  license: mit
9
- short_description: Privacy exposure mapping.
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "PrivacyMap | Privacy Processing Assessment Studio"
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
  license: mit
10
+ short_description: "Privacy exposure mapping."
11
  ---
12
 
13
+ # PrivacyMap | Hugdrigo Project #130
14
+
15
+ **Privacy Processing Assessment Studio**
16
+
17
+ Explore data-processing activities, retention settings, access footprints, and illustrative mitigations.
18
+
19
+ ## Capabilities
20
+
21
+ - Four distinct scenarios, with three adjustable parameters affecting actual model outputs.
22
+ - Record management: add, edit, archive, restore, local save and JSON backup/import.
23
+ - Computed indicators, comparisons and CSV export without cloud calls.
24
+ - Responsive desktop and mobile interface, accessible controls and light/dark themes.
25
+ - Entirely static HTML, CSS and JavaScript, no paid API, build pipeline or CDN.
26
+
27
+ ## Reproducible model methodology
28
+
29
+ - Risk index combines sensitivity, months above target, approximate access surface, and current control maturity.
30
+ - Scenario access reduction and control improvement reduce risk according to an explicit simplified scale.
31
+ - The tool does not assess lawful bases, rights, country-specific regulations, or establish regulatory compliance.
32
+
33
+ **Practical interpretation:** Inspect large sensitive datasets that remain outside proposed retention caps.
34
+
35
+ ## Launch on Hugging Face Spaces
36
+
37
+ Create a **Public** Space named `privacymap-data-processing-risk`, set **Static** SDK, choose **Blank** template and MIT license. Upload `README.md`, `index.html`, and `LICENSE` in the repo root. No GPU or secrets are required.
38
+
39
+ ## Limits and privacy
40
+
41
+ All inputs, names, data, cost estimates and scenario outcomes are synthetic demonstrations, not real forecasts, laboratory evidence, legal advice, certification, audited measurements or investment recommendations. Models deliberately simplify reality. Records and scenario settings are saved only in this browser using localStorage and may be lost if browser storage is cleared. Use JSON export for backups. Do not publish sensitive datasets in a public Space repository.
42
+
43
+ © 2026 Hugdrigo. Project #130. MIT License.
index.html CHANGED
@@ -1,19 +1,175 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#0b1425"><title>PrivacyMap | Hugdrigo</title>
3
+ <style>
4
+ :root{color-scheme:dark;--bg:#080e1a;--nav:#0e1728;--panel:#121e31;--panel2:#17263a;--line:#27394e;--text:#eef5ff;--muted:#93a9c1;--accent:#80d8ce;--green:#7ce2b1;--yellow:#f3c77e;--red:#ff929e;--shadow:0 24px 56px #0003}
5
+ body.light{color-scheme:light;--bg:#f3f7fc;--nav:#eaf2fa;--panel:#fff;--panel2:#eef3fa;--line:#dce4ef;--text:#14263b;--muted:#61758a;--shadow:0 18px 40px #1c466212}
6
+ *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Arial;min-width:280px}body:before{content:"";position:fixed;pointer-events:none;z-index:-1;inset:0;background:radial-gradient(circle at 89% 0%,color-mix(in srgb,var(--accent) 9%,transparent),transparent 35%)}button,input,select{font:inherit}button{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}a{color:inherit}strong{font-weight:720}
7
+ .app{display:flex;min-height:100vh}.sidebar{flex:0 0 238px;position:sticky;top:0;height:100vh;padding:26px 17px;display:flex;flex-direction:column;background:var(--nav);border-right:1px solid var(--line)}.brand{display:flex;align-items:center;gap:12px;padding:5px 12px 28px}.brandmark{height:39px;width:39px;border:1px solid color-mix(in srgb,var(--accent) 55%,transparent);background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-radius:13px;display:grid;place-items:center;font-weight:900;font-size:19px;box-shadow:0 0 20px color-mix(in srgb,var(--accent) 10%,transparent)}.brand strong{display:block;letter-spacing:.5px;font-size:15px}.brand small{display:block;color:var(--muted);font-size:10px}.nav-label{font-size:10px;letter-spacing:2px;font-weight:800;color:var(--muted);margin:8px 12px 13px}.navbtn{display:flex;align-items:center;gap:12px;width:100%;border:1px solid transparent;border-radius:11px;background:transparent;color:var(--muted);padding:12px 13px;margin-bottom:5px;text-align:left;font-weight:650}.navbtn.active{color:var(--text);background:color-mix(in srgb,var(--accent) 11%,var(--panel));border-color:color-mix(in srgb,var(--accent) 25%,transparent)}.navbtn:hover{background:var(--panel2);color:var(--text)}.nav-ico{font-size:16px;width:22px;text-align:center}.sidebottom{margin-top:auto;padding:16px 12px 4px;border-top:1px solid var(--line)}.sidebottom small{color:var(--muted);font-size:11px}.sidebottom b{display:block;margin:8px 0;color:var(--accent);font-size:12px}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);margin-right:6px;box-shadow:0 0 12px var(--green)}
8
+ .main{min-width:0;flex:1;padding:29px clamp(18px,3.2vw,55px) 40px;max-width:1750px;margin:auto}.topbar{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:26px}.crumb{font-size:11px;text-transform:uppercase;color:var(--muted);letter-spacing:1.65px;font-weight:800}.crumb strong{color:var(--accent)}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:flex-end}.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 13px;border:1px solid var(--line);border-radius:9px;color:var(--text);background:var(--panel);font-weight:700;font-size:12px;white-space:nowrap}.btn:hover{border-color:var(--accent)}.btn.primary{background:var(--accent);color:#081625;border-color:var(--accent)}.btn.warn{color:var(--red)}.btn.tiny{font-size:11px;padding:6px 9px}.hero{display:flex;gap:24px;justify-content:space-between;align-items:center;margin-bottom:23px}.eyebrow{font-weight:800;letter-spacing:1.7px;color:var(--accent);font-size:10px;text-transform:uppercase}.hero h1{font-size:clamp(28px,3.6vw,47px);letter-spacing:-1.6px;line-height:1.08;margin:8px 0 11px;font-weight:820}.hero p{margin:0;color:var(--muted);max-width:760px}.hero-art{height:104px;width:156px;flex:0 0 156px;border:1px solid color-mix(in srgb,var(--accent) 30%,var(--line));border-radius:22px;background:linear-gradient(145deg,color-mix(in srgb,var(--accent) 16%,var(--panel)),var(--panel));position:relative;overflow:hidden}.hero-art:before{content:"";position:absolute;width:135px;height:135px;border-radius:45px;border:2px solid var(--accent);transform:rotate(35deg);top:24px;left:50px;opacity:.35}.hero-art:after{content:"";position:absolute;width:75px;height:75px;border-radius:22px;border:1px solid var(--accent);transform:rotate(35deg);top:11px;left:20px;opacity:.6}.hero-art span{position:absolute;top:40px;left:63px;font-size:18px;color:var(--accent);font-weight:900;z-index:2}.statusbar{display:flex;gap:10px;flex-wrap:wrap;margin:0 0 23px}.tag{border:1px solid var(--line);border-radius:18px;padding:6px 11px;font-weight:750;color:var(--muted);font-size:10px;letter-spacing:.5px}.tag.accent{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 27%,var(--line))}
9
+ .metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:19px}.card{min-width:0;border:1px solid var(--line);background:var(--panel);border-radius:15px;box-shadow:var(--shadow)}.metric{padding:20px}.metric .small{font-size:11px;letter-spacing:.1px;color:var(--muted);font-weight:650}.metric strong{font-size:clamp(20px,2.2vw,30px);line-height:1.35;display:block;letter-spacing:-.7px;margin:5px 0 2px;overflow-wrap:anywhere}.metric .foot{color:var(--muted);font-size:10px}.metric i{display:inline-block;height:4px;width:29px;background:var(--accent);border-radius:4px;margin-top:13px}.metric:nth-child(2) i{background:var(--green)}.metric:nth-child(3) i{background:var(--yellow)}.metric:nth-child(4) i{background:#9ca6f8}
10
+ .columns{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(275px,.9fr);gap:18px;align-items:start}.stack{display:grid;gap:18px;align-content:start}.panel{padding:22px}.panel h2{font-size:15px;letter-spacing:-.1px;margin:0;font-weight:800}.panel .sub{font-size:11px;color:var(--muted);margin-top:5px}.panel-head{display:flex;gap:15px;justify-content:space-between;align-items:flex-start;margin-bottom:19px}.badge{font-size:10px;font-weight:760;color:var(--accent);border:1px solid color-mix(in srgb,var(--accent) 25%,var(--line));background:color-mix(in srgb,var(--accent) 8%,transparent);padding:5px 9px;border-radius:50px;white-space:nowrap}.barrow{display:grid;grid-template-columns:minmax(94px,1fr) 3.5fr minmax(72px,1fr);align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--line)}.barrow:last-child{border:0}.barrow .nm{font-weight:650;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.barrow .val{text-align:right;font-variant-numeric:tabular-nums;font-size:11px;font-weight:800}.track{height:9px;border-radius:10px;background:var(--panel2);overflow:hidden}.fill{height:100%;border-radius:10px;background:linear-gradient(90deg,var(--accent),color-mix(in srgb,var(--accent) 45%,#fff))}.insight{padding:13px;border:1px solid color-mix(in srgb,var(--accent) 25%,var(--line));background:color-mix(in srgb,var(--accent) 7%,transparent);border-radius:10px;margin-top:14px;color:var(--text);font-size:12px}.insight strong{color:var(--accent)}.stat-pair{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line);padding:11px 0;font-size:12px}.stat-pair span{color:var(--muted)}.stat-pair:last-child{border:none}.muted{color:var(--muted)}.note{color:var(--muted);font-size:11px;margin-top:13px}
11
+ .preset-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;margin-bottom:19px}.preset{background:var(--panel2);border:1px solid var(--line);border-radius:11px;padding:12px;text-align:left;color:var(--text)}.preset.selected{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,var(--panel2))}.preset b{display:block;font-size:12px}.preset small{display:block;color:var(--muted);font-size:10px;margin-top:5px}.control{padding:13px 0;border-bottom:1px solid var(--line)}.control:last-child{border:none}.controlhead{display:flex;justify-content:space-between;font-size:12px;font-weight:680;margin-bottom:11px}.controlhead output{color:var(--accent);font-weight:800}.control input{width:100%;accent-color:var(--accent)}.spread{display:flex;gap:12px;justify-content:space-between;align-items:center;flex-wrap:wrap}.section-title{margin:0 0 16px}.section-title h2{font-size:20px;margin:0 0 5px;letter-spacing:-.4px}.section-title p{color:var(--muted);margin:0;font-size:12px}
12
+ .toolbar{display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:18px}.search{border:1px solid var(--line);background:var(--panel2);border-radius:9px;color:var(--text);padding:10px 12px;min-width:180px;max-width:100%;flex:1}.tablewrap{overflow:auto;width:100%;border-radius:10px;border:1px solid var(--line)}table{border-collapse:collapse;width:100%;min-width:680px;font-size:11px}thead{background:var(--panel2);color:var(--muted)}th,td{text-align:left;padding:13px 13px;border-bottom:1px solid var(--line);vertical-align:middle}th{font-size:10px;letter-spacing:.4px;font-weight:800;white-space:nowrap}td strong{font-size:12px}tr:last-child td{border-bottom:0}tbody tr:hover{background:color-mix(in srgb,var(--accent) 5%,transparent)}.pill{padding:5px 8px;border-radius:20px;background:color-mix(in srgb,var(--accent) 9%,transparent);color:var(--accent);font-size:10px;font-weight:750}.pill.bad{color:var(--red);background:color-mix(in srgb,var(--red) 9%,transparent)}.rowact{display:flex;gap:6px}.empty{text-align:center;padding:36px;color:var(--muted)}
13
+ .methods{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.method-card{border:1px solid var(--line);border-radius:13px;padding:19px;background:var(--panel)}.method-card .number{font-size:11px;font-weight:900;color:var(--accent);letter-spacing:1px}.method-card p{font-size:12px;color:var(--muted);margin:9px 0 0}.disclaimer{border:1px solid color-mix(in srgb,var(--yellow) 45%,var(--line));background:color-mix(in srgb,var(--yellow) 5%,var(--panel));border-radius:12px;padding:17px;margin-top:16px;font-size:12px;line-height:1.6}.disclaimer strong{color:var(--yellow)}.footer{display:flex;gap:20px;justify-content:space-between;align-items:center;flex-wrap:wrap;color:var(--muted);font-size:10px;margin-top:25px;border-top:1px solid var(--line);padding-top:19px}
14
+ .modal{position:fixed;inset:0;z-index:10;display:none;align-items:center;justify-content:center;padding:16px;background:#020817bb;backdrop-filter:blur(8px)}.modal.show{display:flex}.dialog{width:min(620px,100%);max-height:91vh;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:17px;padding:22px;box-shadow:0 26px 85px #0009}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.field label{display:block;color:var(--muted);font-size:11px;font-weight:720;margin-bottom:5px}.field input{width:100%;min-width:0;border-radius:9px;border:1px solid var(--line);background:var(--panel2);color:var(--text);padding:11px}.field:first-child{grid-column:1/-1}.modalbottom{display:flex;justify-content:flex-end;gap:9px;margin-top:20px}.toast{position:fixed;right:20px;bottom:20px;max-width:min(400px,calc(100% - 40px));z-index:15;background:var(--panel2);color:var(--text);border:1px solid var(--accent);padding:12px 17px;border-radius:10px;box-shadow:var(--shadow);display:none;font-size:12px}.toast.show{display:block}.lab-cols{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}.chartsvg{width:100%;height:180px;display:block;overflow:visible}.small-list{padding:0;margin:0;list-style:none}.small-list li{padding:12px 0;border-bottom:1px solid var(--line);font-size:12px}.small-list li:last-child{border:0}.small-list b{display:block}.small-list span{color:var(--muted)}
15
+ @media(max-width:1110px){.sidebar{flex-basis:206px}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.columns{grid-template-columns:1fr}.hero-art{display:none}}
16
+ @media(max-width:780px){.app{display:block}.sidebar{position:relative;width:100%;height:auto;padding:10px 14px 9px;border-right:0;border-bottom:1px solid var(--line)}.brand{padding:7px 3px 9px}.brandmark{width:32px;height:32px;border-radius:9px}.nav-label,.sidebottom{display:none}.navbox{display:flex;gap:5px;overflow-x:auto;scrollbar-width:none}.navbtn{width:auto;white-space:nowrap;padding:9px;margin:0;font-size:11px}.nav-ico{width:auto}.main{padding:17px 16px 30px}.topbar{margin-bottom:24px}.hero{margin-bottom:18px}.preset-list{grid-template-columns:repeat(2,minmax(0,1fr))}}
17
+ @media(max-width:520px){.crumb{font-size:9px}.topbar{align-items:start}.actions{gap:6px}.actions .btn{font-size:10px;padding:8px}.hero h1{font-size:30px}.hero p{font-size:12px}.metrics{gap:9px}.metric{padding:14px}.metric strong{font-size:22px}.metric .small{font-size:10px}.metric .foot{font-size:9px}.panel{padding:15px}.columns,.stack{gap:12px}.barrow{grid-template-columns:minmax(72px,1fr) 2.0fr minmax(55px,.7fr);gap:7px}.lab-cols,.methods{grid-template-columns:1fr}.fields{grid-template-columns:1fr}.footer{gap:8px}.tag{font-size:9px;padding:5px 8px}}
18
+ </style></head><body><div class="app"><aside class="sidebar"><div class="brand"><div class="brandmark">H</div><div><strong>HUGDRIGO</strong><small>RESEARCH & INTELLIGENCE LABS</small></div></div><div class="nav-label">WORKSPACE</div><nav class="navbox" id="navbox" aria-label="Application navigation"><button class="navbtn active" data-page="overview"><span class="nav-ico">◈</span>Overview</button><button class="navbtn" data-page="lab"><span class="nav-ico">◇</span>Scenario Lab</button><button class="navbtn" data-page="records"><span class="nav-ico">▤</span>Records</button><button class="navbtn" data-page="method"><span class="nav-ico">◉</span>Methodology</button></nav><div class="sidebottom"><small>PROJECT SERIES</small><b>#130 / HUGDRIGO STUDIO</b><small><span class="dot"></span>Static • Offline-capable • Synthetic data</small></div></aside><main class="main"><header class="topbar"><div class="crumb">HUGDRIGO <span aria-hidden="true">/</span> PROJECT <strong>#130</strong></div><div class="actions"><button class="btn" id="themeBtn" title="Toggle theme">☼ Theme</button><button class="btn" id="jsonBtn">↓ Backup</button><button class="btn primary" id="csvBtn">↧ Export CSV</button></div></header><section class="hero"><div><div class="eyebrow">DATA GOVERNANCE · PRIVACY</div><h1>PrivacyMap<span style="color:var(--accent)">.</span></h1><p>Explore data-processing activities, retention settings, access footprints, and illustrative mitigations.</p></div><div class="hero-art" aria-hidden="true"><span>#130</span></div></section><div class="statusbar"><span class="tag accent">LIVE WHAT-IF MODEL</span><span class="tag">SYNTHETIC DATA</span><span class="tag">NO EXTERNAL API</span><span class="tag">PROJECT #130</span></div><section id="page" aria-live="polite"></section><footer class="footer"><span>© 2026 Hugdrigo · MIT licensed · All values are illustrative</span><span>Privacy: all edits are kept only in this browser</span></footer></main></div><div class="modal" id="modal" role="dialog" aria-modal="true" aria-label="Record editor"><div class="dialog"><div class="spread"><h2 id="editTitle">Edit record</h2><button id="closeModal" class="btn tiny" aria-label="Close editor">✕</button></div><form id="editForm"><div class="fields" id="fields"></div><div class="modalbottom"><button type="button" id="cancelModal" class="btn">Cancel</button><button type="submit" class="btn primary">Save record</button></div></form></div></div><div class="toast" id="toast" role="status"></div><input type="file" id="importFile" accept="application/json,.json" hidden><script>
19
+ const CFG={"number":130,"name":"PrivacyMap","slug":"privacymap-data-processing-risk","subtitle":"Privacy Processing Assessment Studio","short":"Privacy exposure mapping.","eyebrow":"DATA GOVERNANCE · PRIVACY","description":"Explore data-processing activities, retention settings, access footprints, and illustrative mitigations.","model":"privacy","accent":"#80d8ce","fields":[{"key":"name","label":"Processing activity","kind":"text","min":0,"max":0,"step":1},{"key":"records","label":"Records held","kind":"number","min":50,"max":50000000,"step":100},{"key":"months","label":"Retention months","kind":"number","min":1,"max":240,"step":1},{"key":"sensitivity","label":"Sensitivity 1–5","kind":"number","min":1,"max":5,"step":1},{"key":"access","label":"Authorized staff","kind":"number","min":1,"max":30000,"step":1},{"key":"readiness","label":"Control maturity /100","kind":"number","min":0,"max":100,"step":1}],"rows":[{"id":"R01","name":"Customer CRM","records":430000,"months":48,"sensitivity":3,"access":140,"readiness":70},{"id":"R02","name":"HR applicant data","records":55000,"months":24,"sensitivity":4,"access":24,"readiness":61},{"id":"R03","name":"Analytics telemetry","records":2900000,"months":18,"sensitivity":2,"access":85,"readiness":78},{"id":"R04","name":"Employee benefits","records":12000,"months":84,"sensitivity":5,"access":18,"readiness":63},{"id":"R05","name":"Support tickets","records":180000,"months":36,"sensitivity":3,"access":110,"readiness":74},{"id":"R06","name":"Event registration","records":94000,"months":30,"sensitivity":2,"access":33,"readiness":87},{"id":"R07","name":"Supplier registry","records":42000,"months":60,"sensitivity":3,"access":48,"readiness":67}],"controls":[{"key":"retention","label":"Target retention cap","min":3,"max":120,"step":3,"value":24,"unit":"mo"},{"key":"accessReduction","label":"Least-privilege reduction","min":0,"max":90,"step":5,"value":35,"unit":"%"},{"key":"controls","label":"Control improvements","min":0,"max":85,"step":5,"value":25,"unit":"%"}],"presets":[{"name":"Baseline","desc":"Typical controls","values":[24,35,25]},{"name":"Over-retention","desc":"Short policy capacity","values":[6,0,0]},{"name":"Least privilege","desc":"Strong access controls","values":[18,80,60]},{"name":"Extended retention","desc":"Permissive policy","values":[96,10,10]}],"metrics":[["Exposure-weighted records","num"],["Excess retention months","num"],["Risk score","pct"]],"methodology":["Risk index combines sensitivity, months above target, approximate access surface, and current control maturity.","Scenario access reduction and control improvement reduce risk according to an explicit simplified scale.","The tool does not assess lawful bases, rights, country-specific regulations, or establish regulatory compliance."],"insight":"Inspect large sensitive datasets that remain outside proposed retention caps."};
20
+ 'use strict';
21
+ const BYID=id=>document.getElementById(id);
22
+ const esc=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
23
+ const num=x=>Number.isFinite(+x)?+x:0;
24
+ const clamp=(v,lo,hi)=>Math.max(lo,Math.min(hi,v));
25
+ const fmt=(v,t)=>{v=Number.isFinite(v)?v:0;return t==='money'?'$'+v.toLocaleString('en-US',{maximumFractionDigits:0}):t==='moneyK'?'$'+v.toLocaleString('en-US',{maximumFractionDigits:0})+'k':t==='pct'?v.toFixed(1)+'%':v.toLocaleString('en-US',{maximumFractionDigits:1});};
26
+ const base=()=>CFG.rows.map(x=>({...x,archived:false}));
27
+ const defaultControls=()=>Object.fromEntries(CFG.controls.map(c=>[c.key,c.value]));
28
+ const STORAGE_KEY='hugdrigo_studio_'+CFG.number+'_v1';
29
+ let state={records:base(), controls:defaultControls(), page:'overview', search:'', preset:0, theme:'dark'};
30
+ let editId=null;
31
+ function validRecord(raw){if(!raw||typeof raw!=='object'||Array.isArray(raw))return null;const r={id:typeof raw.id==='string'&&/^[\w-]{1,45}$/.test(raw.id)?raw.id:'R'+Math.random().toString(36).slice(2,9),archived:raw.archived===true};for(const f of CFG.fields){if(f.kind==='text'){if(typeof raw[f.key]!=='string'||(!raw[f.key].trim()&&f.key!=='deps')||raw[f.key].length>160)return null;r[f.key]=raw[f.key].trim().slice(0,140);}else{const v=raw[f.key];if(typeof v!=='number'||!Number.isFinite(v)||v<f.min||v>f.max)return null;r[f.key]=v;}}if(CFG.model==='coldtrace'&&r.minTemp>=r.maxTemp)return null;if(CFG.model==='survey'&&(r.positive>r.respondents||r.respondents>r.population))return null;return r;}
32
+ function validRecords(records){if(!Array.isArray(records)||records.length>150||records.length<1)return null;const out=records.map(validRecord);if(out.some(x=>!x))return null;const ids=new Set(out.map(x=>x.id));if(ids.size!==out.length)return null;if(CFG.model==='lineage'&&!validGraph(out))return null;return out;}
33
+ function validGraph(rows){const ids=new Set(rows.map(x=>x.assetId));if(ids.size!==rows.length)return false;const map=new Map(rows.map(x=>[x.assetId,x.deps.split(';').map(x=>x.trim()).filter(Boolean)]));for(let [id,deps] of map){if(deps.some(d=>!map.has(d)||d===id))return false;}let visiting=new Set(),visited=new Set();const visit=id=>{if(visiting.has(id))return false;if(visited.has(id))return true;visiting.add(id);for(const d of map.get(id))if(!visit(d))return false;visiting.delete(id);visited.add(id);return true;};return [...ids].every(visit);}
34
+ function load(){try{const s=JSON.parse(localStorage.getItem(STORAGE_KEY)||'null');if(s){const r=validRecords(s.records);if(r)state.records=r;if(s.controls&&typeof s.controls==='object'){for(const c of CFG.controls){const v=s.controls[c.key];if(typeof v==='number'&&Number.isFinite(v))state.controls[c.key]=clamp(v,c.min,c.max);}}if(s.theme==='light')state.theme='light';}}catch(e){}document.body.classList.toggle('light',state.theme==='light');}
35
+ function save(){try{localStorage.setItem(STORAGE_KEY,JSON.stringify({project:CFG.number,records:state.records,controls:state.controls,theme:state.theme}));}catch(e){toast('Browser storage not available; exports still work.');}}
36
+ let toastTimer;function toast(txt){const el=BYID('toast');el.textContent=txt;el.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.remove('show'),3800);}
37
+ function model(rows,p){const kind=CFG.model;let out=[];
38
+ if(kind==='civicbudget'){
39
+ const valid=rows.filter(r=>!r.archived&&r.readiness>=p.readinessFloor);const total=Math.floor(p.budget), n=valid.length;const dp=Array.from({length:n+1},()=>Array(total+1).fill(0));const merit=r=>(r.impact*(1-p.equityWeight/100)+r.equity*(p.equityWeight/100))*Math.sqrt(r.beneficiaries/1000)*(.4+.6*r.readiness/100);
40
+ for(let i=1;i<=n;i++){let r=valid[i-1];for(let b=0;b<=total;b++)dp[i][b]=Math.max(dp[i-1][b], b>=r.budget?dp[i-1][b-r.budget]+merit(r):0);}
41
+ const chosen=new Set();let b=total;for(let i=n;i>0;i--){const r=valid[i-1];if(b>=r.budget && dp[i][b]>dp[i-1][b]+1e-7){chosen.add(r.id);b-=r.budget;}}
42
+ out=rows.filter(r=>!r.archived).map(r=>({r,primary:chosen.has(r.id)?r.budget:0,secondary:chosen.has(r.id)?r.beneficiaries:0,risk:100-r.readiness,score:merit(r),note:chosen.has(r.id)?'Selected by the constrained 0/1 portfolio optimizer.':'Not funded under current constraints.',selected:chosen.has(r.id)}));
43
+ }else if(kind==='lineage'){
44
+ const active=rows.filter(r=>!r.archived),byId=new Map(active.map(x=>[x.assetId,x]));const children=new Map(active.map(r=>[r.assetId,[]]));active.forEach(r=>r.deps.split(';').map(s=>s.trim()).filter(Boolean).forEach(d=>{if(children.has(d))children.get(d).push(r.assetId);}));
45
+ out=active.map(r=>{let seen=new Map([[r.assetId,0]]),todo=[[r.assetId,0]];while(todo.length){const [id,dist]=todo.shift();for(const kid of children.get(id)||[]){if(!seen.has(kid)){seen.set(kid,dist+1);todo.push([kid,dist+1]);}}}let lost=0;for(const [id,depth] of seen){const a=byId.get(id);lost+=a.volume*1000*(p.shock/100)*(1-p.redundancy/100)*Math.pow(.72,depth)*(a.recovery/24)*(1-p.restore/100);}return {r,primary:lost,secondary:seen.size,risk:clamp((seen.size*10+r.criticality*4)*p.shock/100*(1-p.redundancy/100),0,100),score:r.criticality*seen.size,note:seen.size+' assets are reachable from this upstream node.'};});
46
+ }else{
47
+ out=rows.filter(r=>!r.archived).map(r=>{let primary=0,secondary=0,risk=0,score=0,note='';
48
+ switch(kind){
49
+ case 'ocean': {
50
+ const h=clamp(r.health-p.warming*(4.1+r.fishing*.045)+p.restoration*.15+p.enforcement*.042,0,100);
51
+ const compliance=clamp((r.compliance+p.enforcement*.28)/100,0,1);
52
+ primary=r.species*(h/100)*(.65+.35*compliance);secondary=r.hectares*(p.restoration*.32+p.enforcement*.09);
53
+ risk=100-h;score=h*(r.species/100);note=`Illustrative habitat health ${h.toFixed(1)}/100, not a population census.`;break;
54
+ }
55
+ case 'retrofit': {
56
+ const kwhSaved=Math.min(r.kwh,r.kwh*r.savings/100*(p.performance/100)*(.78+.14*r.hvac/100+.08*r.envelope/100));
57
+ primary=kwhSaved*r.tariff*(1+p.tariffShift/100);secondary=r.capex*(1-p.incentive/100);
58
+ const payback=primary>0?secondary/primary:999;
59
+ risk=clamp(payback/15*100,0,100);score=primary/(1+secondary/100000);note=`${kwhSaved.toFixed(0)} kWh/year saved; simple payback ${payback.toFixed(1)} years.`;break;
60
+ }
61
+ case 'survey': {
62
+ const phat=clamp((r.positive/r.respondents)+p.bias/100,0,1);
63
+ const fpc=Math.sqrt(Math.max(0,(r.population-r.respondents)/Math.max(1,r.population-1)));
64
+ const me=p.zScore*Math.sqrt(phat*(1-phat)/r.respondents*r.design*p.designMult)*fpc;
65
+ primary=phat*r.population;secondary=me*r.population;risk=clamp(me*100,0,100);score=100-risk;
66
+ note=`Estimated ${(100*phat).toFixed(1)}% ± ${(100*me).toFixed(1)} percentage points (approximate).`;break;
67
+ }
68
+ case 'bridge': {
69
+ const deterioration=clamp((100-r.condition)/100*(1+p.wear/100)+r.age/600,0,2.5);
70
+ const flood=(1+p.rain*.15)*(1+(100-r.redundancy)/200);
71
+ risk=clamp(deterioration*flood*64,0,100);primary=r.vehicles/1000*risk*(1+r.age/100);secondary=r.rehab;
72
+ score=primary/Math.sqrt(r.rehab+1);note=`Exposure proxy ${(primary).toFixed(0)}; maintenance decision pending allocation.`;break;
73
+ }
74
+ case 'museum': {
75
+ const humidityDeviation=Math.abs(r.humidity-50)/50*(1-p.humidityControl/100);
76
+ const heatDeviation=Math.max(0,r.temp+p.heat-22)/25;
77
+ const luxPressure=r.lux/800*(1-p.lightControl/100);
78
+ const hazard=clamp((humidityDeviation*.4+heatDeviation*.3+luxPressure*.3)*(r.fragility/100),0,1);
79
+ primary=r.value*hazard;secondary=r.items*hazard;risk=hazard*100;score=100-risk;
80
+ note=`Conservation stress ${(hazard*100).toFixed(1)}%; relative exposure, not expected insured loss.`;break;
81
+ }
82
+ case 'privacy': {
83
+ const retentionExcess=Math.max(0,r.months-p.retention),access=Math.max(0,r.access*(1-p.accessReduction/100));
84
+ const accessSurface=clamp(Math.log1p(access)/Math.log(30001),0,1);
85
+ const raw=(r.sensitivity/5*.38+Math.min(1,retentionExcess/60)*.27+accessSurface*.20+(1-r.readiness/100)*.15);
86
+ risk=clamp(100*raw*(1-p.controls/150),0,100);primary=r.records*risk/100;secondary=retentionExcess;
87
+ score=100-risk;note=`${retentionExcess} months above cap; ${Math.round(access)} modeled users with access.`;break;
88
+ }
89
+ case 'bond': {
90
+ const baseYield=r.yieldRate/100;
91
+ const scenarioYield=Math.max(.00001,baseYield+(p.rates+p.spreads)/10000);
92
+ const bondPv=(rate,cut)=>{let pv=0;for(let y=1;y<=r.maturity;y++){const cash=r.face*r.coupon/100+(y===r.maturity?r.face*(1-cut):0);pv+=cash/Math.pow(1+rate,y);}return pv;};
93
+ const basePrice=bondPv(baseYield,0),scenarioPrice=bondPv(scenarioYield,p.haircut/100);
94
+ primary=scenarioPrice*r.quantity;secondary=(scenarioPrice-basePrice)*r.quantity;
95
+ risk=basePrice?clamp(100*Math.abs(scenarioPrice/basePrice-1),0,100):0;score=primary;
96
+ note=`Bond price $${scenarioPrice.toFixed(2)} vs. original $${basePrice.toFixed(2)} (annual coupons).`;break;
97
+ }
98
+ case 'event': {
99
+ const sold=Math.min(r.seats,r.tickets*(1+p.demand/100));const attendees=sold*r.show/100;
100
+ const gross=sold*r.price+p.sponsor/rows.filter(x=>!x.archived).length;
101
+ const cost=(r.fixed+attendees*r.variable)*(1+p.inflation/100);
102
+ primary=gross-cost;secondary=attendees;risk=clamp((r.seats-sold)/r.seats*100,0,100);score=primary;
103
+ note=`${sold.toFixed(0)} tickets; ${attendees.toFixed(0)} expected attendees; costs $${cost.toFixed(0)}.`;break;
104
+ }
105
+ case 'orbit': {
106
+ const demand=r.terminals*r.demand*(1+p.growth/100);
107
+ const latencyFactor=clamp(1-(r.latency-30)/3000,.55,1);
108
+ const capacity=r.bandwidth*(1+p.spectrum/100)*(r.coverage/100)*(r.availability/100)*(1-p.weather/100)*latencyFactor;
109
+ primary=Math.min(capacity,demand);secondary=Math.max(0,demand-primary);
110
+ risk=demand>0?100*secondary/demand:0;score=100-risk;
111
+ note=`Capacity ${capacity.toFixed(1)} Mbps; demand ${demand.toFixed(1)} Mbps; nominal latency ${r.latency} ms.`;break;
112
+ }
113
+ case 'tourism': {
114
+ const seasonFactor=1+(r.season/100)*.1;
115
+ const demand=clamp(r.occupancy/100*(1+p.demand/100)*seasonFactor,0,1);
116
+ const occupancy=Math.min(demand,p.staff/100);
117
+ const nights=r.rooms*30*occupancy;
118
+ const gross=nights*r.adr*(1+p.rate/100);
119
+ primary=gross-nights*r.spend-r.fixed;secondary=nights;risk=occupancy*100;score=primary;
120
+ note=`Monthly room occupancy ${(occupancy*100).toFixed(1)}%; revenue $${gross.toFixed(0)}.`;break;
121
+ }
122
+
123
+
124
+ case 'urbanheat':{const temp=r.baseTemp+p.heat-.055*p.trees-.015*p.cooling;const excess=Math.max(0,temp-33);const frac=clamp((excess/12)*(.25+.75*r.sensitivity/100)*(.5+r.impervious/200)*clamp(1-(r.canopy+p.trees)/170,.25,1),0,1);primary=r.population*frac;secondary=r.population*r.unitCost*(p.trees+p.cooling/4)/100;risk=100*frac;score=100-risk;note='Projected '+temp.toFixed(1)+'°C after assumed cooling, '+(frac*100).toFixed(1)+'% exposed.';break;}
125
+ case 'coldtrace':{const offset=p.ambient*(1-p.insulation/100),t=r.temp+offset;const exceed=Math.max(0,t-r.maxTemp,r.minTemp-t),degree=exceed*(r.hours+p.delay);const frac=clamp(1-Math.exp(-degree/85),0,1);primary=r.units*r.unitValue*frac;secondary=r.units*frac;risk=100*frac;score=100-risk;note=degree.toFixed(1)+' excursion degree-hours; product loss is illustrative.';break;}
126
+ case 'agropulse':{const deficit=clamp(1-(p.rain/100*500)/r.waterDemand,0,1),ym=Math.max(0,1-r.sensitivity*deficit),harvest=r.hectares*r.yieldHa*ym;primary=harvest*r.priceTon*(1+p.price/100)-r.hectares*r.costHa*(1+p.input/100);secondary=harvest;risk=clamp(deficit*r.sensitivity*100,0,100);score=primary/(1+r.hectares);note='Modeled yield '+(r.yieldHa*ym).toFixed(2)+' t/ha; weather is not forecast.';break;}
127
+ case 'slo':{const requests=r.requests*30*p.traffic;const error=(r.errorRate/100)*p.errors*(1-p.mitigation/100);const failed=requests*error+requests*(r.outage/(30*24*60))*(1-p.mitigation/100);const allowed=requests*(1-r.target/100);primary=failed;secondary=failed*r.cost;risk=clamp(allowed>0?failed/allowed*100:100,0,10000);score=100-Math.min(100,risk);note='Budget burn '+(allowed?failed/allowed*100:100).toFixed(1)+'%; above 100% means modeled overspend.';break;}
128
+ case 'patents':{const merit=(r.breadth*6+Math.log1p(r.citations)*7+r.families*.7)*(1-r.overlap/150)*(1-p.legal/180);const value=r.potential*p.market/100*(1-r.overlap/250)*(1-p.legal/240);primary=value;secondary=r.renewal;risk=clamp(r.overlap*.65+p.legal*.35,0,100);score=merit;note='Illustrative adjusted commercial value $'+value.toFixed(1)+'k.';break;}
129
+ case 'cloud':{const growth=1+p.growth/100;const effectiveCompute=r.hours*r.rate*growth*(1-r.reserved/100*p.discount/100);const storage=r.storage*.023,egress=r.egress*p.egressRate*growth;primary=effectiveCompute+storage+egress;secondary=r.hours*growth;risk=100-r.util;score=100-r.util;note='Compute $'+effectiveCompute.toFixed(0)+'; storage $'+storage.toFixed(0)+'; egress $'+egress.toFixed(0)+'.';break;}
130
+ case 'transit':{const adequacy=clamp(r.vehicles/(r.trips/5),.55,1.3);const load=(r.boardings*p.demand/100)/(r.trips*r.capacity*adequacy*(1+p.fleet/100));const wait=r.headway/2/(adequacy*(1+p.fleet/100))+r.late+p.traffic;primary=r.boardings*p.demand/100*wait/60;secondary=r.boardings*p.demand/100;risk=clamp(load*100,0,300);score=100-Math.min(100,risk);note='Modeled wait '+wait.toFixed(1)+' min; crowding '+(load*100).toFixed(1)+'%.';break;}
131
+ case 'eval':{const acc=r.correct/10,unsup=r.hallucination/10;const quality=clamp(.6*acc+.4*r.robustness-.30*unsup,0,100);const latencyScore=clamp(100*(1-r.latency/15000),0,100);const costScore=clamp(100*(1-(r.inputCost*.3+r.outputCost*.7)/50),0,100);const scoreVal=clamp(quality*p.quality/100+(latencyScore*.6+costScore*.4)*(1-p.quality/100),0,100);const eligible=r.latency<=p.maxLatency&&r.outputCost<=p.maxCost;primary=eligible?scoreVal:0;secondary=eligible?1:0;risk=unsup;score=scoreVal;note=eligible?'Eligible under configured latency and price ceilings.':'Excluded by a hard latency or output-cost cap.';break;}
132
+ }
133
+ return {r,primary,secondary,risk,score,note};});
134
+ }
135
+ // Patent renewal prioritization: spend only on families affordable under the annual cap.
136
+ if(kind==='patents'){const ranked=[...out].sort((a,b)=>(b.score/(b.r.renewal+1))-(a.score/(a.r.renewal+1)));let budget=p.funding*1000;for(const x of ranked){x.selected=x.secondary<=budget;if(x.selected)budget-=x.secondary;else x.secondary=0;x.note+=(x.selected?' Selected for renewal.':' Excluded under renewal budget.');}}
137
+ if(kind==='bridge'){
138
+ const ranked=[...out].sort((a,b)=>b.score-a.score);let remaining=p.budget;
139
+ for(const x of ranked){x.selected=x.r.rehab<=remaining;if(x.selected)remaining-=x.r.rehab;x.secondary=x.selected?x.r.rehab:0;x.note+=(x.selected?' Scheduled in simulated budget.':' Unfunded in simulated budget.');}
140
+ }
141
+ return out;
142
+ }
143
+ function summary(out){const sum=(key)=>out.reduce((a,x)=>a+x[key],0);let p=sum('primary'),s=sum('secondary'),r=out.length?sum('risk')/out.length:0;
144
+ if(CFG.model==='eval')p=Math.max(0,...out.map(x=>x.primary));
145
+ if(CFG.model==='civicbudget')r=sum('score') /* portfolio value */;
146
+ if(CFG.model==='lineage')s=out.filter(x=>x.secondary>1).length;
147
+ if(CFG.model==='patents')s=out.reduce((a,x)=>a+(x.selected?x.r.renewal:0),0);
148
+ return [p,s,r,out.length];}
149
+ function viewStats(out){const s=summary(out),md=CFG.metrics.concat([['Active records','num']]);return `<div class="metrics">${md.map((d,i)=>`<div class="card metric"><div class="small">${esc(d[0])}</div><strong>${fmt(s[i],d[1])}</strong><div class="foot">${i===3?'Editable synthetic observations':i===2?'Scenario-adjusted diagnostic':'Derived from active records'}</div><i></i></div>`).join('')}</div>`;}
150
+ function bars(out){const records=[...out].sort((a,b)=>b.primary-a.primary).slice(0,7);const max=Math.max(1,...records.map(x=>Math.abs(x.primary)));return records.map(x=>`<div class="barrow"><span class="nm" title="${esc(x.r.name)}">${esc(x.r.name)}</span><div class="track"><div class="fill" style="width:${clamp(Math.abs(x.primary)/max*100,1,100).toFixed(2)}%"></div></div><span class="val">${fmt(x.primary,CFG.metrics[0][1])}</span></div>`).join('')||'<div class="empty">No active records.</div>';}
151
+ function spark(out){const y=out.slice(0,15).map(x=>x.primary),n=y.length;if(n<2)return '';const min=Math.min(...y),max=Math.max(...y),range=max-min||1;const point=y.map((v,i)=>[(i/(n-1)*620+4).toFixed(1),(145-(v-min)/range*112).toFixed(1)]);const poly=point.map(p=>p.join(',')).join(' ');return `<svg class="chartsvg" viewBox="0 0 630 180" role="img" aria-label="Calculated record value trend"><defs><linearGradient id="area" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="var(--accent)" stop-opacity=".31"/><stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/></linearGradient></defs><path d="M ${point[0][0]} 160 L ${point.map(p=>p.join(' ')).join(' L ')} L ${point[n-1][0]} 160 Z" fill="url(#area)"/><polyline points="${poly}" stroke="var(--accent)" stroke-width="3" fill="none" stroke-linejoin="round" stroke-linecap="round"/>${point.map(p=>`<circle cx="${p[0]}" cy="${p[1]}" r="3.5" fill="var(--accent)"/>`).join('')}<line x1="0" y1="160" x2="630" y2="160" stroke="var(--line)" /></svg>`;}
152
+ function presets(){return `<div class="preset-list">${CFG.presets.map((x,i)=>`<button class="preset ${state.preset===i?'selected':''}" data-preset="${i}"><b>${esc(x.name)}</b><small>${esc(x.desc)}</small></button>`).join('')}</div>`;}
153
+ function sliderHTML(){return CFG.controls.map(c=>`<div class="control"><div class="controlhead"><label for="ctrl-${c.key}">${esc(c.label)}</label><output id="out-${c.key}">${fmt(state.controls[c.key])}${esc(c.unit)}</output></div><input aria-label="${esc(c.label)}" id="ctrl-${c.key}" type="range" data-ctrl="${c.key}" min="${c.min}" max="${c.max}" step="${c.step}" value="${state.controls[c.key]}"></div>`).join('');}
154
+ function overview(out){const highest=[...out].sort((a,b)=>b.risk-a.risk)[0];return `${viewStats(out)}<div class="columns"><section class="card panel"><div class="panel-head"><div><h2>Scenario-adjusted results</h2><div class="sub">Ranked by ${esc(CFG.metrics[0][0].toLowerCase())}</div></div><span class="badge">${esc(CFG.presets[state.preset]?.name||'Custom')} case</span></div>${bars(out)}</section><div class="stack"><section class="card panel"><div class="panel-head"><div><h2>Portfolio signal</h2><div class="sub">Calculated directly in your browser</div></div><span class="badge">LIVE MODEL</span></div>${spark(out)}<div class="stat-pair"><span>Records analyzed</span><b>${out.length}</b></div><div class="stat-pair"><span>Highest diagnostic risk</span><b>${out.length?fmt(Math.max(...out.map(x=>x.risk))):'0'}</b></div><div class="stat-pair"><span>Current scenario</span><b>${esc(CFG.presets[state.preset]?.name||'Custom')}</b></div></section><section class="card panel"><h2>Analyst interpretation</h2><p class="sub">${esc(CFG.insight)}</p><div class="insight"><strong>Focus:</strong> ${highest?esc(highest.r.name)+' · '+esc(highest.note):'Add an active record to begin.'}</div><p class="note">Everything here is a what-if simulation with fictitious inputs.</p></section></div></div>`;}
155
+ function lab(out){return `<div class="section-title"><h2>Scenario laboratory</h2><p>Choose a scenario preset or adjust parameters. Results update immediately.</p></div><section class="card panel"><div class="panel-head"><div><h2>Explore different assumptions</h2><div class="sub">Four model-specific scenarios</div></div><span class="badge">INTERACTIVE</span></div>${presets()}</section><div class="lab-cols" style="margin-top:17px"><section class="card panel"><div class="panel-head"><div><h2>Scenario controls</h2><div class="sub">Only illustrative settings, no external APIs</div></div><button class="btn tiny" id="resetCtrl">↻ Reset</button></div>${sliderHTML()}</section><section class="card panel"><div class="panel-head"><div><h2>Results under this scenario</h2><div class="sub">Live recomputation</div></div><span class="badge">${out.length} records</span></div>${bars(out.slice(0,8))}</section></div><section class="card panel" style="margin-top:17px"><h2>Model trace</h2><p class="sub">Top diagnostic examples for the current assumptions</p><ul class="small-list">${[...out].sort((a,b)=>b.risk-a.risk).slice(0,5).map(x=>`<li><b>${esc(x.r.name)}</b><span>${esc(x.note)}</span></li>`).join('')}</ul></section>`;}
156
+ function fieldCell(x,f){const val=x.r[f.key];return `<td>${f.kind==='number'?fmt(num(val)):esc(val)}</td>`;}
157
+ function recordsView(){const all=state.records.filter(r=>Object.values(r).some(v=>typeof v==='string'&&v.toLowerCase().includes(state.search.toLowerCase())));return `<div class="section-title"><h2>Records library</h2><p>Edit assumptions, archive or restore records, and export all work without an account.</p></div><section class="card panel"><div class="toolbar"><input id="search" class="search" type="search" placeholder="Search records..." aria-label="Search records" value="${esc(state.search)}"><div class="actions"><button class="btn" id="importBtn">↑ Import JSON</button><button class="btn" id="resetBtn">↻ Restore demo</button><button class="btn primary" id="addBtn">+ New record</button></div></div><div class="tablewrap"><table><thead><tr><th>STATUS</th>${CFG.fields.slice(0,5).map(f=>`<th>${esc(f.label)}</th>`).join('')}<th>ACTIONS</th></tr></thead><tbody>${all.map(r=>`<tr><td><span class="pill ${r.archived?'bad':''}">${r.archived?'Archived':'Active'}</span></td>${CFG.fields.slice(0,5).map(f=>fieldCell({r},f)).join('')}<td><div class="rowact"><button class="btn tiny" data-edit="${esc(r.id)}">Edit</button><button class="btn tiny ${r.archived?'':'warn'}" data-archive="${esc(r.id)}">${r.archived?'Restore':'Archive'}</button></div></td></tr>`).join('')||'<tr><td colspan="8" class="empty">No matching records.</td></tr>'}</tbody></table></div><p class="note">CSV spreadsheets guard against formula injection. JSON restores validate all fields before replacing records.</p></section>`;}
158
+ function methodology(){return `<div class="section-title"><h2>Methodology & data</h2><p>Transparent assumptions, limits, and privacy.</p></div><div class="methods">${CFG.methodology.map((x,i)=>`<article class="method-card"><div class="number">METHOD ${String(i+1).padStart(2,'0')}</div><p>${esc(x)}</p></article>`).join('')}<article class="method-card"><div class="number">LOCAL-FIRST ARCHITECTURE</div><p>Native HTML, CSS, and JavaScript. Records are stored in this browser, and CSV/JSON exports run offline without an API key.</p></article></div><div class="disclaimer"><strong>Limitations.</strong> All observations, rates, names, metrics and outcomes are synthetic, illustrative, and for training and prototyping. This project is not an operational, scientific, regulatory, medical, legal, or investment decision system. Validate actual decisions against primary data and relevant experts.</div><section class="card panel" style="margin-top:17px"><h2>Data controls</h2><p class="sub">Export a JSON backup before clearing your browser. Import validates numeric ranges and rejects invalid record shapes. Reset restores the demo dataset.</p><div class="actions" style="justify-content:flex-start;margin-top:15px"><button id="methodJSON" class="btn primary">Download backup</button><button id="methodImport" class="btn">Import JSON</button><button id="methodCSV" class="btn">Export CSV</button></div></section>`;}
159
+ function paint(){let all=model(state.records,state.controls);const target=BYID('page');if(state.page==='overview')target.innerHTML=overview(all);else if(state.page==='lab')target.innerHTML=lab(all);else if(state.page==='records')target.innerHTML=recordsView();else target.innerHTML=methodology();document.querySelectorAll('.navbtn').forEach(b=>b.classList.toggle('active',b.dataset.page===state.page));}
160
+ function download(filename,content,type){const blob=new Blob([content],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=filename;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),800);}
161
+ function backup(){download(CFG.slug+'-backup.json',JSON.stringify({project:CFG.number,version:1,records:state.records,controls:state.controls},null,2),'application/json');toast('JSON backup downloaded.');}
162
+ function csvSafe(value){let s=String(value??'');if(/^[\s]*[=+@\-\t\r]/.test(s))s="'"+s;return '"'+s.replaceAll('"','""')+'"';}
163
+ function csv(){const out=model(state.records,state.controls),head=['id','archived',...CFG.fields.map(f=>f.key),'metric_primary','metric_secondary','diagnostic_risk'];const lines=[head.join(','),...out.map(x=>[csvSafe(x.r.id),'false',...CFG.fields.map(f=>f.kind==='text'?csvSafe(x.r[f.key]):x.r[f.key]),x.primary.toFixed(5),x.secondary.toFixed(5),x.risk.toFixed(5)].join(','))];download(CFG.slug+'-results.csv',lines.join('\r\n'),'text/csv;charset=utf-8');toast('Calculated CSV exported.');}
164
+ function openEdit(id){editId=id||null;const r=id?state.records.find(x=>x.id===id):null;BYID('editTitle').textContent=r?'Edit '+r.name:'Create record';BYID('fields').innerHTML=CFG.fields.map(f=>`<div class="field"><label for="f-${esc(f.key)}">${esc(f.label)}</label><input id="f-${esc(f.key)}" name="${esc(f.key)}" type="${f.kind==='text'?'text':'number'}" ${f.kind==='number'?`min="${f.min}" max="${f.max}" step="any"`:'maxlength="140"'} value="${esc(r?r[f.key]:f.kind==='text'?'':Math.max(f.min,Math.min(f.max,1)))}" ${f.key==='deps'?'':'required'}></div>`).join('');BYID('modal').classList.add('show');BYID('fields').querySelector('input')?.focus();}
165
+ function closeEdit(){BYID('modal').classList.remove('show');}
166
+ function editSubmit(e){e.preventDefault();const fd=new FormData(BYID('editForm')),existing=state.records.find(r=>r.id===editId);const raw={id:editId||'R'+Date.now().toString(36)+'_'+Math.random().toString(36).slice(2,5),archived:existing?.archived||false};for(const f of CFG.fields){let v=fd.get(f.key);raw[f.key]=f.kind==='text'?String(v||'').trim():Number(v);}const next=validRecord(raw);if(!next){toast('Invalid record. Review required fields and numeric limits.');return;}const candidate=existing?state.records.map(r=>r.id===editId?next:r):[...state.records,next];if(CFG.model==='lineage'&&!validGraph(candidate)){toast('Invalid dependency graph, unknown asset code or circular reference.');return;}if(candidate.length>150){toast('Maximum 150 records.');return;}state.records=candidate;closeEdit();save();paint();toast(existing?'Record updated.':'New record added.');}
167
+ async function importJSON(file){if(!file)return;if(file.size>1024*1024){toast('JSON is too large (max 1 MB).');return;}try{const text=await file.text(),parsed=JSON.parse(text);if(parsed?.project!==CFG.number){toast('This backup belongs to another project.');return;}const rows=validRecords(parsed.records);if(!rows){toast('Backup rejected: invalid records or graph.');return;}let next=defaultControls();if(parsed.controls&&typeof parsed.controls==='object')for(const c of CFG.controls){const v=parsed.controls[c.key];if(typeof v!=='number'||!Number.isFinite(v)||v<c.min||v>c.max){toast('Backup rejected: invalid scenario values.');return;}next[c.key]=v;}state.records=rows;state.controls=next;state.preset=-1;save();paint();toast('Validated backup successfully imported.');}catch(e){toast('Invalid JSON: original records were preserved.');}}
168
+ BYID('navbox').addEventListener('click',e=>{const b=e.target.closest('[data-page]');if(b){state.page=b.dataset.page;paint();window.scrollTo({top:0,behavior:'instant'});}});
169
+ BYID('page').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.preset!==undefined){const i=+b.dataset.preset;state.preset=i;CFG.controls.forEach((c,j)=>state.controls[c.key]=CFG.presets[i].values[j]);save();paint();}if(b.id==='resetCtrl'){state.controls=defaultControls();state.preset=0;save();paint();}if(b.id==='addBtn')openEdit(null);if(b.dataset.edit)openEdit(b.dataset.edit);if(b.dataset.archive){state.records=state.records.map(x=>x.id===b.dataset.archive?{...x,archived:!x.archived}:x);save();paint();toast('Record status updated.');}if(b.id==='importBtn'||b.id==='methodImport')BYID('importFile').click();if(b.id==='resetBtn'){if(confirm('Restore all original sample records? Export JSON first to preserve your edits.')){state.records=base();state.controls=defaultControls();state.preset=0;state.search='';save();paint();toast('Demo records restored.');}}if(b.id==='methodJSON')backup();if(b.id==='methodCSV')csv();});
170
+ BYID('page').addEventListener('input',e=>{if(e.target.dataset.ctrl){const k=e.target.dataset.ctrl;state.controls[k]=+e.target.value;state.preset=-1;save();paint();}if(e.target.id==='search'){const p=e.target.selectionStart??e.target.value.length;state.search=e.target.value;paint();BYID('search')?.focus();BYID('search')?.setSelectionRange(p,p);}});
171
+ BYID('jsonBtn').addEventListener('click',backup);BYID('csvBtn').addEventListener('click',csv);BYID('themeBtn').addEventListener('click',()=>{state.theme=state.theme==='dark'?'light':'dark';document.body.classList.toggle('light',state.theme==='light');save();});BYID('closeModal').addEventListener('click',closeEdit);BYID('cancelModal').addEventListener('click',closeEdit);BYID('modal').addEventListener('click',e=>{if(e.target===BYID('modal'))closeEdit();});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeEdit();});BYID('editForm').addEventListener('submit',editSubmit);
172
+ BYID('importFile').addEventListener('change',async e=>{const f=e.target.files?.[0];await importJSON(f);e.target.value='';});
173
+ load();paint();
174
+
175
+ </script></body></html>