pvanand commited on
Commit
cfa9669
Β·
verified Β·
1 Parent(s): 56d6a7c

Upload 2 files

Browse files
Files changed (2) hide show
  1. main.py +124 -3
  2. static/index.html +55 -2
main.py CHANGED
@@ -22,6 +22,8 @@ app.add_middleware(
22
 
23
  DATA_DIR = Path("output")
24
  CONFIG_PATH = Path("data.json")
 
 
25
 
26
  # ── Helpers ──────────────────────────────────────────────────────────────
27
 
@@ -54,6 +56,128 @@ def load_config() -> dict:
54
  return json.load(f)
55
 
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  # ── Config & Specs ───────────────────────────────────────────────────────
58
 
59
  @app.get("/api/config")
@@ -113,9 +237,6 @@ def get_water_budget():
113
 
114
  # ── Time Series Endpoints ────────────────────────────────────────────────
115
 
116
- POWER_RESOLUTIONS = {"1SEC", "1MIN", "15MIN", "1H", "1DAY"}
117
- WATER_RESOLUTIONS = {"1MIN", "15MIN", "1H", "1DAY"}
118
-
119
 
120
  # ── Hourly Profile (MUST be before /{resolution} routes) ────────────────
121
 
 
22
 
23
  DATA_DIR = Path("output")
24
  CONFIG_PATH = Path("data.json")
25
+ POWER_RESOLUTIONS = {"1SEC", "1MIN", "15MIN", "1H", "1DAY"}
26
+ WATER_RESOLUTIONS = {"1MIN", "15MIN", "1H", "1DAY"}
27
 
28
  # ── Helpers ──────────────────────────────────────────────────────────────
29
 
 
56
  return json.load(f)
57
 
58
 
59
+ def get_csv_headers(path: Path) -> list[str]:
60
+ """Return column names from first line of CSV."""
61
+ if not path.exists():
62
+ return []
63
+ with open(path) as f:
64
+ return next(csv.reader(f), [])
65
+
66
+
67
+ def infer_type(col: str) -> str:
68
+ """Infer JSON schema type from column name."""
69
+ if col == "Time":
70
+ return "string (ISO8601)"
71
+ if "Pct" in col or "Level" in col or "Ah" in col or "Voltage" in col or "Flow" in col or "Total" in col or "kWh" in col or "Lpm" in col or "_L" in col:
72
+ return "number"
73
+ return "string"
74
+
75
+
76
+ # ── Schema & Data Tables APIs ───────────────────────────────────────────
77
+
78
+ def _build_schema_and_mermaid() -> tuple[dict, str]:
79
+ """Build schema dict and Mermaid ER diagram from actual CSV files."""
80
+ tables = {}
81
+ # Power: sample from 15MIN (flow) and 1H (totals) to cover both column sets
82
+ for res in POWER_RESOLUTIONS:
83
+ path = DATA_DIR / "power" / f"{res}.csv"
84
+ if path.exists():
85
+ headers = get_csv_headers(path)
86
+ if headers and (res not in tables or len(headers) > len(tables.get(res, {}).get("columns", []))):
87
+ tables[f"power_{res}"] = {
88
+ "stream": "power",
89
+ "resolution": res,
90
+ "columns": [{"name": h, "type": infer_type(h)} for h in headers],
91
+ }
92
+ # Water
93
+ for res in WATER_RESOLUTIONS:
94
+ path = DATA_DIR / "water" / f"{res}.csv"
95
+ if path.exists():
96
+ headers = get_csv_headers(path)
97
+ if headers:
98
+ tables[f"water_{res}"] = {
99
+ "stream": "water",
100
+ "resolution": res,
101
+ "columns": [{"name": h, "type": infer_type(h)} for h in headers],
102
+ }
103
+
104
+ # Mermaid ER diagram: one entity per logical table (power_series, water_series)
105
+ power_cols = set()
106
+ water_cols = set()
107
+ for key, meta in tables.items():
108
+ for c in meta["columns"]:
109
+ if meta["stream"] == "power":
110
+ power_cols.add((c["name"], c["type"]))
111
+ else:
112
+ water_cols.add((c["name"], c["type"]))
113
+ power_cols = sorted(power_cols, key=lambda x: (0 if x[0] == "Time" else 1, x[0]))
114
+ water_cols = sorted(water_cols, key=lambda x: (0 if x[0] == "Time" else 1, x[0]))
115
+
116
+ def mermaid_type(t: str) -> str:
117
+ if "string" in t or "ISO" in t:
118
+ return "string"
119
+ return "decimal"
120
+ def mermaid_row(name: str, t: str) -> str:
121
+ attr = name.replace(" ", "_")
122
+ typ = mermaid_type(t)
123
+ return f" {typ} {attr}"
124
+ lines = [
125
+ "erDiagram",
126
+ " power_timeseries {",
127
+ *[mermaid_row(c[0], c[1]) for c in power_cols],
128
+ " }",
129
+ " water_timeseries {",
130
+ *[mermaid_row(c[0], c[1]) for c in water_cols],
131
+ " }",
132
+ " power_timeseries ||--o| water_timeseries : aligned_by_Time",
133
+ ]
134
+ mermaid = "\n".join(lines)
135
+ return {"tables": tables}, mermaid
136
+
137
+
138
+ @app.get("/api/schema")
139
+ def get_schema():
140
+ """Return CSV data schema (tables, columns, types) and Mermaid ER diagram."""
141
+ schema_dict, mermaid = _build_schema_and_mermaid()
142
+ return {
143
+ **schema_dict,
144
+ "mermaid": mermaid,
145
+ }
146
+
147
+
148
+ @app.get("/api/data/tables")
149
+ def get_data_tables_list():
150
+ """Return list of available data tables (stream, resolution, columns, row_count)."""
151
+ result = []
152
+ for res in POWER_RESOLUTIONS:
153
+ path = DATA_DIR / "power" / f"{res}.csv"
154
+ if path.exists():
155
+ headers = get_csv_headers(path)
156
+ with open(path) as f:
157
+ row_count = sum(1 for _ in f) - 1 # exclude header
158
+ result.append({
159
+ "id": f"power_{res}",
160
+ "stream": "power",
161
+ "resolution": res,
162
+ "columns": headers,
163
+ "row_count": max(0, row_count),
164
+ })
165
+ for res in WATER_RESOLUTIONS:
166
+ path = DATA_DIR / "water" / f"{res}.csv"
167
+ if path.exists():
168
+ headers = get_csv_headers(path)
169
+ with open(path) as f:
170
+ row_count = sum(1 for _ in f) - 1
171
+ result.append({
172
+ "id": f"water_{res}",
173
+ "stream": "water",
174
+ "resolution": res,
175
+ "columns": headers,
176
+ "row_count": max(0, row_count),
177
+ })
178
+ return {"tables": result}
179
+
180
+
181
  # ── Config & Specs ───────────────────────────────────────────────────────
182
 
183
  @app.get("/api/config")
 
237
 
238
  # ── Time Series Endpoints ────────────────────────────────────────────────
239
 
 
 
 
240
 
241
  # ── Hourly Profile (MUST be before /{resolution} routes) ────────────────
242
 
static/index.html CHANGED
@@ -7,6 +7,7 @@
7
  <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3"></script>
 
10
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&display=swap" rel="stylesheet">
11
  <style>
12
  *{margin:0;padding:0;box-sizing:border-box}
@@ -102,6 +103,7 @@ html,body{height:100%;background:var(--bg-0);color:var(--t1);font-family:var(--s
102
  .gen-status.ok{color:var(--accent)}.gen-status.err{color:var(--danger)}
103
  .ld{display:flex;align-items:center;justify-content:center;height:200px;color:var(--t3);font:12px var(--mono);letter-spacing:1px}
104
  .pulse{animation:pulse 1.2s ease infinite}
 
105
  @keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
106
  @media(max-width:900px){
107
  .sb{width:54px}.sb-logo p,.ni span,.sb-ft,.sb-sec{display:none}
@@ -134,6 +136,10 @@ html,body{height:100%;background:var(--bg-0);color:var(--t1);font-family:var(--s
134
  <div class="ni" :class="{on:pg==='components'}" @click="go('components')">
135
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg><span>Components</span>
136
  </div>
 
 
 
 
137
  <div class="sb-sec" style="margin-top:8px">Settings</div>
138
  <div class="ni" :class="{on:pg==='generate'}" @click="go('generate')">
139
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg><span>Generate Data</span>
@@ -243,6 +249,25 @@ html,body{height:100%;background:var(--bg-0);color:var(--t1);font-family:var(--s
243
  <div class="pc"><div class="cd"><div class="cd-b" style="padding:0;max-height:calc(100vh - 130px);overflow-y:auto"><table class="tb"><thead><tr><th>Component</th><th>Voltage</th><th>Amps</th><th>Power</th><th>Water</th><th>Idle</th></tr></thead><tbody><template v-for="(items,cat) in comps" :key="cat"><tr><td colspan="6" class="tb-cat">{{cat}}</td></tr><tr v-for="(c,i) in items" :key="i"><td style="color:var(--t1)">{{c.name}}</td><td>{{c.voltage_v}}V</td><td>{{c.avg_amps}}A</td><td style="color:var(--solar)">{{(c.voltage_v*c.avg_amps).toFixed(1)}}W</td><td>{{c.water_gal_per_cycle||'β€”'}}</td><td>{{c.idle_amps||'β€”'}}</td></tr></template></tbody></table></div></div></div>
244
  </template>
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  <!-- GENERATE -->
247
  <template v-if="pg==='generate'">
248
  <div class="ph"><div><h2>Generate Data</h2><div class="sub">Configure trip parameters and regenerate simulation</div></div></div>
@@ -304,6 +329,8 @@ createApp({
304
  const pDay=ref(0),pDayInsight=ref('');
305
  const gen=reactive({user_type:'Typical',num_people:2,trip_duration_days:5,temperature:'Hot',sunlight:'Hi- Sunny',humidity:'Comfortable',seed:42});
306
  const genBusy=ref(false),genMsg=ref(''),genOk=ref(true);
 
 
307
  let pw15=[],pwH=[],wtH=[],wtD=[],wt15=[];
308
 
309
  const pBMax=computed(()=>Math.max(...Object.values(pBudget.value||{}),1));
@@ -454,11 +481,37 @@ createApp({
454
  genBusy.value=false;
455
  }
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  function go(p){pg.value=p;}
458
- watch(pg,async p=>{await nextTick();if(p==='overview')renderOV();if(p==='power')loadPW();if(p==='water')loadWT();if(p==='budget')loadBG();});
 
459
  onMounted(async()=>{await loadAll();await nextTick();renderOV();});
460
 
461
- return{pg,loading,cfg,S,pBudget,wBudget,comps,hProfile,whProfile,pDay,pDayInsight,gen,genBusy,genMsg,genOk,battCol,pBMax,wBMax,rd,fL,fmtK,bPct,bCol,go,setPDay,doGen};
462
  }
463
  }).mount('#app');
464
  </script>
 
7
  <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
11
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&display=swap" rel="stylesheet">
12
  <style>
13
  *{margin:0;padding:0;box-sizing:border-box}
 
103
  .gen-status.ok{color:var(--accent)}.gen-status.err{color:var(--danger)}
104
  .ld{display:flex;align-items:center;justify-content:center;height:200px;color:var(--t3);font:12px var(--mono);letter-spacing:1px}
105
  .pulse{animation:pulse 1.2s ease infinite}
106
+ .mermaid-wrap{display:flex;justify-content:center;align-items:center;padding:20px;background:var(--bg-0);border-radius:6px}.mermaid-wrap svg{max-width:100%}
107
  @keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
108
  @media(max-width:900px){
109
  .sb{width:54px}.sb-logo p,.ni span,.sb-ft,.sb-sec{display:none}
 
136
  <div class="ni" :class="{on:pg==='components'}" @click="go('components')">
137
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg><span>Components</span>
138
  </div>
139
+ <div class="sb-sec" style="margin-top:8px">Data</div>
140
+ <div class="ni" :class="{on:pg==='data'}" @click="go('data')">
141
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg><span>Schema &amp; Tables</span>
142
+ </div>
143
  <div class="sb-sec" style="margin-top:8px">Settings</div>
144
  <div class="ni" :class="{on:pg==='generate'}" @click="go('generate')">
145
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg><span>Generate Data</span>
 
249
  <div class="pc"><div class="cd"><div class="cd-b" style="padding:0;max-height:calc(100vh - 130px);overflow-y:auto"><table class="tb"><thead><tr><th>Component</th><th>Voltage</th><th>Amps</th><th>Power</th><th>Water</th><th>Idle</th></tr></thead><tbody><template v-for="(items,cat) in comps" :key="cat"><tr><td colspan="6" class="tb-cat">{{cat}}</td></tr><tr v-for="(c,i) in items" :key="i"><td style="color:var(--t1)">{{c.name}}</td><td>{{c.voltage_v}}V</td><td>{{c.avg_amps}}A</td><td style="color:var(--solar)">{{(c.voltage_v*c.avg_amps).toFixed(1)}}W</td><td>{{c.water_gal_per_cycle||'β€”'}}</td><td>{{c.idle_amps||'β€”'}}</td></tr></template></tbody></table></div></div></div>
250
  </template>
251
 
252
+ <!-- DATA: Schema & Data Tables -->
253
+ <template v-if="pg==='data'">
254
+ <div class="ph"><div><h2>Data Schema &amp; Tables</h2><div class="sub">CSV structure and raw data tables</div></div></div>
255
+ <div class="pc">
256
+ <div class="dtabs">
257
+ <div class="dtab" :class="{on:dataSubTab==='schema'}" @click="dataSubTab='schema'">Schema (Mermaid)</div>
258
+ <div class="dtab" :class="{on:dataSubTab==='tables'}" @click="dataSubTab='tables'">Data Tables</div>
259
+ </div>
260
+ <template v-if="dataSubTab==='schema'">
261
+ <div class="cd" style="margin-bottom:14px"><div class="cd-h"><span class="cd-t">Entity relationship (CSV schema)</span></div><div class="cd-b" style="min-height:320px"><div id="mermaid-container" class="mermaid-wrap"></div><div v-if="schemaMermaidErr" class="insight danger"><span class="insight-icon">⚠️</span><div>{{schemaMermaidErr}}</div></div></div></div>
262
+ <div class="cd"><div class="cd-h"><span class="cd-t">Table definitions</span></div><div class="cd-b" style="max-height:400px;overflow-y:auto"><table class="tb"><thead><tr><th>Table</th><th>Column</th><th>Type</th></tr></thead><tbody><template v-for="(meta,key) in schemaTables" :key="key"><tr v-for="(col,i) in (meta.columns||[])" :key="i"><td v-if="i===0" :rowspan="(meta.columns||[]).length" class="tb-cat">{{key}}</td><td>{{col.name}}</td><td style="color:var(--t3)">{{col.type}}</td></tr></template></tbody></table></div></div></div>
263
+ </template>
264
+ <template v-if="dataSubTab==='tables'">
265
+ <div class="cd" style="margin-bottom:14px"><div class="cd-h"><span class="cd-t">Select table</span></div><div class="cd-b"><div class="form-row"><select class="form-select" v-model="selectedTableId" style="max-width:280px" @change="loadTableData"><option value="">β€” Choose β€”</option><option v-for="t in dataTablesList" :key="t.id" :value="t.id">{{t.stream}} / {{t.resolution}} ({{t.row_count}} rows)</option></select><span class="cd-t" style="color:var(--t2)" v-if="selectedTableId">Limit</span><input type="number" class="form-input" v-model.number="tableLimit" min="10" max="2000" step="50" style="width:90px" @change="loadTableData"/></div></div></div>
266
+ <div class="cd"><div class="cd-b" style="padding:0;max-height:calc(100vh - 280px);overflow:auto"><table class="tb" v-if="tableData.length"><thead><tr><th v-for="col in tableColumns" :key="col">{{col}}</th></tr></thead><tbody><tr v-for="(row,i) in tableData" :key="i"><td v-for="col in tableColumns" :key="col">{{fmtCell(row[col])}}</td></tr></tbody></table><div v-else class="ld" style="min-height:120px">{{selectedTableId?'Loading...':'Select a table above.'}}</div></div></div></div>
267
+ </template>
268
+ </div>
269
+ </template>
270
+
271
  <!-- GENERATE -->
272
  <template v-if="pg==='generate'">
273
  <div class="ph"><div><h2>Generate Data</h2><div class="sub">Configure trip parameters and regenerate simulation</div></div></div>
 
329
  const pDay=ref(0),pDayInsight=ref('');
330
  const gen=reactive({user_type:'Typical',num_people:2,trip_duration_days:5,temperature:'Hot',sunlight:'Hi- Sunny',humidity:'Comfortable',seed:42});
331
  const genBusy=ref(false),genMsg=ref(''),genOk=ref(true);
332
+ const dataSubTab=ref('schema'),schemaTables=ref({}),schemaMermaidText=ref(''),schemaMermaidErr=ref(''),dataTablesList=ref([]);
333
+ const selectedTableId=ref(''),tableLimit=ref(500),tableData=ref([]),tableColumns=ref([]);
334
  let pw15=[],pwH=[],wtH=[],wtD=[],wt15=[];
335
 
336
  const pBMax=computed(()=>Math.max(...Object.values(pBudget.value||{}),1));
 
481
  genBusy.value=false;
482
  }
483
 
484
+ async function renderMermaid(){
485
+ const container=document.getElementById('mermaid-container');if(!container||!schemaMermaidText.value||typeof mermaid==='undefined')return;
486
+ container.innerHTML='';
487
+ try{
488
+ mermaid.initialize({startOnLoad:false,theme:'dark',themeVariables:{primaryColor:'#0c1118',primaryTextColor:'#e8edf4',primaryBorderColor:'#283848',lineColor:'#506070',secondaryColor:'#121a24',tertiaryColor:'#182230'}});
489
+ const id='mermaid-'+Date.now();const {svg}=await mermaid.render(id,schemaMermaidText.value);container.innerHTML=svg;schemaMermaidErr.value='';
490
+ }catch(e){schemaMermaidErr.value=e.message||'Mermaid render failed';}
491
+ }
492
+ async function loadSchema(){
493
+ try{
494
+ const r=await api('/api/schema');schemaTables.value=r.tables||{};schemaMermaidText.value=r.mermaid||'';schemaMermaidErr.value='';
495
+ await nextTick();await renderMermaid();
496
+ }catch(e){schemaMermaidErr.value=e.message||'Failed to load schema';}
497
+ }
498
+ async function loadDataTablesList(){try{const r=await api('/api/data/tables');dataTablesList.value=r.tables||[];}catch(e){dataTablesList.value=[];}}
499
+ async function loadTableData(){
500
+ if(!selectedTableId.value){tableData.value=[];tableColumns.value=[];return;}
501
+ const [stream,res]=selectedTableId.value.split('_');const limit=Math.min(2000,Math.max(10,tableLimit.value||500));
502
+ try{
503
+ const r=await api(`/api/${stream}/${res}?limit=${limit}`);const rows=r.data||[];tableData.value=rows;
504
+ tableColumns.value=rows.length?Object.keys(rows[0]):(r.columns||[]);
505
+ }catch(e){tableData.value=[];tableColumns.value=[];}
506
+ }
507
+ function fmtCell(v){if(v==null||v==='')return 'β€”';if(typeof v==='number')return Number.isInteger(v)?v:v.toFixed(4);return v;}
508
+
509
  function go(p){pg.value=p;}
510
+ watch(pg,async p=>{await nextTick();if(p==='overview')renderOV();if(p==='power')loadPW();if(p==='water')loadWT();if(p==='budget')loadBG();if(p==='data'){await loadSchema();loadDataTablesList();}});
511
+ watch(dataSubTab,async (t)=>{if(t==='schema'&&schemaMermaidText.value)await nextTick().then(()=>renderMermaid());});
512
  onMounted(async()=>{await loadAll();await nextTick();renderOV();});
513
 
514
+ return{pg,loading,cfg,S,pBudget,wBudget,comps,hProfile,whProfile,pDay,pDayInsight,gen,genBusy,genMsg,genOk,battCol,pBMax,wBMax,rd,fL,fmtK,bPct,bCol,go,setPDay,doGen,dataSubTab,schemaTables,schemaMermaidErr,dataTablesList,selectedTableId,tableLimit,tableData,tableColumns,loadTableData,fmtCell};
515
  }
516
  }).mount('#app');
517
  </script>