Skip to content

Commit 9115d41

Browse files
committed
Add local table picker and selection API
Expose local table management: add GET /api/local-tables and POST /api/select-local-table endpoints to list available local tables and switch the active local source (selection is blocked while a sync is running and it invalidates unsynced cache/state). Implement SyncEngine.list_local_tables to query the local DB information_schema for tables matching ^wfr[0-9]. Update the web UI to include a local table dropdown, loadLocalTables/selectLocalTable JS handlers, polling integration, and minor label tweak ("New" -> "New cloud"). Normalize the default local table name from wfr26test_base to wfr26base across config, .env.macbook and docker-compose defaults (also normalizing casing for TIMESCALE_TABLE), so the composed services and environment use the new base table name.
1 parent a4c6eed commit 9115d41

6 files changed

Lines changed: 103 additions & 9 deletions

File tree

universal-telemetry-software/cloud-sync/app.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,29 @@ class CreateTablePayload(BaseModel):
139139
table: str
140140

141141

142+
@app.get("/api/local-tables")
143+
def list_local_tables():
144+
"""List existing local tables (tables matching ^wfr[0-9] on the local DB)."""
145+
tables = engine.list_local_tables()
146+
return {"tables": tables, "current": engine.local_table}
147+
148+
149+
@app.post("/api/select-local-table")
150+
def select_local_table(payload: SelectTablePayload):
151+
"""Switch the active local source table for the next sync."""
152+
if _sync_state["running"]:
153+
raise HTTPException(status_code=409, detail="Cannot change table while sync is running")
154+
name = payload.table.lower().strip()
155+
if not name:
156+
raise HTTPException(status_code=400, detail="Table name is required")
157+
engine.local_table = name
158+
# Invalidate unsynced cache
159+
_sync_state["_unsynced_ts"] = 0.0
160+
_sync_state["_unsynced_count"] = None
161+
_sync_state["_cloud_cursor"] = None
162+
return {"selected": name}
163+
164+
142165
@app.get("/api/cloud-tables")
143166
def list_cloud_tables():
144167
"""List existing cloud tables (tables matching ^wfr[0-9] on the cloud DB)."""

universal-telemetry-software/cloud-sync/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"LOCAL_POSTGRES_DSN",
55
"postgresql://wfr:wfr_password@timescaledb:5432/wfr",
66
)
7-
LOCAL_TABLE = os.getenv("LOCAL_TABLE", "wfr26test_base").lower()
7+
LOCAL_TABLE = os.getenv("LOCAL_TABLE", "wfr26base").lower()
88

99
CLOUD_POSTGRES_DSN = os.getenv("CLOUD_POSTGRES_DSN", "")
1010
CLOUD_TABLE = os.getenv("CLOUD_TABLE", "wfr26").lower()

universal-telemetry-software/cloud-sync/static/index.html

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,17 @@ <h1>WFR Cloud Sync</h1>
285285
</div>
286286

287287
<div class="table-picker">
288+
<label>Local table:</label>
289+
<select id="local-table-select" onchange="selectLocalTable(this.value)">
290+
<option value="">— loading —</option>
291+
</select>
292+
<div class="divider"></div>
288293
<label>Cloud table:</label>
289294
<select id="table-select" onchange="selectTable(this.value)">
290295
<option value="">— loading —</option>
291296
</select>
292297
<div class="divider"></div>
293-
<label>New:</label>
298+
<label>New cloud:</label>
294299
<input type="text" id="new-table-input" placeholder="wfr27" maxlength="32"
295300
onkeydown="if(event.key==='Enter') createTable()" />
296301
<button onclick="createTable()" id="btn-create">Create</button>
@@ -509,7 +514,55 @@ <h1>WFR Cloud Sync</h1>
509514
}
510515
}
511516

512-
// ── Table picker ───────────────────────────────────────────────────────────
517+
// ── Local table picker ─────────────────────────────────────────────────────
518+
519+
async function loadLocalTables() {
520+
try {
521+
const res = await fetch('/api/local-tables');
522+
if (!res.ok) return;
523+
const data = await res.json();
524+
const sel = document.getElementById('local-table-select');
525+
const current = data.current;
526+
sel.innerHTML = '';
527+
if (data.tables.length === 0) {
528+
sel.innerHTML = '<option value="">— no tables found —</option>';
529+
} else {
530+
data.tables.forEach(t => {
531+
const opt = document.createElement('option');
532+
opt.value = t;
533+
opt.textContent = t;
534+
if (t === current) opt.selected = true;
535+
sel.appendChild(opt);
536+
});
537+
if (!data.tables.includes(current)) {
538+
const opt = document.createElement('option');
539+
opt.value = current;
540+
opt.textContent = current + ' (env)';
541+
opt.selected = true;
542+
sel.prepend(opt);
543+
}
544+
}
545+
} catch (e) {
546+
// local DB not reachable yet
547+
}
548+
}
549+
550+
async function selectLocalTable(table) {
551+
if (!table) return;
552+
try {
553+
await fetch('/api/select-local-table', {
554+
method: 'POST',
555+
headers: { 'Content-Type': 'application/json' },
556+
body: JSON.stringify({ table }),
557+
});
558+
addLog(`Local table switched to: ${table}`, '');
559+
refreshStatus();
560+
} catch (e) {
561+
addLog(`Failed to select local table: ${e}`, 'err');
562+
}
563+
}
564+
565+
// ── Cloud table picker ─────────────────────────────────────────────────────
513566

