Spaces:
Sleeping
Sleeping
Upload 2 files
Browse files- main.py +124 -3
- 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 & 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 & 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>
|