Skip to content

Commit 87791bf

Browse files
refactor: finalize tutorial download workflow and datasette improvements
1 parent 3239a43 commit 87791bf

2 files changed

Lines changed: 111 additions & 25 deletions

File tree

frontend/src/App.jsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,35 @@ function App() {
6060
const resp = await fetch(`${API_BASE}/api/config`);
6161
if (resp.ok) {
6262
const data = await resp.json();
63-
// If we don't have a path yet or it's the default, update it
64-
if (
65-
data.tutorial_database &&
66-
(!config.input_database || config.input_database.includes("tutorial"))
67-
) {
63+
// Update ports if needed, but DO NOT overwrite input_database automatically
64+
setConfig((prev) => ({
65+
...prev,
66+
explorer_port: data.explorer_port || 8001,
67+
}));
68+
}
69+
} catch (e) {
70+
console.error("Failed to fetch config", e);
71+
}
72+
};
73+
74+
const downloadTutorial = async () => {
75+
try {
76+
const resp = await fetch(`${API_BASE}/api/download_tutorial`, { method: "POST" });
77+
if (resp.ok) {
78+
const data = await resp.json();
79+
if (data.path) {
6880
setConfig((prev) => ({
6981
...prev,
70-
input_database: data.tutorial_database,
71-
explorer_port: data.explorer_port || 8001,
82+
input_database: data.path,
7283
}));
84+
// Also refresh files to show the assets folder if needed
85+
fetchFiles(currentPath);
7386
}
87+
} else {
88+
alert("Failed to download tutorial data.");
7489
}
7590
} catch (e) {
76-
console.error("Failed to fetch config", e);
91+
console.error("Error downloading tutorial:", e);
7792
}
7893
};
7994

