your-local-cost-explorer 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -9
- package/engine/local_app.py +4 -1
- package/package.json +1 -1
- package/web/app.js +13 -20
- package/web/index.html +3 -3
- package/web/style.css +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
# 0.4.
|
|
2
|
-
|
|
3
|
-
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
# 0.4.2
|
|
2
|
+
|
|
3
|
+
- Refresh local sessions securely from the origin-checked page, repairing uploads and team saves after restart.
|
|
4
|
+
- Move upload/run controls to Check Charges; external check buttons open that page and its file picker. Dashboard shows summaries.
|
|
5
|
+
- Remove the device resources panel and bulk mapping text area; keep structured team add/edit/delete controls.
|
|
6
|
+
|
|
7
|
+
# 0.4.1
|
|
8
|
+
|
|
9
|
+
- Restore the selected device baseline when switching comparison modes. Respect the saved supported date-range preference and keep empty metric placeholders as `-`. Focused DOM regression passed.
|
|
10
|
+
|
|
11
|
+
# 0.4.0 - Your Local Cost Explorer
|
|
12
|
+
|
|
13
|
+
- Rename the public project and npm command to `your-local-cost-explorer`. Keep the existing private data/runtime directory so upgrading does not lose saved work.
|
|
14
|
+
- Keep the local light design and sidebar. Remove Data Sources navigation; upload and retained-file controls appear only on Dashboard. Replace broken dash characters with `-`.
|
|
15
|
+
- Inspect observed CUR dates locally for CSV, gzip and Parquet. Offer supported 7/30/90/180-day windows ending at the supplied data's latest date, with earlier rows as baseline. Missing history stays unavailable.
|
|
16
|
+
- Put previous-check and device-file comparisons below the baseline fields. Retained comparisons verify original file hashes and cannot contaminate primary data.
|
|
17
|
+
- Preserve all eight detectors, exact decimal financial calculations, commitments, allocation, settings and downloadable local reports. No hosted or automatic connections are included.
|
|
18
|
+
|
|
19
|
+
Focused verification: real loopback uploads, CUR date aliases/formats/conflicts, calendar windows, previous-check provenance and separation, DOM upload/period/button behavior, small independent signed-charge/rounding ledger, duplicate removal, original eight-detector and commitment controls, hosted pricing withdrawal tests, frontend typecheck and production build. No large-file laptop stress test.
|
package/engine/local_app.py
CHANGED
|
@@ -208,7 +208,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
208
208
|
if not self.guard(False):
|
|
209
209
|
return
|
|
210
210
|
name, kind = {'/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'application/javascript'), '/style.css': ('style.css', 'text/css')}[parsed.path]
|
|
211
|
-
|
|
211
|
+
body = (WEB / name).read_bytes()
|
|
212
|
+
if parsed.path == '/':
|
|
213
|
+
body = body.replace(b'</head>', ('<meta name="local-session" content="'+self.server.token+'"></head>').encode())
|
|
214
|
+
self.respond(200, body, kind)
|
|
212
215
|
return
|
|
213
216
|
if not self.guard():
|
|
214
217
|
return
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
const token = new URLSearchParams(location.hash.slice(1)).get('token') || sessionStorage.getItem('local-token') || '';
|
|
2
|
+
const token = document.querySelector('meta[name=local-session]')?.content || new URLSearchParams(location.hash.slice(1)).get('token') || sessionStorage.getItem('local-token') || '';
|
|
3
3
|
if (token) sessionStorage.setItem('local-token', token);
|
|
4
4
|
history.replaceState(null, '', '/');
|
|
5
5
|
const $ = id => document.getElementById(id);
|
|
6
6
|
let periodTouched=false;
|
|
7
|
-
let fileMetadata=[], comparisonMode='history', comparisonIds=new Set(), detectedSpan=null;
|
|
7
|
+
let fileMetadata=[], comparisonMode='history', comparisonIds=new Set(), detectedSpan=null, deviceSpan=null;
|
|
8
8
|
let selected = new Set(), current = null, result = null, activeView = 'overview', pending = false, running = false;
|
|
9
9
|
const titles = {overview:'Dashboard',check:'Check Charges',findings:'Findings',commitments:'Commitments',allocation:'Allocation',reports:'Reports',settings:'Settings',teams:'Team mappings',evidence:'Check history & evidence'};
|
|
10
10
|
function navigate(view) {
|
|
11
|
-
activeView = view; $('view-title').textContent = titles[view];
|
|
11
|
+
activeView = view; $('new-check').hidden=view==='check'; $('view-title').textContent = titles[view];
|
|
12
12
|
document.querySelectorAll('[data-panels]').forEach(panel => { panel.hidden = !panel.dataset.panels.split(' ').includes(view); });
|
|
13
13
|
document.querySelectorAll('[data-view]').forEach(item => { if (item.dataset.view === view) item.setAttribute('aria-current','page'); else item.removeAttribute('aria-current'); });
|
|
14
14
|
document.querySelectorAll('[data-only]').forEach(item => { item.hidden = item.dataset.only !== view; });
|
|
15
|
-
const scopes={overview:[
|
|
15
|
+
const scopes={overview:[],findings:['findings','baselines'],commitments:['sp_assessment','ri_assessment','commitments'],allocation:['allocation'],teams:['teams'],reports:['caveats','input_files']};
|
|
16
16
|
document.querySelectorAll('#results details').forEach(item => { item.hidden = view !== 'evidence' && !(scopes[view]||[]).includes(item.dataset.key); });
|
|
17
|
-
$('finding-table').hidden =
|
|
17
|
+
$('finding-table').hidden = view!=='findings';
|
|
18
18
|
if(typeof renderManualPanels==='function') renderManualPanels();
|
|
19
19
|
if(view==='reports')loadReports().catch(error=>status(error.message));
|
|
20
|
-
if(view==='settings'){loadSettings().catch(error=>status(error.message));
|
|
20
|
+
if(view==='settings'){loadSettings().catch(error=>status(error.message));}
|
|
21
21
|
}
|
|
22
|
-
document.querySelectorAll('[data-view]').forEach(item => { item.onclick = () => navigate(item.dataset.view); });
|
|
23
|
-
$('start-check').onclick = () => { navigate('
|
|
24
|
-
$('new-check').onclick = () => { navigate(
|
|
22
|
+
document.querySelectorAll('[data-view]').forEach(item => { item.onclick = () => { const choose=item.dataset.view==='check'&&activeView!=='check'; navigate(item.dataset.view); if(choose)$('upload').click(); }; });
|
|
23
|
+
$('start-check').onclick = () => { navigate('check'); $('upload').click(); };
|
|
24
|
+
$('new-check').onclick = () => { navigate('check'); $('upload').click(); };
|
|
25
25
|
const money = cents => Number.isSafeInteger(cents) ? new Intl.NumberFormat(undefined,{style:'currency',currency:'USD'}).format(cents/100) : 'Unavailable';
|
|
26
26
|
function renderSummary(data) {
|
|
27
|
-
$('metric-charges').textContent = money(data
|
|
27
|
+
$('metric-charges').textContent = data ? money(data.validation?.window_total_cents) : '-';
|
|
28
28
|
$('metric-findings').textContent = Array.isArray(data?.findings) ? data.findings.length : '-';
|
|
29
29
|
$('metric-rows').textContent = Number.isSafeInteger(data?.validation?.unique_rows) ? data.validation.unique_rows.toLocaleString() : '-';
|
|
30
30
|
$('metric-commitments').textContent = Array.isArray(data?.commitments) ? data.commitments.length : '-';
|
|
@@ -52,7 +52,6 @@ async function refresh() {
|
|
|
52
52
|
}
|
|
53
53
|
await loadHistory();updatePeriods();
|
|
54
54
|
if (!current && state.runs.length) { current = state.runs[0].id; await show(); }
|
|
55
|
-
if (!$('mappings').dataset.loaded) { $('mappings').value = Object.entries(state.mappings).map(([account, team]) => `${account} | ${team}`).join('\n'); $('mappings').dataset.loaded = 'yes'; }
|
|
56
55
|
}
|
|
57
56
|
async function show() {
|
|
58
57
|
if (!current || pending) return;
|
|
@@ -87,9 +86,6 @@ $('analyse').onclick = async () => {
|
|
|
87
86
|
} catch (error) { status(error.message); }
|
|
88
87
|
};
|
|
89
88
|
$('cancel').onclick = async () => { try { await request('/api/cancel', {method:'POST', body:'{}'}); await refresh(); await show(); } catch (error) { status(error.message); } };
|
|
90
|
-
$('save-mappings').onclick = async () => {
|
|
91
|
-
try { const mappings = {}; for (const line of $('mappings').value.split('\n').filter(line => line.trim())) { const [account, team] = line.split('|').map(item => item.trim()); if (account in mappings) throw new Error('Duplicate mapped account'); mappings[account] = team; } await request('/api/mappings', {method:'POST', body:JSON.stringify({mappings})}); await loadSettings(); status('Mappings saved. New analyses use this mapping snapshot.'); } catch (error) { status(error.message); }
|
|
92
|
-
};
|
|
93
89
|
$('download').onclick = () => { if (!result) return; const url = URL.createObjectURL(new Blob([JSON.stringify({...result,local_review:findingUpdates},null,2)], {type:'application/json'})); const a = document.createElement('a'); a.href = url; a.download = `your-local-cost-explorer-${current}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); };
|
|
94
90
|
$('print').onclick = () => { if(result) openPrintable(); };
|
|
95
91
|
let findingUpdates={}, page=0, historyCursor=null, historyRows=[], editing=null;
|
|
@@ -188,7 +184,6 @@ async function loadSettings(reset=false){
|
|
|
188
184
|
$('team-list').append(safeTable('Team mappings',['Name','AWS account id','Actions'],Object.entries(data.teams).map(([account,name])=>{
|
|
189
185
|
const actions=document.createElement('div');actions.append(button('Edit',async()=>{editingTeam=account;$('team-name').value=name;$('team-account').value=account;$('add-team').textContent='Save';$('cancel-team').hidden=false;}),button('Delete',async()=>{if(!confirm(`Delete the team "${name}" (${account})?`))return;await request('/api/settings/teams',{method:'POST',body:JSON.stringify({action:'delete',account})});await loadSettings();}));return [name,account,actions];
|
|
190
186
|
})));
|
|
191
|
-
$('mappings').value=Object.entries(data.teams).map(([account,name])=>`${account} | ${name}`).join('\n');
|
|
192
187
|
}
|
|
193
188
|
$('save-settings').onclick=async()=>{try{await request('/api/settings',{method:'POST',body:JSON.stringify({name:$('settings-company').value,defaultDateRange:$('settings-range').value,creepMonthlyGrowthPercent:$('settings-creep').value})});await loadSettings(true);$('settings-status').textContent='Saved. Applies to future checks; saved results are unchanged.';}catch(error){$('settings-status').textContent=error.message;}};
|
|
194
189
|
$('discard-settings').onclick=()=>loadSettings(true).then(()=>{$('settings-status').textContent='Changes discarded.';}).catch(error=>status(error.message));
|
|
@@ -205,8 +200,6 @@ $('generate-report').onclick=async()=>{try{await request('/api/reports/update',{
|
|
|
205
200
|
Promise.all([refresh(),loadSettings()]).then(()=>status('Ready. Choose a saved check or start a manual analysis.')).catch(error=>status(error.message));
|
|
206
201
|
setInterval(()=>{if(current&&running)show().catch(error=>status(error.message));},2500);
|
|
207
202
|
|
|
208
|
-
async function loadResources(){const data=await request('/api/resources');const processing=data.processing;const size=bytes=>Number.isSafeInteger(bytes)?(bytes/1024**3).toFixed(2)+' GiB':'Unavailable';$('hardware-info').textContent=`No fixed input-size cap. ${processing.cpu_threads} available CPU threads; ${size(processing.available_memory_bytes)} available RAM, ${size(processing.duckdb_memory_bytes)} allocated to the database; ${size(processing.spill_bytes)} usable working disk. Upload storage: ${size(data.uploads.usable_bytes)} currently usable. The app reserves headroom for the operating system; successful completion is not guaranteed.`;}
|
|
209
|
-
$('refresh-resources').onclick=()=>loadResources().catch(error=>status(error.message));
|
|
210
203
|
|
|
211
204
|
$('report-checks').onchange=()=>{$('generate-report').disabled=!$('report-checks').selectedOptions.length;};
|
|
212
205
|
$('cancel-report').onclick=()=>{$('report-name').value='';for(const option of $('report-checks').options)option.selected=false;$('generate-report').disabled=true;status('Report draft cancelled. No report was saved.');};
|
|
@@ -216,7 +209,7 @@ function updatePeriods(){
|
|
|
216
209
|
const spans=fileMetadata.filter(file=>selected.has(file.id)&&file.role==='primary').map(file=>file.inspection).filter(span=>span?.status==='available');
|
|
217
210
|
if(!spans.length){detectedSpan=null;$('date-notice').textContent='Upload supported files to detect dates, or enter dates manually.';return;}
|
|
218
211
|
const first=spans.map(s=>s.min_date).sort()[0],last=spans.map(s=>s.max_date).sort().at(-1);detectedSpan={first,last};const count=Math.round((new Date(last)-new Date(first))/86400000)+1;
|
|
219
|
-
const picker=$('period-choice'),old=picker.value;picker.replaceChildren();for(const days of [7,30,90,180])if(days<=count){const option=document.createElement('option');option.value=String(days);option.textContent='Last '+days+' days';picker.append(option);}const manual=document.createElement('option');manual.value='manual';manual.textContent='Manual dates';picker.append(manual);picker.value=periodTouched&&Array.from(picker.options).some(o=>o.value===old)?old:String([
|
|
212
|
+
const picker=$('period-choice'),old=picker.value;picker.replaceChildren();for(const days of [7,30,90,180])if(days<=count){const option=document.createElement('option');option.value=String(days);option.textContent='Last '+days+' days';picker.append(option);}const manual=document.createElement('option');manual.value='manual';manual.textContent='Manual dates';picker.append(manual);picker.value=periodTouched&&Array.from(picker.options).some(o=>o.value===old)?old:String([7,30,90,180].includes(Number(storedSettings?.defaultDateRange))&&Number(storedSettings.defaultDateRange)<=count?storedSettings.defaultDateRange:count>=30?30:count>=7?7:'manual');if(!periodTouched&&count<7){$('start').value=first;$('end').value=last;if(comparisonMode==='history'){$('baseline_start').value=$('baseline_end').value=shiftDay(first,-1);}$('date-notice').textContent=`Observed file dates: ${first} to ${last}. No earlier rows are available for a baseline. Bounds do not prove completeness.`;}applyPeriod();
|
|
220
213
|
}
|
|
221
214
|
function applyPeriod(){
|
|
222
215
|
if(!detectedSpan||$('period-choice').value==='manual')return;
|
|
@@ -224,9 +217,9 @@ function applyPeriod(){
|
|
|
224
217
|
$('date-notice').textContent=`Observed file dates: ${first} to ${last}. ${first<start?'All supplied history before the analysis window is the baseline.':'No earlier rows exist in this file; historical comparisons are unavailable unless you supply a baseline.'} Bounds do not prove complete coverage.`;
|
|
225
218
|
}
|
|
226
219
|
$('period-choice').onchange=()=>{periodTouched=true;applyPeriod();};
|
|
227
|
-
function compareMode(mode){comparisonMode=mode;$('previous-wrap').hidden=mode!=='previous';$('device-wrap').hidden=mode!=='device';$('comparison-notice').textContent=mode==='history'?'Use all earlier rows in the selected primary files.':mode==='device'?'Selected comparison files remain separate from primary data.':'Previous check must retain unchanged source files and end before this analysis.';if(mode==='history')applyPeriod();}
|
|
220
|
+
function compareMode(mode){comparisonMode=mode;$('previous-wrap').hidden=mode!=='previous';$('device-wrap').hidden=mode!=='device';$('comparison-notice').textContent=mode==='history'?'Use all earlier rows in the selected primary files.':mode==='device'?'Selected comparison files remain separate from primary data.':'Previous check must retain unchanged source files and end before this analysis.';if(mode==='history')applyPeriod();if(mode==='device'&&deviceSpan){$('baseline_start').value=deviceSpan.first;$('baseline_end').value=deviceSpan.last;}}
|
|
228
221
|
$('compare-previous').onclick=()=>{compareMode('previous');if($('previous-check').value)$('previous-check').dispatchEvent(new Event('change'));};$('compare-device').onclick=()=>compareMode('device');$('compare-history').onclick=()=>compareMode('history');
|
|
229
222
|
$('previous-check').onchange=async()=>{const requested=$('previous-check').value;try{const old=await request('/api/runs/'+requested);if(comparisonMode==='previous'&&$('previous-check').value===requested&&old.result?.window){$('baseline_start').value=old.result.window.start;$('baseline_end').value=old.result.window.end;}}catch(error){status(error.message);}};
|
|
230
|
-
$('comparison-upload').onchange=async event=>{try{comparisonIds.clear();const spans=[];for(const file of event.target.files){status('Uploading comparison '+file.name);const uploaded=await request('/api/upload?name='+encodeURIComponent(file.name)+'&role=comparison',{method:'PUT',body:file});comparisonIds.add(uploaded.id);if(uploaded.inspection?.status==='available')spans.push(uploaded.inspection);}if(
|
|
223
|
+
$('comparison-upload').onchange=async event=>{try{comparisonIds.clear();deviceSpan=null;const spans=[];for(const file of event.target.files){status('Uploading comparison '+file.name);const uploaded=await request('/api/upload?name='+encodeURIComponent(file.name)+'&role=comparison',{method:'PUT',body:file});comparisonIds.add(uploaded.id);if(uploaded.inspection?.status==='available')spans.push(uploaded.inspection);}if(spans.length){deviceSpan={first:spans.map(s=>s.min_date).sort()[0],last:spans.map(s=>s.max_date).sort().at(-1)};if(comparisonMode==='device')compareMode('device');}status(comparisonIds.size+' comparison files selected. Review baseline dates before running.');}catch(error){status(error.message);}finally{event.target.value='';}};
|
|
231
224
|
for(const id of ['start','end','baseline_start','baseline_end'])$(id).addEventListener('input',()=>{periodTouched=true;$('period-choice').value='manual';});
|
|
232
225
|
navigate('overview');
|
package/web/index.html
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Your Local Cost Explorer</title><link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script></head>
|
|
3
3
|
<body><aside class="sidebar"><a class="brand" href="#">L<span>Your Local Cost Explorer</span><small>LOCAL WORKSPACE</small></a><nav aria-label="Dashboard"><button data-view="overview" aria-current="page">Dashboard</button><button data-view="check">Check Charges</button><button data-view="findings">Findings</button><button data-view="commitments">Commitments</button><button data-view="allocation">Allocation</button><button data-view="reports">Reports</button><button data-view="settings">Settings</button><button data-view="evidence">Check history & evidence</button></nav><div class="local-badge"><i></i> Offline by design<p>Your data stays on this device.</p></div></aside><main><div class="workspace-bar"><span>Your local workspace</span><button id="new-check" class="primary">Check AWS Charges</button></div><header><p class="eyebrow">Your local workspace</p><h1 id="view-title">Dashboard.</h1><p>Check reported charges, commitments and allocation on your own device. Files stay here.</p></header>
|
|
4
4
|
<p class="notice">Estimates use only the AWS data provided. Changes in cloud usage do not establish company or revenue growth. Missing exports, workload changes, discounts and billing adjustments can affect the result.</p>
|
|
5
|
-
<div class="metrics" aria-label="Selected check summary"><article><span>Reported charges</span><strong id="metric-charges">-</strong><small>Selected analysis period</small></article><article><span>Findings</span><strong id="metric-findings">-</strong><small>Review evidence before acting</small></article><article><span>Canonical observations</span><strong id="metric-rows">-</strong><small>After proven copies removed</small></article><article><span>Commitments</span><strong id="metric-commitments">-</strong><small>Source-supported assessments</small></article></div><p id="status" role="status" aria-live="polite"></p>
|
|
6
|
-
<section data-panels="
|
|
5
|
+
<div data-panels="overview" class="metrics" aria-label="Selected check summary"><article><span>Reported charges</span><strong id="metric-charges">-</strong><small>Selected analysis period</small></article><article><span>Findings</span><strong id="metric-findings">-</strong><small>Review evidence before acting</small></article><article><span>Canonical observations</span><strong id="metric-rows">-</strong><small>After proven copies removed</small></article><article><span>Commitments</span><strong id="metric-commitments">-</strong><small>Source-supported assessments</small></article></div><p id="status" role="status" aria-live="polite"></p>
|
|
6
|
+
<section data-panels="check" hidden><h2>Billing files</h2><p>CSV, gzip or Parquet. No fixed file-size or batch-size limit. Your available hardware determines what can complete. If your device cannot handle a check, the app will report the resource problem. Missing observations are never assumed zero.</p><label>Choose files <input id="upload" type="file" accept=".csv,.gz,.parquet" multiple></label><div id="files"></div></section>
|
|
7
7
|
<section data-panels="check" hidden><h2>New charge check</h2><label>Analysis period <select id="period-choice"><option value="manual">Upload a file to detect its dates</option></select></label><p id="date-notice" role="status"></p><div class="grid"><label>Analysis start <input id="start" type="date" required></label><label>Analysis end <input id="end" type="date" required></label><label>Baseline start <input id="baseline_start" type="date" required></label><label>Baseline end <input id="baseline_end" type="date" required></label></div><div class="comparison-controls"><button id="compare-previous" class="secondary">Compare with previous checks</button> <button id="compare-device" class="secondary">Compare from your device</button> <button id="compare-history" class="secondary">Use earlier rows in this file</button><label id="previous-wrap" hidden>Previous completed check <select id="previous-check"></select></label><label id="device-wrap" hidden>Comparison files <input id="comparison-upload" type="file" accept=".csv,.gz,.parquet" multiple></label><p id="comparison-notice"></p></div><p>Baseline must end before analysis begins. Comparison files remain separate from primary files.</p>
|
|
8
8
|
<details><summary>Service expectations</summary><p>Optional: one service, expected total in USD and note per line, separated by |. Example: AmazonEC2 | 100.00 | agreed budget</p><label>Expected charges <textarea id="expectations" rows="3"></textarea></label></details>
|
|
9
9
|
<button id="analyse">Check AWS Charges</button> <button id="cancel" class="secondary">Cancel check</button></section>
|
|
10
|
-
<section data-panels="settings" hidden><h2>General & teams</h2><div id="settings-general"><label>Company name <input id="settings-company" maxlength="255"></label><label>Default date range <select id="settings-range"><option value="7">7-day lookback (8 inclusive calendar days)</option><option value="30">30-day lookback (31 inclusive calendar days)</option><option value="60">60-day lookback (61 inclusive calendar days)</option><option value="90">90-day lookback (91 inclusive calendar days)</option></select></label><label>Creep monthly growth percentage <input id="settings-creep" value="10" inputmode="decimal" maxlength="128"></label><p>Default 10%. Applies to future checks; previous evidence is preserved.</p><button id="save-settings">Save account and defaults</button> <button id="discard-settings" class="secondary">Discard</button><p id="settings-status" role="status"></p></div><h3>Team mappings</h3><div class="grid"><label>Team name <input id="team-name" maxlength="255"></label><label>AWS account id <input id="team-account" maxlength="12" inputmode="numeric"></label></div><button id="add-team">Add team</button> <button id="cancel-team" class="secondary" hidden>Cancel</button><div id="team-list"></div><
|
|
10
|
+
<section data-panels="settings" hidden><h2>General & teams</h2><div id="settings-general"><label>Company name <input id="settings-company" maxlength="255"></label><label>Default date range <select id="settings-range"><option value="7">7-day lookback (8 inclusive calendar days)</option><option value="30">30-day lookback (31 inclusive calendar days)</option><option value="60">60-day lookback (61 inclusive calendar days)</option><option value="90">90-day lookback (91 inclusive calendar days)</option></select></label><label>Creep monthly growth percentage <input id="settings-creep" value="10" inputmode="decimal" maxlength="128"></label><p>Default 10%. Applies to future checks; previous evidence is preserved.</p><button id="save-settings">Save account and defaults</button> <button id="discard-settings" class="secondary">Discard</button><p id="settings-status" role="status"></p></div><h3>Team mappings</h3><div class="grid"><label>Team name <input placeholder="Platform team" id="team-name" maxlength="255"></label><label>AWS account id <input placeholder="12-digit AWS account ID" id="team-account" maxlength="12" inputmode="numeric"></label></div><button id="add-team">Add team</button> <button id="cancel-team" class="secondary" hidden>Cancel</button><div id="team-list"></div><h3>Documentation</h3><p>This private workspace has no account login, billing, AWS or delivery connections, schedules or rules. Upload primary exports, choose comparison evidence, review findings, commitments and allocation, then download reports locally. Installation provisions Python and dependencies automatically.</p></section>
|
|
11
11
|
<section data-panels="overview findings evidence commitments allocation reports"><h2>Results and evidence</h2><p>Checks cover unexpected charge, spike, gradual creep, new charge, usage/cost mismatch, Marketplace, expectation mismatch and unused commitment. Missing optional evidence limits the relevant check.</p><div class="report-toolbar"><label>Selected check <select id="check-picker"><option value="">No saved checks</option></select></label><button id="archive-check" class="secondary" disabled>Archive check</button></div><div data-only="evidence" hidden><div id="runs" class="run-list"></div><button id="history-more" class="secondary" hidden>Load older checks</button></div><div class="filter-row" data-only="findings" hidden><label>Search <input id="finding-search" type="search" placeholder="Service, account or finding"></label><label>Severity <select id="finding-severity"><option value="">All severities</option><option>critical</option><option>high</option><option>medium</option><option>low</option></select></label><label>Detector <select id="finding-detector"><option value="">All detectors</option></select></label><label>Status <select id="finding-status"><option value="">All statuses</option><option>open</option><option>investigating</option><option>resolved</option></select></label><label><input id="finding-archived" type="checkbox"> Include archived</label></div><div id="finding-table"><div class="empty-state"><h3>Your next check starts here</h3><p>Add a CUR export to see reported charges, unusual spend and commitment evidence.</p><button id="start-check">Add billing files</button></div></div><div data-only="reports" hidden><label>Report name <input id="report-name" maxlength="255" placeholder="Monthly AWS review"></label><label>Completed checks <select id="report-checks" multiple size="4"></select></label><p>Check snapshots remain separate. Their totals and overlapping findings are not added together.</p><button id="generate-report" disabled>Save</button> <button id="cancel-report" class="secondary">Cancel</button><div id="saved-reports"></div><button id="reports-more" class="secondary" hidden>Load more</button></div><div id="manual-tables"></div><div id="results"></div><button id="download" disabled>Download complete JSON</button> <button id="print" disabled class="secondary">Print report</button> <button id="html-report" disabled class="secondary">Download meeting report</button> <button id="csv-report" disabled class="secondary">Export findings CSV</button></section>
|
|
12
12
|
<dialog id="finding-dialog"><div class="dialog-head"><h2 id="finding-title">Finding evidence</h2><button id="close-finding" class="secondary">Close</button></div><div id="finding-evidence"></div><label>Status <select id="edit-status"><option>open</option><option>investigating</option><option>resolved</option></select></label><label>Review note <textarea id="edit-note" maxlength="4096" rows="3"></textarea></label><label><input id="edit-archived" type="checkbox"> Archive this finding</label><button id="save-finding">Save changes</button></dialog><footer>Local processing. No AWS connection, hosted login, telemetry, mail or webhook delivery. Reports contain your billing identifiers; share them deliberately.</footer></main></body></html>
|
package/web/style.css
CHANGED
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
/* Local controls deliberately retain this edition's light palette and sidebar. */
|
|
4
4
|
.workspace-bar,.report-toolbar,.filter-row,.dialog-head{display:flex;gap:14px;align-items:center;justify-content:space-between;flex-wrap:wrap}.workspace-bar{margin-bottom:25px;padding:14px 20px;background:#fff;border:1px solid #e5e9f0;border-radius:9px;color:#728097}.filter-row{justify-content:flex-start}.filter-row label{font-size:12px}.report-toolbar select{max-width:100%}input:not([type=checkbox]):not([type=file]){padding:9px 12px;border:1px solid #d6dfe9;border-radius:6px;background:#fff;color:#263445;max-width:100%}.link-button{border:0;background:none!important;color:#3559c7!important;box-shadow:none;text-align:left;padding:0;text-decoration:underline}.table-hint{font-size:12px;color:#718197}dialog{border:1px solid #d6dfe9;border-radius:12px;background:#fff;color:#263445;width:min(850px,94vw);max-height:88vh;overflow:auto;padding:26px}dialog::backdrop{background:#17263880}.table-scroll caption{text-align:left;padding:12px 0;font-weight:600}.observed-chart{padding:18px;margin:20px 0;border:1px solid #e5e9f0;border-radius:9px;background:#fafbfd}.observed-chart svg{width:100%;height:auto;max-height:220px}.observed-chart .table-scroll{max-height:300px;overflow:auto}#settings-general{max-width:600px}#settings-general input,#settings-general select{display:block;width:100%}.report-row{padding:12px 0;border-bottom:1px solid #edf0f5}.report-row button{margin-right:6px}@media(max-width:650px){.filter-row label{width:100%}.report-toolbar label{width:100%}.report-toolbar select{width:100%}}@media print{.workspace-bar,.filter-row,.report-toolbar,dialog{display:none!important}#results details{display:none}}
|
|
5
|
+
|
|
6
|
+
#team-name,#team-account{display:block;width:100%;padding:12px;border:1px solid #d6dfe9;border-radius:6px;background:white;margin-top:6px} .comparison-controls{margin-top:16px}
|