your-local-cost-explorer 0.4.1 → 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 +6 -0
- package/engine/local_app.py +4 -1
- package/package.json +1 -1
- package/web/app.js +8 -15
- package/web/index.html +3 -3
- package/web/style.css +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,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
|
+
|
|
1
7
|
# 0.4.1
|
|
2
8
|
|
|
3
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.
|
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,5 +1,5 @@
|
|
|
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);
|
|
@@ -8,20 +8,20 @@ let fileMetadata=[], comparisonMode='history', comparisonIds=new Set(), detected
|
|
|
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
27
|
$('metric-charges').textContent = data ? money(data.validation?.window_total_cents) : '-';
|
|
@@ -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.');};
|
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}
|