@@ -314,6 +329,7 @@ function App() {
314329
value={config.input_database}
315330
onChange={handleChange}
316331
style={{ flex: 1 }}
332+
placeholder="/path/to/database.sqlite"
317333
/>
318334
<button
319335
onClick={() => {
@@ -324,6 +340,13 @@ function App() {
324340
>
325341
Browse
326342
</button>
343+
<button
344+
onClick={downloadTutorial}
345+
className="action-btn secondary"
346+
title="Download and use sample data"
347+
>
348+
Use Tutorial
349+
</button>
327350
</div>
328351
</div>
329352
<div>

temoa_runner.py

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ def ensure_assets():
132132
@app.get("/api/config")
133133
def get_config():
134134
"""Return key configuration paths and settings for the frontend."""
135-
ensure_assets()
136135
# In the runner, we assume assets are in the current directory or nearby
137136
tutorial_db = Path("assets/tutorial_database.sqlite")
138137
return {
@@ -144,6 +143,18 @@ def get_config():
144143
}
145144

146145

146+
@app.post("/api/download_tutorial")
147+
def download_tutorial():
148+
"""Explicitly download tutorial assets."""
149+
ensure_assets()
150+
tutorial_db = Path("assets/tutorial_database.sqlite")
151+
if tutorial_db.exists():
152+
# Ensure it's served
153+
start_datasette(str(tutorial_db.absolute()))
154+
return {"path": str(tutorial_db.absolute())}
155+
raise HTTPException(status_code=500, detail="Failed to download tutorial assets")
156+
157+
147158
@app.get("/api/files")
148159
def list_files(path: str = "."):
149160
"""Helper to browse files for input database selection."""
@@ -324,6 +335,16 @@ def flush(self):
324335
f"Output Database target: {run_toml['output_database']}"
325336
)
326337

338+
# Ensure this output database is being served by Datasette
339+
# If it's outside our known list, restart datasette to include it.
340+
# We do this before starting the run so the user can see it appear or update.
341+
try:
342+
target_db = run_toml["output_database"]
343+
if target_db:
344+
start_datasette(str(Path(target_db).absolute()))
345+
except Exception as e:
346+
print(f"Failed to refresh datasette: {e}")
347+
327348
config_path = output_dir / "run_config.toml"
328349
with open(config_path, "w", encoding="utf-8") as f:
329350
f.write(tomlkit.dumps(run_toml))
@@ -378,43 +399,78 @@ async def websocket_endpoint(websocket: WebSocket):
378399
manager.disconnect(websocket)
379400

380401

381-
if __name__ == "__main__":
382-
import uvicorn
402+
# --- Datasette Management ---
403+
DATASETTE_PROCESS = None
404+
SERVED_DATABASES = set()
405+
406+
407+
def start_datasette(new_db: Optional[str] = None):
408+
"""
409+
Start or restart Datasette serving the tutorial DB + output DBs.
410+
If new_db is provided and not already served, restart the process to include it.
411+
"""
412+
global DATASETTE_PROCESS, SERVED_DATABASES
383413
import subprocess
384414
import os
415+
import sys
385416

386-
ensure_assets()
417+
# If new_db is already served, no need to restart
418+
if new_db:
419+
abs_new = str(Path(new_db).resolve())
420+
if abs_new in SERVED_DATABASES:
421+
return
422+
SERVED_DATABASES.add(abs_new)
423+
424+
# Kill existing process if running
425+
if DATASETTE_PROCESS:
426+
try:
427+
DATASETTE_PROCESS.terminate()
428+
DATASETTE_PROCESS.wait(timeout=5)
429+
except Exception:
430+
try:
431+
DATASETTE_PROCESS.kill()
432+
except Exception:
433+
pass
434+
435+
print("Starting/Restarting Datasette on port 8001...", flush=True)
387436

388-
# Start Datasette in a subprocess
389-
print("Starting Datasette on port 8001...", flush=True)
390437
try:
391438
log_file = open("datasette.log", "a")
392-
393-
# Create output directory if missing
394439
output_base = Path("output")
395440
output_base.mkdir(exist_ok=True)
396441

442+
# Baseline: Tutorial DB + All found in output directory
397443
tutorial_db = Path("assets/tutorial_database.sqlite")
444+
current_serve_list = []
398445

399-
# Discover all sqlite files in the output directory recursively
400-
to_serve = [str(p.absolute()) for p in output_base.rglob("*.sqlite")]
446+
# 1. Add recursive output/ files
447+
for p in output_base.rglob("*.sqlite"):
448+
current_serve_list.append(str(p.absolute()))
449+
450+
# 2. Add tutorial DB
401451
if tutorial_db.exists():
402-
to_serve.append(str(tutorial_db.absolute()))
452+
current_serve_list.append(str(tutorial_db.absolute()))
453+
454+
# 3. Add any globally tracked external databases (like the new input/output one)
455+
# Merge them in to ensure we serve what we've seen so far.
456+
for db_path in SERVED_DATABASES:
457+
if db_path not in current_serve_list:
458+
current_serve_list.append(db_path)
459+
460+
# Deduplicate just in case
461+
final_serve_list = list(set(current_serve_list))
403462

404-
# Isolate Datasette from host configs (like ~/.datasette/config.json)
405-
# by pointing HOME to a known clean location.
463+
# Isolate Datasette environment
406464
env = os.environ.copy()
407465
env["HOME"] = str(Path(".").absolute())
408466

409-
# Use sys.executable -m datasette to ensure we run in the same environment.
410-
# We serve ONLY the explicit database files found.
411-
subprocess.Popen(
467+
DATASETTE_PROCESS = subprocess.Popen(
412468
[
413469
sys.executable,
414470
"-m",
415471
"datasette",
416472
"serve",
417-
*to_serve,
473+
*final_serve_list,
418474
"--port",
419475
"8001",
420476
"--host",
@@ -430,7 +486,14 @@ async def websocket_endpoint(websocket: WebSocket):
430486
print(
431487
"Datasette process launched. (Logs available in datasette.log)", flush=True
432488
)
489+
433490
except Exception as e:
434491
print(f"Warning: Could not start Datasette: {e}", flush=True)
435492

493+
494+
if __name__ == "__main__":
495+
import uvicorn
496+
497+
start_datasette() # Initial start
498+
436499
uvicorn.run(app, host="0.0.0.0", port=8000)

0 commit comments

Comments
 (0)