Upload 3 files
Browse files- LICENSE +21 -0
- README.md +56 -6
- index.html +118 -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,62 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description: Contract risk
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ContractPilot
|
| 3 |
+
emoji: 📑
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
short_description: Contract renewals and risk simulation.
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# ContractPilot
|
| 14 |
+
|
| 15 |
+
### Contract Exposure Intelligence Studio | Hugdrigo Portfolio Project #107
|
| 16 |
+
|
| 17 |
+
**ContractPilot** is a dependency-free, self-contained browser simulator for analyzing synthetic business contract portfolios, upcoming renewal deadlines, annual spend, simple expected incident-loss exposure, and stress-testing price and service-risk assumptions. The application works offline in a Hugging Face Static Space, without tokens, external APIs, a database or paid compute.
|
| 18 |
+
|
| 19 |
+
**Short description:** Contract renewals and risk simulation.
|
| 20 |
+
|
| 21 |
+
> **Important:** All eight sample contracts, counterparties, amounts, probabilities, and losses are **invented**. The simulator is not legal advice, insurance modeling, actuarial forecasting, or a replacement for contract review. Probability figures are user-supplied assumptions and have not been calibrated to real incidents. Date calculations do not interpret contract text and do not issue actual notifications.
|
| 22 |
+
|
| 23 |
+
## Publish on Hugging Face
|
| 24 |
+
|
| 25 |
+
1. Open [Create a new Space](https://huggingface.co/new-space).
|
| 26 |
+
2. Set **Owner** `Hugdrigo`, **Space name** `contractpilot-exposure-intelligence`, **SDK** `Static`, **Template** `Blank`, **Visibility** `Public`, and **License** `MIT`.
|
| 27 |
+
3. Extract the ZIP. Upload exactly `README.md`, `index.html`, and `LICENSE` to the repository root, not inside a subfolder.
|
| 28 |
+
4. Select **Commit changes** and open the **App** tab.
|
| 29 |
+
|
| 30 |
+
**YAML validity:** `colorFrom: blue` and `colorTo: indigo` both use supported Hugging Face color tokens (`red`, `yellow`, `green`, `blue`, `indigo`, `purple`, `pink`, `gray`). The Space SDK is `static` and the entrypoint is `index.html`. No build process required.
|
| 31 |
+
|
| 32 |
+
## Features
|
| 33 |
+
|
| 34 |
+
| Module | Included features |
|
| 35 |
+
|---|---|
|
| 36 |
+
| Command overview | Active annual commitments, expected loss exposure, notice priority count, next-year renewals chart, category share and exposure ranking |
|
| 37 |
+
| Scenario studio | Four presets, 3 sliders for price / incident likelihood / impact severity, baseline comparison, 400 deterministic loss-event simulation draws and P10/P50/P90 |
|
| 38 |
+
| Contract registry | Add/edit fictional contracts, archive/restore records (no destructive record deletion), search, category and status filters, CSV/JSON exports |
|
| 39 |
+
| Renewal calendar | 12-month counts and renewal values, notice-date table, days remaining and overdue tagging |
|
| 40 |
+
| Methodology | Exact rules and assumptions, privacy constraints and explicit limits |
|
| 41 |
+
|
| 42 |
+
## Calculation methodology
|
| 43 |
+
|
| 44 |
+
- **Annual portfolio commitment:** Sum of `value` for active contract records in USD annual equivalents. Not recognized revenue, accruals, or future cash outflows.
|
| 45 |
+
- **Conditional expected annual incident-loss exposure:** `sum(incident_probability_percent / 100 × incident_loss_usd)` over active contracts. Assumes one independent binary event per contract over a hypothetical year. This number is a rule-based expectation, not an insurance estimate.
|
| 46 |
+
- **Scenario annual commitment:** `sum(value × (1 + price_change_percent / 100))`.
|
| 47 |
+
- **Scenario expected loss:** `sum(clamp(probability_percent + probability_change_percentage_points, 0, 100) / 100 × loss_amount × (1 + loss_severity_change_percent / 100))`.
|
| 48 |
+
- **Renewal notice date:** UTC calendar renewal day minus integer `notice_days`. The app does not automatically roll renewal dates forward or interpret agreement clauses. It shows notice deadlines even when they are already overdue.
|
| 49 |
+
- **Simulation:** 400 draws from a fixed deterministic seed. Each contract independently samples an event according to its assumed probability; each triggered loss varies randomly by ±10% of the assumed amount. P10, P50 and P90 describe only these 400 hypothetical drawings and **are not** calibrated forecast intervals.
|
| 50 |
+
|
| 51 |
+
## Input and storage safeguards
|
| 52 |
+
|
| 53 |
+
- Data is stored in browser `localStorage` when available. This storage is **not encrypted**. Do not enter sensitive or confidential contract terms.
|
| 54 |
+
- Imports must be `.json` under 2MB, with `kind=ContractPilotBackup`, `version=1`, integer unique positive IDs, monotonic next ID, valid ISO date, bounded numeric fields, specified categories, and at most 150 contracts. Invalid imports are rejected before state replacement. Current data replacement requires browser confirmation.
|
| 55 |
+
- Names and counterparties are HTML-escaped before rendering. CSV cells are quoted and strings beginning with spreadsheet formula characters are escaped. JSON exports contain plain text and should be secured as needed.
|
| 56 |
+
- Archiving retains a record and excludes it from active calculations. Reset demo requires confirmation and replaces local data. Export a JSON backup first.
|
| 57 |
+
|
| 58 |
+
## Source and dependencies
|
| 59 |
+
|
| 60 |
+
`index.html` contains complete HTML, CSS, JavaScript and inline SVG. No external resources are loaded. Repository includes an MIT `LICENSE`. All sample content and computed analysis are synthetic for portfolio demonstration and educational use.
|
| 61 |
+
|
| 62 |
+
© 2026 Hugdrigo. Project #107.
|
index.html
CHANGED
|
@@ -1,19 +1,119 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="#15293e"><meta name="description" content="ContractPilot: offline contract renewal and exposure planning simulator with synthetic data."><title>ContractPilot | Contract Exposure Intelligence Studio</title>
|
| 3 |
+
<style>
|
| 4 |
+
:root{color-scheme:light;--bg:#f3f6fa;--card:#fff;--card2:#f7f9fc;--text:#172b43;--muted:#73859b;--line:#e1e8ef;--nav:#14293d;--blue:#3369c4;--teal:#0f928d;--orange:#c27c3d;--red:#bf5962;--pale:#eaf1fc;--shade:#ecf7f5;--shadow:0 12px 44px rgba(27,56,84,.045)}
|
| 5 |
+
body.dark{color-scheme:dark;--bg:#101d2b;--card:#182b3b;--card2:#22384a;--text:#e7eff9;--muted:#a4b9cd;--line:#345066;--nav:#0a1825;--blue:#a3beff;--teal:#71d9cf;--orange:#ffca85;--red:#ff9ca5;--pale:#273c55;--shade:#244440;--shadow:none}
|
| 6 |
+
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:13px/1.55 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:var(--text);background:var(--bg)}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}a{color:var(--blue)}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--blue);outline-offset:3px}.shell{display:grid;grid-template-columns:254px minmax(0,1fr);min-height:100vh}.side{position:sticky;top:0;height:100vh;background:var(--nav);padding:26px 18px;display:flex;flex-direction:column;color:#adc4d3}.brand{display:flex;align-items:center;gap:12px;margin:3px 4px 37px}.mark{width:46px;height:46px;display:grid;place-items:center;background:linear-gradient(130deg,#9cedcf,#84b4f8);border-radius:14px;color:#123047;font-size:25px;font-weight:900;box-shadow:0 9px 18px #4fdac425}.brand b{display:block;font-size:21px;line-height:1.1;color:#fff;letter-spacing:-.8px}.brand small{letter-spacing:1.55px;font-size:9px;font-weight:800;color:#93b7ca}.nav-label{margin:0 12px 10px;color:#7296ab;font-size:10px;letter-spacing:1.7px;font-weight:850}.nav{display:grid;gap:5px}.nav button{border:0;background:transparent;color:#b3cad9;width:100%;display:flex;align-items:center;gap:13px;padding:12px 13px;border-radius:11px;text-align:left;font-weight:750;font-size:12px}.nav button:hover,.nav button.active{background:#2b495f;color:#fff}.nav button.active{box-shadow:inset 3px 0 #78e7d1}.nav .sym{font-size:17px;width:22px;text-align:center}.sidefoot{margin-top:auto;border:1px solid #355365;border-radius:14px;padding:15px;background:#1c384c;font-size:11px;color:#b6d0db}.sidefoot strong{display:block;color:#fff;margin-bottom:4px}.dot{display:inline-block;width:8px;height:8px;background:#83e7bd;border-radius:50%;margin-right:7px}
|
| 7 |
+
main{min-width:0;width:100%;max-width:1650px;padding:25px clamp(16px,3.1vw,46px) 34px}.top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:28px}.crumb{font-size:11px;color:var(--muted);font-weight:700}.crumb strong{color:var(--text)}.topact{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border-radius:9px;padding:9px 13px;background:var(--card);border:1px solid var(--line);color:var(--text);font-size:11px;font-weight:800;white-space:nowrap;min-height:35px}.btn:hover{filter:brightness(.96)}.btn.primary{background:#3977ce;color:white;border-color:#3977ce}.btn.ghost{background:var(--pale);border-color:transparent;color:var(--blue)}.btn.danger{color:var(--red)}.hero{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;margin-bottom:25px}.eyebrow{text-transform:uppercase;font-weight:900;letter-spacing:1.65px;color:var(--teal);font-size:10px}.hero h1{margin:3px 0 5px;font-size:clamp(26px,3vw,38px);line-height:1.16;letter-spacing:-1.2px}.hero p{margin:0;color:var(--muted);max-width:670px}.pill{border-radius:30px;background:var(--shade);color:var(--teal);font-weight:800;padding:7px 12px;font-size:10px;white-space:nowrap}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px;margin:0 0 18px}.card{background:var(--card);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);min-width:0}.kpi{padding:19px 18px}.kpihead{display:flex;justify-content:space-between;align-items:center;gap:6px;font-size:11px;font-weight:780;color:var(--muted)}.kpiicon{width:31px;height:31px;background:var(--pale);color:var(--blue);border-radius:9px;display:grid;place-items:center;font-size:15px}.kpiv{font-size:clamp(20px,2.1vw,29px);letter-spacing:-.9px;font-weight:850;margin:12px 0 4px;overflow-wrap:anywhere}.kpif{font-size:10px;color:var(--muted)}.kpi.danger .kpiicon{background:#fff0ee;color:#b65f59}.grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(310px,1fr);gap:16px;margin-bottom:16px}.grid.equal{grid-template-columns:repeat(2,minmax(0,1fr))}.pane{padding:22px}.panehead{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:17px}.panehead h2{font-size:14px;margin:0;letter-spacing:-.25px}.panehead p{margin:3px 0 0;color:var(--muted);font-size:11px}.mini{font-size:10px;color:var(--muted)}.legend{display:flex;gap:13px;flex-wrap:wrap;font-size:10px;color:var(--muted)}.swatch{display:inline-block;width:8px;height:8px;background:var(--blue);border-radius:2px;margin-right:5px}.swatch.teal{background:var(--teal)}.swatch.orange{background:var(--orange)}.chartbox{width:100%;min-height:215px;overflow:hidden}.chartbox svg{display:block;width:100%;height:auto}.rowlist{display:grid;gap:12px}.lineitem{display:flex;gap:12px;align-items:center}.lineitem .name{min-width:100px;font-size:11px;font-weight:750;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.track{height:10px;background:var(--card2);border-radius:10px;flex:1;overflow:hidden}.bar{height:100%;border-radius:9px;background:linear-gradient(90deg,#62a5df,#4b7aca)}.bar.teal{background:linear-gradient(90deg,#60c8ae,#0f948d)}.bar.orange{background:#d9a366}.lineitem .num{min-width:69px;font-size:11px;text-align:right;font-weight:850}.insight{margin-top:16px;background:var(--card2);border:1px solid var(--line);padding:13px;border-radius:11px;color:var(--muted);font-size:11px}.insight strong{color:var(--text)}.badgetag{display:inline-block;border-radius:7px;background:var(--pale);color:var(--blue);padding:4px 8px;font-size:10px;font-weight:850}.badgetag.warn{background:#fbeddc;color:#a77930}.badgetag.risk{background:#fde9e8;color:#a84d5c}.badgetag.good{background:var(--shade);color:var(--teal)}.listrow{display:flex;gap:12px;justify-content:space-between;align-items:center;padding:13px 0;border-bottom:1px solid var(--line)}.listrow:last-child{border:0}.listrow b{font-size:11px}.listrow small{display:block;color:var(--muted);font-size:10px;margin-top:2px}.value{text-align:right;font-weight:840;font-size:12px}.value small{font-weight:500}.section-title{font-size:12px;text-transform:uppercase;letter-spacing:1.1px;color:var(--muted);font-weight:850;margin:22px 0 12px}.tools{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0 0 15px}.field{display:grid;gap:6px;min-width:0}.field label{font-weight:800;font-size:10px;color:var(--muted);letter-spacing:.15px}.field input,.field select{border:1px solid var(--line);background:var(--card);color:var(--text);border-radius:9px;padding:10px 11px;min-width:0;width:100%}.tools .field{min-width:190px;flex:1}.tools .field.shrink{min-width:140px;flex:0 1 175px}.tablewrap{overflow:auto;width:100%;border-radius:13px;border:1px solid var(--line)}table{border-collapse:collapse;width:100%;font-size:11px}th{text-align:left;background:var(--card2);color:var(--muted);font-size:10px;white-space:nowrap;font-weight:850;letter-spacing:.3px}td,th{padding:13px 14px;border-bottom:1px solid var(--line)}td{vertical-align:middle}tbody tr:last-child td{border:0}td b{font-size:11px}td small{color:var(--muted);font-size:10px;display:block}td:last-child,th:last-child{text-align:right}.mono{font-variant-numeric:tabular-nums}.empty{padding:27px;text-align:center;color:var(--muted)}.smallmuted{color:var(--muted);font-size:11px}.hidden{display:none!important}.cols3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.control{padding:15px;border:1px solid var(--line);background:var(--card2);border-radius:12px}.control .clabel{display:flex;justify-content:space-between;font-size:11px;font-weight:800;gap:8px;margin-bottom:12px}.control output{color:var(--blue);font-weight:900}.control input[type=range]{width:100%;accent-color:#3984c6}.presetrow{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:17px}.presetrow .btn.sel{background:var(--nav);color:#fff;border-color:var(--nav)}.methodlist{display:grid;gap:13px}.method{border:1px solid var(--line);background:var(--card2);padding:15px 17px;border-radius:12px}.method b{display:block;font-size:12px;margin-bottom:4px}.method p{margin:0;color:var(--muted);font-size:11px}.method code{display:block;white-space:normal;margin:6px 0;color:var(--teal)}.footnote{color:var(--muted);font-size:10px;margin:16px 0 0}.page{display:none}.page.active{display:block}.alert{border-left:3px solid var(--orange);padding:12px 14px;background:var(--card2);border-radius:0 10px 10px 0;color:var(--muted);font-size:11px;margin:12px 0}.alert strong{color:var(--text)}.notice{font-size:10px;color:var(--muted)}.cal{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:7px;min-width:540px}.calmonth{background:var(--card2);padding:10px 6px;border-radius:9px;text-align:center;min-height:96px}.calmonth b{font-size:10px;color:var(--muted)}.calcount{display:block;font-size:24px;font-weight:900;margin:10px 0 1px}.calmonth small{font-size:9px;color:var(--muted)}.calendarbox{overflow:auto}.modalback{position:fixed;inset:0;background:#0b1a2dbb;display:none;align-items:center;justify-content:center;padding:14px;z-index:30}.modalback.open{display:flex}.modal{background:var(--card);color:var(--text);border-radius:18px;width:min(650px,100%);max-height:92vh;overflow:auto;padding:25px;box-shadow:0 25px 80px #0004}.modal h2{margin:0 0 4px;font-size:21px;letter-spacing:-.5px}.modal p{margin:0 0 18px;color:var(--muted);font-size:11px}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.modfoot{display:flex;justify-content:flex-end;gap:8px;margin-top:19px;flex-wrap:wrap}.mobilebar{display:none}.toast{position:fixed;right:24px;bottom:22px;background:#183247;color:#fff;padding:12px 18px;border-radius:11px;font-size:12px;box-shadow:0 8px 32px #0002;z-index:40;max-width:min(410px,90vw)}
|
| 8 |
+
@media(max-width:1120px){.shell{grid-template-columns:214px minmax(0,1fr)}.side{padding:25px 11px}.brand b{font-size:18px}.cards{grid-template-columns:repeat(2,minmax(0,1fr))}.grid{grid-template-columns:minmax(0,1fr)}.cols3{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:730px){.chartbox{min-height:145px}.shell{display:block}.side{height:auto;position:relative;min-height:0;padding:12px 15px}.brand{margin:0}.side .nav-label,.side .nav,.sidefoot{display:none}.mobilebar{display:flex;position:sticky;top:0;z-index:10;background:var(--nav);padding:7px 9px;gap:5px;overflow:auto;box-shadow:0 4px 15px #0919291e}.mobilebar button{border:0;background:transparent;color:#c6d7e2;border-radius:9px;padding:9px 12px;font-weight:750;font-size:11px;white-space:nowrap}.mobilebar button.active{background:#36536a;color:#fff}main{padding:18px 13px 26px}.top{margin-bottom:17px;align-items:flex-start}.topact{justify-content:flex-end}.topact .btn{padding:8px 9px}.hero{display:block}.hero .pill{display:inline-block;margin-top:10px}.pane{padding:16px}.cards{gap:9px}.kpi{padding:15px 13px}.kpiv{font-size:23px}.cols3,.grid.equal{grid-template-columns:minmax(0,1fr)}.formgrid{grid-template-columns:1fr}.lineitem .name{min-width:75px}.tools .field{min-width:135px}.topact .hide-small{display:none}}@media(max-width:365px){.top .crumb{font-size:10px}.cards{grid-template-columns:repeat(2,minmax(0,1fr))}.kpiv{font-size:20px}.brand b{font-size:17px}.kpiicon{width:25px;height:25px}.btn{font-size:10px}}
|
| 9 |
+
</style></head><body>
|
| 10 |
+
<div class="shell"><aside class="side"><div class="brand"><span class="mark">⌘</span><div><b>ContractPilot</b><small>HUGDRIGO / #107</small></div></div><div class="nav-label">WORKSPACE</div><nav id="desktopNav" class="nav" aria-label="Main navigation">
|
| 11 |
+
<button data-page="overview" class="active"><span class="sym">▦</span>Command overview</button><button data-page="scenarios"><span class="sym">◫</span>Scenario studio</button><button data-page="registry"><span class="sym">▤</span>Contract registry</button><button data-page="renewals"><span class="sym">◷</span>Renewal calendar</button><button data-page="method"><span class="sym">◇</span>Methodology</button></nav><div class="sidefoot"><strong><span class="dot"></span>Local simulation</strong>Sample contracts only<br>Offline calculations · No external API<br>Data stored in your browser</div></aside>
|
| 12 |
+
<div class="mobilebar" id="mobileNav"><button data-page="overview" class="active">Overview</button><button data-page="scenarios">Scenarios</button><button data-page="registry">Registry</button><button data-page="renewals">Renewals</button><button data-page="method">Methods</button></div>
|
| 13 |
+
<main><header class="top"><span class="crumb">HUGDRIGO <span style="opacity:.55">/ BUSINESS INTELLIGENCE /</span> <strong id="crumb">OVERVIEW</strong></span><div class="topact"><button id="themeBtn" class="btn" aria-label="Toggle dark mode">◐ Theme</button><button id="csvBtn" class="btn hide-small">↓ Export CSV</button><button id="jsonBtn" class="btn primary">↓ Backup</button></div></header>
|
| 14 |
+
<section id="overview" class="page active"><div class="hero"><div><div class="eyebrow">CONTRACT EXPOSURE INTELLIGENCE</div><h1>Control obligations.<br>See exposure clearly.</h1><p>Model annual commitments, renewal deadlines, and conditional loss exposure with a transparent, local-first simulator.</p></div><span class="pill">◆ SYNTHETIC DATASET</span></div><div id="kpis" class="cards"></div><div class="grid"><div class="card pane"><div class="panehead"><div><h2>Renewal exposure · next 12 months</h2><p>Upcoming annualized amounts by renewal month</p></div><span class="badgetag">USD</span></div><div class="legend"><span><i class="swatch"></i>Annual contract amount</span><span><i class="swatch teal"></i>Expected loss exposure</span></div><div id="renewalChart" class="chartbox"></div></div><div class="card pane"><div class="panehead"><div><h2>Exposure by category</h2><p>Annual commitment across active contracts</p></div><span class="badgetag">PORTFOLIO</span></div><div id="categoryBars" class="rowlist"></div><div id="categoryInsight" class="insight"></div></div></div><div class="grid equal"><div class="card pane"><div class="panehead"><div><h2>Priority notice deadlines</h2><p>Contractual opt-out or renegotiation notices</p></div><button data-go="renewals" class="btn">Calendar →</button></div><div id="deadlineList"></div></div><div class="card pane"><div class="panehead"><div><h2>Largest conditional losses</h2><p>Simple probability × impact exposure ranking</p></div><button data-go="registry" class="btn">Review →</button></div><div id="riskList"></div></div></div></section>
|
| 15 |
+
<section id="scenarios" class="page"><div class="hero"><div><div class="eyebrow">SCENARIO PLANNING LAB</div><h1>Stress-test the portfolio.</h1><p>Change assumptions, see the difference versus baseline, and explore bounded simulated loss outcomes.</p></div><span class="pill">4 SCENARIOS</span></div><div id="presets" class="presetrow"></div><div class="cols3" id="sliders"></div><div id="scenarioKpis" class="cards" style="margin-top:18px"></div><div class="grid"><div class="card pane"><div class="panehead"><div><h2>Estimated annual exposure · comparison</h2><p>Scenario assumptions, not contract forecasts</p></div><span class="badgetag">BASE VS SCENARIO</span></div><div id="scenarioChart" class="chartbox"></div></div><div class="card pane"><div class="panehead"><div><h2>Simulated incident-loss distribution</h2><p>400 seeded independent draws · illustrative P10 / P50 / P90</p></div><span class="badgetag">MONTE CARLO</span></div><div id="simResults"></div><div class="alert"><strong>Interpretation:</strong> A percentage here describes only the selected hypothetical model, not a calibrated real-world risk.</div></div></div></section>
|
| 16 |
+
<section id="registry" class="page"><div class="hero"><div><div class="eyebrow">CONTRACT REGISTER</div><h1>One registry. Clear actions.</h1><p>Edit fictional agreements, preserve archived records, and export your working assumptions as portable files.</p></div><button id="addBtn" class="btn primary">+ New contract</button></div><div class="card pane"><div class="tools"><div class="field"><label for="search">Search contracts or counterparties</label><input id="search" type="search" placeholder="Search portfolio…" maxlength="100"></div><div class="field shrink"><label for="catFilter">Category</label><select id="catFilter"><option value="all">All categories</option><option>Software</option><option>Logistics</option><option>Facilities</option><option>Professional services</option></select></div><div class="field shrink"><label for="statusFilter">Status</label><select id="statusFilter"><option value="active">Active</option><option value="archived">Archived</option><option value="all">All records</option></select></div></div><div class="tablewrap"><table><thead><tr><th>Contract / counterparty</th><th>Category</th><th>Annual USD</th><th>Renewal</th><th>Notice window</th><th>Loss exposure</th><th>Actions</th></tr></thead><tbody id="contractRows"></tbody></table></div><p class="footnote" id="registryFoot"></p></div><div class="alert">Imported and edited entries are stored locally on this device when browser storage is available. Do not enter confidential contracts or personally identifiable data.</div></section>
|
| 17 |
+
<section id="renewals" class="page"><div class="hero"><div><div class="eyebrow">RENEWAL CONTROL CENTER</div><h1>Prepare before deadlines.</h1><p>See the notice deadlines and renewal concentration of active contracts. The dashboard calculates dates; it does not issue reminders.</p></div><span class="pill">12-MONTH VIEW</span></div><div class="card pane" style="margin-bottom:16px"><div class="panehead"><div><h2>Contract renewal calendar</h2><p>Count and value of agreements renewing within each coming month</p></div></div><div id="monthCalendar" class="calendarbox"></div></div><div class="card pane"><div class="panehead"><div><h2>Notice deadline tracker</h2><p>Notice deadline = renewal date − contract notice days</p></div><span class="badgetag">DATE-BASED</span></div><div class="tablewrap"><table><thead><tr><th>Agreement</th><th>Notice deadline</th><th>Days remaining</th><th>Renewal</th><th>Annual USD</th><th>Priority</th></tr></thead><tbody id="noticeRows"></tbody></table></div><p class="footnote">Dates are computed using UTC calendar days to avoid local daylight-saving shifts. Always confirm real contractual notice terms independently.</p></div></section>
|
| 18 |
+
<section id="method" class="page"><div class="hero"><div><div class="eyebrow">ASSUMPTIONS & LIMITS</div><h1>Transparent by design.</h1><p>Every display and simulated result follows explicit formulas. Nothing uses external data or constitutes legal advice.</p></div><span class="pill">RULE-BASED DEMO</span></div><div class="grid equal"><div class="card pane"><div class="panehead"><h2>Calculation rules</h2></div><div class="methodlist"><div class="method"><b>Annual commitment</b><code>Σ annual_value over active contracts</code><p>Contract values are simplified annual USD equivalents, not accrued expenses or a cash flow forecast.</p></div><div class="method"><b>Expected incident-loss exposure</b><code>Σ (annual_incident_probability × conditional_loss_amount)</code><p>Assumes one independent binary loss event per agreement during the modeled year. No correlations.</p></div><div class="method"><b>Scenario adjustment</b><code>annual_value' = annual_value × (1 + price_delta / 100)<br>p' = clamp(p + probability_delta, 0, 100)<br>loss' = loss_amount × (1 + severity_delta / 100)</code><p>Scenarios modify all active records uniformly and do not forecast contractual terms.</p></div><div class="method"><b>Renewal notice</b><code>notice_deadline = renewal_date − notice_days</code><p>Only the first supplied renewal date is shown. Automatic renewal, extension and termination clauses are not modeled.</p></div></div></div><div class="card pane"><div class="panehead"><h2>Responsible interpretation</h2></div><div class="methodlist"><div class="method"><b>Synthetic starting data</b><p>Eight entirely invented contract records and counterparties. No real pricing, legal terms, renewal schedules or incidents.</p></div><div class="method"><b>Simulation</b><p>400 seeded draws sample a Bernoulli loss event for each active contract, with illustrative severity variation of ±10%. P10/P50/P90 summarize those draws, not a statistical confidence interval.</p></div><div class="method"><b>Privacy and security</b><p>No server or remote API; browser localStorage is not encrypted. Backups contain plain JSON. Avoid confidential contractual terms. Imported names are escaped before HTML display and CSV is protected against formula execution.</p></div><div class="method"><b>Not legal or financial advice</b><p>This is a self-contained educational decision-support demonstrator. Validate notice obligations, applicable law, pricing, risk probabilities, contract rights and interdependencies separately before taking action.</p></div></div><div class="tools" style="margin-top:15px"><button id="importBtn" class="btn">↑ Restore JSON</button><button id="resetBtn" class="btn danger">↺ Reset demo data</button></div></div></div></section>
|
| 19 |
+
<footer class="footnote">HUGDRIGO PORTFOLIO · PROJECT #107 · LOCAL-FIRST · SYNTHETIC EXAMPLES · © 2026 · FOR EDUCATIONAL SIMULATION ONLY</footer></main></div>
|
| 20 |
+
<div class="modalback" id="modalWrap" role="presentation"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalHeading"><h2 id="modalHeading">New contract</h2><p>Fictional planning information only. Fields are validated before saving.</p><form id="contractForm"><div class="formgrid"><div class="field"><label for="fName">Contract name</label><input id="fName" required maxlength="72" autocomplete="off"></div><div class="field"><label for="fParty">Counterparty</label><input id="fParty" required maxlength="72" autocomplete="off"></div><div class="field"><label for="fCategory">Category</label><select id="fCategory"><option>Software</option><option>Logistics</option><option>Facilities</option><option>Professional services</option></select></div><div class="field"><label for="fValue">Annual contract value (USD)</label><input id="fValue" type="number" required min="0" max="100000000" step="0.01"></div><div class="field"><label for="fRenew">Next renewal date</label><input id="fRenew" type="date" required min="2020-01-01" max="2100-12-31"></div><div class="field"><label for="fNotice">Notice days before renewal</label><input id="fNotice" type="number" required min="0" max="365" step="1"></div><div class="field"><label for="fRisk">Annual incident probability (%)</label><input id="fRisk" type="number" required min="0" max="100" step="0.1"></div><div class="field"><label for="fLoss">Loss amount if incident occurs (USD)</label><input id="fLoss" type="number" required min="0" max="100000000" step="0.01"></div></div><div class="modfoot"><button id="cancelModal" type="button" class="btn">Cancel</button><button type="submit" class="btn primary">Save contract</button></div></form></div></div><input id="fileInput" type="file" accept=".json,application/json" class="hidden" aria-label="Select JSON backup">
|
| 21 |
+
<script>
|
| 22 |
+
'use strict';
|
| 23 |
+
(() => {
|
| 24 |
+
const CATEGORIES=['Software','Logistics','Facilities','Professional services'];
|
| 25 |
+
const DEFAULTS=[
|
| 26 |
+
{id:1,name:'Nimbus Cloud Suite',party:'Cumulus Systems (demo)',category:'Software',value:148000,renew:'2026-11-18',notice:30,risk:11,loss:92000,archived:false},
|
| 27 |
+
{id:2,name:'Regional Freight Network',party:'BlueRoute Freight (demo)',category:'Logistics',value:265000,renew:'2027-01-12',notice:60,risk:22,loss:154000,archived:false},
|
| 28 |
+
{id:3,name:'Facilities Service Plan',party:'Cedar Property Group (demo)',category:'Facilities',value:98000,renew:'2027-02-28',notice:45,risk:8,loss:54000,archived:false},
|
| 29 |
+
{id:4,name:'Business Systems Advisory',party:'Ridge Partners (demo)',category:'Professional services',value:74000,renew:'2026-12-15',notice:45,risk:13,loss:32000,archived:false},
|
| 30 |
+
{id:5,name:'Data Operations License',party:'Mesa Analytics (demo)',category:'Software',value:113000,renew:'2027-05-10',notice:90,risk:17,loss:135000,archived:false},
|
| 31 |
+
{id:6,name:'Cross-Dock Distribution',party:'NorthBridge Cargo (demo)',category:'Logistics',value:172000,renew:'2027-06-25',notice:60,risk:24,loss:89000,archived:false},
|
| 32 |
+
{id:7,name:'Workplace Energy Service',party:'Solstice Utilities (demo)',category:'Facilities',value:79000,renew:'2027-08-30',notice:120,risk:9,loss:62000,archived:false},
|
| 33 |
+
{id:8,name:'Compliance Operations',party:'Axial Consulting (demo)',category:'Professional services',value:59000,renew:'2027-09-14',notice:60,risk:14,loss:76000,archived:false}
|
| 34 |
+
];
|
| 35 |
+
const PRESETS=[{name:'Baseline',price:0,prob:0,severity:0},{name:'Price shock',price:16,prob:0,severity:0},{name:'Supplier disruption',price:5,prob:18,severity:25},{name:'Renegotiation',price:-10,prob:-6,severity:-10}];
|
| 36 |
+
const STORE='hugdrigo_contractpilot_107_v1', THEME='hugdrigo_contractpilot_theme';
|
| 37 |
+
const clone=o=>JSON.parse(JSON.stringify(o));
|
| 38 |
+
const byId=id=>document.getElementById(id);
|
| 39 |
+
const cap=(x,lo,hi)=>Math.min(hi,Math.max(lo,x));
|
| 40 |
+
const esc=v=>String(v).replace(/[&<>"']/g,x=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[x]));
|
| 41 |
+
const fmt=(n,currency=true)=>currency?'$'+Number(n).toLocaleString('en-US',{maximumFractionDigits:0}):Number(n).toLocaleString('en-US',{maximumFractionDigits:1});
|
| 42 |
+
const compact=n=>Math.abs(n)>=1000000?(n/1000000).toFixed(2)+'M':Math.abs(n)>=1000?(n/1000).toFixed(1)+'K':Math.round(n).toString();
|
| 43 |
+
const iso=/^\d{4}-\d{2}-\d{2}$/;
|
| 44 |
+
const dayMs=86400000;
|
| 45 |
+
const dateNumber=s=>{const t=Date.parse(s+'T00:00:00Z');return Number.isFinite(t)?t:NaN};
|
| 46 |
+
const utcDay=()=>{const d=new Date();return Date.UTC(d.getFullYear(),d.getMonth(),d.getDate())};
|
| 47 |
+
const shiftDate=(s,days)=>{const d=new Date(dateNumber(s)-days*dayMs);return d.toISOString().slice(0,10)};
|
| 48 |
+
const dateLabel=s=>new Date(dateNumber(s)).toLocaleDateString('en-US',{timeZone:'UTC',month:'short',day:'numeric',year:'numeric'});
|
| 49 |
+
const active=()=>state.items.filter(i=>!i.archived);
|
| 50 |
+
function validContract(r){if(!r||typeof r!=='object'||Array.isArray(r))return false;
|
| 51 |
+
if(!Number.isSafeInteger(r.id)||r.id<1||r.id>1000000000)return false;
|
| 52 |
+
if(typeof r.name!=='string'||!r.name.trim()||r.name.length>72||typeof r.party!=='string'||!r.party.trim()||r.party.length>72)return false;
|
| 53 |
+
if(!CATEGORIES.includes(r.category)||typeof r.renew!=='string'||!iso.test(r.renew)||!Number.isFinite(dateNumber(r.renew))||new Date(dateNumber(r.renew)).toISOString().slice(0,10)!==r.renew||r.renew<'2020-01-01'||r.renew>'2100-12-31')return false;
|
| 54 |
+
if(typeof r.archived!=='boolean')return false;
|
| 55 |
+
return ['value','notice','risk','loss'].every(k=>typeof r[k]==='number'&&Number.isFinite(r[k]))&&r.value>=0&&r.value<=100000000&&Number.isInteger(r.notice)&&r.notice>=0&&r.notice<=365&&r.risk>=0&&r.risk<=100&&r.loss>=0&&r.loss<=100000000;
|
| 56 |
+
}
|
| 57 |
+
function validBackup(obj){if(!obj||obj.kind!=='ContractPilotBackup'||obj.version!==1||!Array.isArray(obj.items)||obj.items.length>150||typeof obj.nextId!=='number'||!Number.isSafeInteger(obj.nextId)||obj.nextId<1||obj.nextId>1000000001||!obj.items.every(validContract))return false;
|
| 58 |
+
const ids=obj.items.map(r=>r.id);return new Set(ids).size===ids.length && obj.nextId>Math.max(0,...ids);
|
| 59 |
+
}
|
| 60 |
+
let state={items:clone(DEFAULTS),nextId:9};let page='overview';let scenario={price:0,prob:0,severity:0},preset='Baseline',editing=null,toastId=0;
|
| 61 |
+
function load(){try{const raw=localStorage.getItem(STORE);if(raw){const data=JSON.parse(raw);if(validBackup(data))state=data;}}catch(e){}try{if(localStorage.getItem(THEME)==='dark')document.body.classList.add('dark')}catch(e){}}
|
| 62 |
+
function save(){try{localStorage.setItem(STORE,JSON.stringify({kind:'ContractPilotBackup',version:1,...state}));}catch(e){notify('Local storage unavailable; use JSON backup.')}}
|
| 63 |
+
function notify(m){const old=document.querySelector('.toast');if(old)old.remove();const p=document.createElement('div');p.className='toast';p.setAttribute('role','status');p.textContent=m;document.body.appendChild(p);const id=++toastId;setTimeout(()=>{if(id===toastId)p.remove()},3500)}
|
| 64 |
+
function project(sc=scenario,items=active()) {let committed=0,expected=0,baseExpected=0;for(const x of items){if(x.archived)continue;const v=x.value*(1+sc.price/100);const p=cap(x.risk+sc.prob,0,100)/100;const l=x.loss*(1+sc.severity/100);committed+=v;expected+=p*l;baseExpected+=x.loss*x.risk/100}return{committed,expected,baseExpected,total:committed+expected}}
|
| 65 |
+
function kpi(title,value,detail,icon,cls=''){return `<div class="card kpi ${cls}"><div class="kpihead"><span>${esc(title)}</span><span class="kpiicon">${esc(icon)}</span></div><div class="kpiv mono">${esc(value)}</div><div class="kpif">${esc(detail)}</div></div>`}
|
| 66 |
+
function riskFor(x,sc={price:0,prob:0,severity:0}){return cap(x.risk+sc.prob,0,100)/100*x.loss*(1+sc.severity/100)}
|
| 67 |
+
function dates(x){const deadline=shiftDate(x.renew,x.notice);return {deadline,days:Math.round((dateNumber(deadline)-utcDay())/dayMs),renewDays:Math.round((dateNumber(x.renew)-utcDay())/dayMs)}}
|
| 68 |
+
function deadlineTag(d){return d<0?'Overdue':d<=30?'Due soon':'Scheduled'}
|
| 69 |
+
function deadlineClass(d){return d<0?'risk':d<=30?'warn':'good'}
|
| 70 |
+
function monthBuckets(items){const now=new Date(utcDay()), first=Date.UTC(now.getUTCFullYear(),now.getUTCMonth(),1);return Array.from({length:12},(_,i)=>{const t=new Date(Date.UTC(now.getUTCFullYear(),now.getUTCMonth()+i,1));const end=Date.UTC(t.getUTCFullYear(),t.getUTCMonth()+1,1);const list=items.filter(x=>dateNumber(x.renew)>=Math.max(first,t.getTime())&&dateNumber(x.renew)<end);return{label:t.toLocaleDateString('en-US',{month:'short',timeZone:'UTC'}),ym:t.toISOString().slice(0,7),count:list.length,amount:list.reduce((s,x)=>s+x.value,0),risk:list.reduce((s,x)=>s+riskFor(x),0)}})}
|
| 71 |
+
function svgBars(data,mode='renewals'){
|
| 72 |
+
const mx=Math.max(1,...data.map(d=>d.amount)),w=750,h=258,left=30,bottom=217,step=(w-50)/Math.max(1,data.length),barW=Math.min(40,step*.62);
|
| 73 |
+
let content=`<svg viewBox="0 0 ${w} ${h}" role="img" aria-label="${mode==='renewals'?'Monthly contract renewal amounts and associated expected loss':'Baseline and scenario cost comparison'}"><g stroke="var(--line)" stroke-width="1">`;
|
| 74 |
+
for(let j=0;j<4;j++){const y=31+j*56;content+=`<line x1="32" y1="${y}" x2="${w-13}" y2="${y}" stroke-dasharray="3,5"/>`;}content+='</g>';
|
| 75 |
+
for(let i=0;i<data.length;i++){
|
| 76 |
+
const d=data[i],x=left+step*i+step/2-barW/2,ht=d.amount/mx*162,y=bottom-ht,split=mode==='renewals';
|
| 77 |
+
const aw=split?barW*.61:barW;
|
| 78 |
+
content+=`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${aw.toFixed(1)}" height="${Math.max(0,ht).toFixed(1)}" rx="4" fill="#4b88cb"/>`;
|
| 79 |
+
if(split){const rh=(d.risk||0)/mx*162;content+=`<rect x="${(x+aw+2).toFixed(1)}" y="${(bottom-rh).toFixed(1)}" width="${(barW-aw-2).toFixed(1)}" height="${Math.max(0,rh).toFixed(1)}" rx="3" fill="#15a49c"/>`;}
|
| 80 |
+
content+=`<text x="${(x+barW/2).toFixed(1)}" y="237" font-size="11" text-anchor="middle" fill="var(--muted)">${esc(d.label)}</text>`;
|
| 81 |
+
if(ht>30)content+=`<text x="${(x+barW/2).toFixed(1)}" y="${Math.max(14,y-7).toFixed(1)}" font-size="10" text-anchor="middle" fill="var(--text)" font-weight="700">${compact(d.amount)}</text>`;
|
| 82 |
+
}
|
| 83 |
+
return content+'</svg>'}
|
| 84 |
+
function overview(){const items=active(),val=project({price:0,prob:0,severity:0},items),urgent=items.map(x=>({...x,...dates(x)})).filter(x=>x.renewDays>=0&&x.days<=30).length;
|
| 85 |
+
byId('kpis').innerHTML=kpi('Annual commitments',fmt(val.committed),'Sum of active annual equivalents','▤')+kpi('Expected loss exposure',fmt(val.expected),'Probability × impact model','⚑','danger')+kpi('Active contracts',String(items.length),`${state.items.filter(x=>x.archived).length} archived · preserved`,'▦')+kpi('Notice priorities',String(urgent),'Overdue or within 30 days','◷');
|
| 86 |
+
const months=monthBuckets(items);byId('renewalChart').innerHTML=svgBars(months);
|
| 87 |
+
const amounts=CATEGORIES.map(c=>({name:c,amount:items.filter(x=>x.category===c).reduce((s,x)=>s+x.value,0)})).sort((a,b)=>b.amount-a.amount);const mx=Math.max(1,...amounts.map(a=>a.amount));byId('categoryBars').innerHTML=amounts.map((a,i)=>`<div class="lineitem"><span class="name">${esc(a.name)}</span><div class="track"><div class="bar ${i===0?'teal':''}" style="width:${(100*a.amount/mx).toFixed(2)}%"></div></div><span class="num mono">${fmt(a.amount)}</span></div>`).join('');byId('categoryInsight').innerHTML=amounts[0]&&amounts[0].amount?`<strong>Highest commitment:</strong> ${esc(amounts[0].name)} represents ${(amounts[0].amount/Math.max(1,val.committed)*100).toFixed(1)}% of modeled annual spend.`:'No active contract values to compare.';
|
| 88 |
+
const priorities=items.map(x=>({...x,...dates(x)})).filter(x=>x.renewDays>=0).sort((a,b)=>a.days-b.days).slice(0,4);byId('deadlineList').innerHTML=priorities.length?priorities.map(x=>`<div class="listrow"><div><b>${esc(x.name)}</b><small>Notice ${dateLabel(x.deadline)}</small></div><div class="value"><span class="badgetag ${deadlineClass(x.days)}">${deadlineTag(x.days)}</span><small>${x.days<0?Math.abs(x.days)+' days late':x.days+' days left'}</small></div></div>`).join(''):'<div class="empty">No upcoming renewals.</div>';
|
| 89 |
+
const risks=[...items].sort((a,b)=>riskFor(b)-riskFor(a)).slice(0,4);byId('riskList').innerHTML=risks.length?risks.map(x=>`<div class="listrow"><div><b>${esc(x.name)}</b><small>${esc(x.category)} · ${x.risk}% assumed incident probability</small></div><div class="value mono">${fmt(riskFor(x))}<small>conditional expected loss</small></div></div>`).join(''):'<div class="empty">No active contracts.</div>';
|
| 90 |
+
}
|
| 91 |
+
function seed(n){return ()=>{n=(Math.imul(1664525,n)+1013904223)>>>0;return n/4294967296}}
|
| 92 |
+
function simulate(sc=scenario,items=active(),iterations=400){const rnd=seed(1072026);let arr=[];for(let i=0;i<iterations;i++){let loss=0;for(const x of items){const p=cap(x.risk+sc.prob,0,100)/100;if(rnd()<p)loss+=x.loss*(1+sc.severity/100)*(0.9+0.2*rnd());}arr.push(loss)}arr.sort((a,b)=>a-b);return {p10:arr[Math.floor((iterations-1)*.10)],p50:arr[Math.floor((iterations-1)*.5)],p90:arr[Math.floor((iterations-1)*.90)],min:arr[0],max:arr[arr.length-1]}}
|
| 93 |
+
function renderScenario(refreshControls=true){if(refreshControls){byId('presets').innerHTML=PRESETS.map(x=>`<button class="btn ${preset===x.name?'sel':''}" data-preset="${esc(x.name)}">${esc(x.name)}</button>`).join('');const controls=[['price','Contract price adjustment','%',-25,50],['prob','Incident probability change',' pp',-20,35],['severity','Conditional loss severity','%',-40,60]];byId('sliders').innerHTML=controls.map(([key,label,unit,min,max])=>`<div class="control"><div class="clabel"><label for="slide_${key}">${esc(label)}</label><output id="out_${key}">${scenario[key]>0?'+':''}${scenario[key]}${unit}</output></div><input id="slide_${key}" type="range" min="${min}" max="${max}" step="1" value="${scenario[key]}" data-slide="${key}" aria-label="${esc(label)}"></div>`).join('');}
|
| 94 |
+
const a=project({price:0,prob:0,severity:0}),b=project();byId('scenarioKpis').innerHTML=kpi('Modeled annual spend',fmt(b.committed),`Change: ${fmt(b.committed-a.committed)}`,'▤')+kpi('Modeled loss exposure',fmt(b.expected),`Change: ${fmt(b.expected-a.expected)}`,'⚑')+kpi('Total economic exposure',fmt(b.total),'Annual spend + expected loss','▦')+kpi('Spend change',`${a.committed?((b.committed/a.committed-1)*100).toFixed(1):'0.0'}%`,'Relative to baseline','↗');
|
| 95 |
+
byId('scenarioChart').innerHTML=svgBars([{label:'Baseline',amount:a.total},{label:'Scenario',amount:b.total}],'scenario')+'<p class="smallmuted">Comparison combines modeled annual spend and expected incident loss. These are different financial concepts and are shown together only for sensitivity analysis.</p>';
|
| 96 |
+
const sim=simulate();byId('simResults').innerHTML=`<div class="rowlist">${[['P10',sim.p10],['Median (P50)',sim.p50],['P90',sim.p90]].map(([name,v])=>`<div class="listrow"><div><b>${esc(name)}</b><small>Simulated incident-loss total</small></div><span class="value mono">${fmt(v)}</span></div>`).join('')}</div><p class="footnote">Fixed seed · 400 draws · 8 or fewer independent events in default data · No calibrated probabilities</p>`;
|
| 97 |
+
}
|
| 98 |
+
function contractRows(){const query=byId('search').value.trim().toLowerCase(),cat=byId('catFilter').value,status=byId('statusFilter').value;let items=state.items.filter(x=>(!query||(x.name+' '+x.party).toLowerCase().includes(query))&&(cat==='all'||x.category===cat)&&(status==='all'||(status==='active'?!x.archived:x.archived)));items.sort((a,b)=>a.renew.localeCompare(b.renew));byId('contractRows').innerHTML=items.map(x=>{const d=dates(x);return `<tr><td><b>${esc(x.name)}</b><small>${esc(x.party)} ${x.archived?'· archived':''}</small></td><td>${esc(x.category)}</td><td class="mono">${fmt(x.value)}</td><td class="mono">${dateLabel(x.renew)}</td><td><span class="badgetag ${deadlineClass(d.days)}">${x.notice} days</span></td><td class="mono">${fmt(riskFor(x))}</td><td><div style="display:flex;justify-content:flex-end;gap:6px"><button class="btn" data-edit="${x.id}">Edit</button><button class="btn ${x.archived?'ghost':'danger'}" data-archive="${x.id}">${x.archived?'Restore':'Archive'}</button></div></td></tr>`}).join('')||'<tr><td colspan="7" class="empty">No matching contracts.</td></tr>';byId('registryFoot').textContent=`Showing ${items.length} of ${state.items.length} records. Archived records stay in your backup.`}
|
| 99 |
+
function renewals(){const months=monthBuckets(active());byId('monthCalendar').innerHTML=`<div class="cal">${months.map(x=>`<div class="calmonth"><b>${esc(x.label)} ${esc(x.ym.slice(2,4))}</b><span class="calcount">${x.count}</span><small>${fmt(x.amount)}</small></div>`).join('')}</div>`;const sorted=active().map(x=>({...x,...dates(x)})).sort((a,b)=>a.deadline.localeCompare(b.deadline));byId('noticeRows').innerHTML=sorted.map(x=>`<tr><td><b>${esc(x.name)}</b><small>${esc(x.party)}</small></td><td class="mono">${dateLabel(x.deadline)}</td><td>${x.days<0?'−'+Math.abs(x.days):x.days} days</td><td class="mono">${dateLabel(x.renew)}</td><td class="mono">${fmt(x.value)}</td><td><span class="badgetag ${deadlineClass(x.days)}">${deadlineTag(x.days)}</span></td></tr>`).join('')||'<tr><td class="empty" colspan="6">No active contracts.</td></tr>'}
|
| 100 |
+
function update(){overview();renderScenario();contractRows();renewals()}
|
| 101 |
+
function setPage(p){if(!['overview','scenarios','registry','renewals','method'].includes(p))return;page=p;document.querySelectorAll('.page').forEach(el=>el.classList.toggle('active',el.id===p));document.querySelectorAll('[data-page]').forEach(el=>el.classList.toggle('active',el.dataset.page===p));byId('crumb').textContent=({overview:'OVERVIEW',scenarios:'SCENARIO STUDIO',registry:'CONTRACTS',renewals:'RENEWALS',method:'METHODOLOGY'})[p];if(p==='scenarios')renderScenario();if(p==='registry')contractRows();if(p==='renewals')renewals();window.scrollTo(0,0)}
|
| 102 |
+
function openModal(id=null){editing=id;const x=state.items.find(i=>i.id===id);byId('modalHeading').textContent=x?'Edit contract':'Add contract';const fields={fName:x?.name||'',fParty:x?.party||'',fCategory:x?.category||'Software',fValue:x?.value??75000,fRenew:x?.renew||'2027-04-15',fNotice:x?.notice??60,fRisk:x?.risk??12,fLoss:x?.loss??40000};Object.entries(fields).forEach(([k,v])=>byId(k).value=v);byId('modalWrap').classList.add('open');byId('fName').focus()}
|
| 103 |
+
function closeModal(){byId('modalWrap').classList.remove('open');editing=null}
|
| 104 |
+
function formData(){const n=k=>Number(byId(k).value);return{id:editing===null?state.nextId:editing,name:byId('fName').value.trim(),party:byId('fParty').value.trim(),category:byId('fCategory').value,value:n('fValue'),renew:byId('fRenew').value,notice:n('fNotice'),risk:n('fRisk'),loss:n('fLoss'),archived:editing===null?false:state.items.find(x=>x.id===editing)?.archived??false}}
|
| 105 |
+
function csvCell(v){let s=String(v??'');if(/^[\t\r\n\s]*[=+@-]/.test(s)&&!/^[-]\d+(\.\d+)?$/.test(s.trim()))s="'"+s;return '"'+s.replace(/"/g,'""')+'"'}
|
| 106 |
+
function download(name,txt,mime){const b=new Blob([txt],{type:mime});const url=URL.createObjectURL(b);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000)}
|
| 107 |
+
function exportCsv(){const heads=['id','name','counterparty','category','annual_usd','renewal_date','notice_days','incident_probability_percent','incident_loss_usd','expected_loss_usd','archived'];const rows=state.items.map(x=>[x.id,x.name,x.party,x.category,x.value,x.renew,x.notice,x.risk,x.loss,riskFor(x),x.archived]);download('contractpilot-contracts.csv',[heads,...rows].map(a=>a.map(csvCell).join(',')).join('\r\n'),'text/csv;charset=utf-8');notify('CSV exported.')}function exportJson(){download('contractpilot-backup.json',JSON.stringify({kind:'ContractPilotBackup',version:1,...state},null,2),'application/json');notify('JSON backup exported.')}
|
| 108 |
+
function attach(){document.querySelectorAll('[data-page]').forEach(el=>el.addEventListener('click',()=>setPage(el.dataset.page)));document.querySelectorAll('[data-go]').forEach(el=>el.addEventListener('click',()=>setPage(el.dataset.go)));byId('themeBtn').addEventListener('click',()=>{document.body.classList.toggle('dark');try{localStorage.setItem(THEME,document.body.classList.contains('dark')?'dark':'light')}catch(e){};update()});byId('csvBtn').addEventListener('click',exportCsv);byId('jsonBtn').addEventListener('click',exportJson);
|
| 109 |
+
byId('addBtn').addEventListener('click',()=>{if(state.items.length>=150)return notify('Limit of 150 contracts reached.');openModal()});['search','catFilter','statusFilter'].forEach(k=>byId(k).addEventListener(k==='search'?'input':'change',contractRows));
|
| 110 |
+
byId('cancelModal').addEventListener('click',closeModal);byId('modalWrap').addEventListener('click',e=>{if(e.target.id==='modalWrap')closeModal()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});
|
| 111 |
+
byId('contractForm').addEventListener('submit',e=>{e.preventDefault();const x=formData();if(!validContract(x))return notify('Invalid contract input. Check all fields and limits.');if(editing===null){if(state.items.length>=150)return;state.items.push(x);state.nextId++}else{const ix=state.items.findIndex(r=>r.id===editing);if(ix<0)return;state.items[ix]=x}save();closeModal();update();notify('Contract saved locally.')});
|
| 112 |
+
byId('contractRows').addEventListener('click',e=>{const ed=e.target.closest('[data-edit]'),ar=e.target.closest('[data-archive]');if(ed){openModal(Number(ed.dataset.edit))}if(ar){const id=Number(ar.dataset.archive),x=state.items.find(r=>r.id===id);if(!x)return;x.archived=!x.archived;save();update();notify(x.archived?'Record archived, not deleted.':'Record restored.')}});
|
| 113 |
+
byId('presets').addEventListener('click',e=>{const b=e.target.closest('[data-preset]');if(!b)return;const a=PRESETS.find(x=>x.name===b.dataset.preset);if(!a)return;scenario={price:a.price,prob:a.prob,severity:a.severity};preset=a.name;renderScenario()});byId('sliders').addEventListener('input',e=>{if(!e.target.matches('[data-slide]'))return;const key=e.target.dataset.slide;if(!['price','prob','severity'].includes(key))return;scenario[key]=Number(e.target.value);preset='Custom';byId('out_'+key).textContent=(scenario[key]>0?'+':'')+scenario[key]+(key==='prob'?' pp':'%');document.querySelectorAll('[data-preset]').forEach(b=>b.classList.remove('sel'));renderScenario(false)});
|
| 114 |
+
byId('importBtn').addEventListener('click',()=>byId('fileInput').click());byId('fileInput').addEventListener('change',async e=>{const f=e.target.files?.[0];e.target.value='';if(!f)return;if(f.size>2*1024*1024){notify('Backup is too large (max 2 MB).');return}try{const x=JSON.parse(await f.text());if(!validBackup(x)){notify('Backup rejected: invalid schema or field values.');return}if(!confirm('Replace current local registry with the selected validated backup?'))return;state={items:clone(x.items),nextId:x.nextId};save();update();notify('Validated backup restored.')}catch(err){notify('Backup rejected: could not parse JSON.')}});
|
| 115 |
+
byId('resetBtn').addEventListener('click',()=>{if(!confirm('Replace ALL local records with eight fictional demo contracts? Export JSON first if needed.'))return;state={items:clone(DEFAULTS),nextId:9};save();update();notify('Demo restored.')});}
|
| 116 |
+
load();attach();update();
|
| 117 |
+
window.__contractPilotTest={cap,esc,validContract,validBackup,project,simulate,riskFor,dates,monthBuckets,seed,preview:()=>clone(state),setScenario:s=>{scenario=s;renderScenario()},version:1};
|
| 118 |
+
})();
|
| 119 |
+
</script></body></html>
|