514567
async function loadCloudTables() {
515568
try {
@@ -593,8 +646,10 @@ <h1>WFR Cloud Sync</h1>
593646
// ── Init ───────────────────────────────────────────────────────────────────
594647

595648
refreshStatus();
649+
loadLocalTables();
596650
loadCloudTables();
597651
idlePollInterval = setInterval(refreshStatus, 10000);
652+
setInterval(loadLocalTables, 30000);
598653
setInterval(loadCloudTables, 30000);
599654
</script>
600655
</body>

universal-telemetry-software/cloud-sync/sync.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ def get_unsynced_count(self, cursor: Optional[datetime]) -> int:
7979
row = cur.fetchone()
8080
return row[0] if row else 0
8181

82+
def list_local_tables(self) -> list:
83+
"""List existing tables on the local DB matching ^wfr[0-9]."""
84+
try:
85+
with self._local_conn() as conn:
86+
with conn.cursor() as cur:
87+
cur.execute("""
88+
SELECT table_name
89+
FROM information_schema.tables
90+
WHERE table_schema = 'public'
91+
AND table_type = 'BASE TABLE'
92+
AND table_name ~ '^wfr[0-9]'
93+
ORDER BY table_name DESC
94+
""")
95+
return [r[0] for r in cur.fetchall()]
96+
except Exception as e:
97+
logger.warning(f"list_local_tables failed: {e}")
98+
return []
99+
82100
def list_cloud_tables(self) -> list:
83101
"""List existing tables on the cloud DB matching ^wfr[0-9]. No DBC needed."""
84102
if not self.cloud_dsn:

universal-telemetry-software/deploy/.env.macbook

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Usage: docker compose -f deploy/docker-compose.macbook-base.yml --env-file deploy/.env.macbook up -d
33

44
REMOTE_IP=10.71.1.10
5-
TIMESCALE_TABLE=WFR26test
5+
TIMESCALE_TABLE=wfr26base
66
DBC_HOST_PATH=../../secret-dbc/WFR25.dbc
77
DBC_DISPLAY_NAME=WFR25.dbc
88
GRAFANA_ADMIN_PASSWORD=admin
@@ -12,6 +12,4 @@ GRAFANA_ADMIN_PASSWORD=admin
1212
# Example: postgresql://wfr:password@your-vps-ip:5432/wfr
1313
CLOUD_POSTGRES_DSN=postgresql://wfr:wfr_password@100.72.11.60:5432/wfr
1414
CLOUD_TABLE=wfr26
15-
# LOCAL_TABLE overrides the TIMESCALE_TABLE+_base compose logic.
16-
# Set this explicitly if your local volume already has tables without the _base suffix.
17-
LOCAL_TABLE=wfr26
15+
LOCAL_TABLE=wfr26base

universal-telemetry-software/deploy/docker-compose.macbook-base.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ services:
3939
- SET_TIME_ENABLED=false
4040
- ENABLE_TIMESCALE_LOGGING=true
4141
- POSTGRES_DSN=postgresql://wfr:wfr_password@timescaledb:5432/wfr
42-
- TIMESCALE_TABLE=${TIMESCALE_TABLE:-WFR26test}_base
42+
- TIMESCALE_TABLE=${TIMESCALE_TABLE:-wfr26base}
4343
- DBC_FILE_PATH=/app/active.dbc
4444
- DBC_DISPLAY_NAME=${DBC_DISPLAY_NAME:-example.dbc}
4545
volumes:
@@ -131,7 +131,7 @@ services:
131131
- "8092:8092"
132132
environment:
133133
- LOCAL_POSTGRES_DSN=postgresql://wfr:wfr_password@timescaledb:5432/wfr
134-
- LOCAL_TABLE=${LOCAL_TABLE:-${TIMESCALE_TABLE:-WFR26test}_base}
134+
- LOCAL_TABLE=${LOCAL_TABLE:-${TIMESCALE_TABLE:-wfr26base}}
135135
- CLOUD_POSTGRES_DSN=${CLOUD_POSTGRES_DSN:-}
136136
- CLOUD_TABLE=${CLOUD_TABLE:-wfr26}
137137
- SYNC_BATCH_SIZE=${SYNC_BATCH_SIZE:-5000}

0 commit comments

Comments
 (0)