py/facett_demo/app.py
"""facett-demo — Streamlit entrypoint.
Run with the installed command (Streamlit is started in-process)::
facett-demo # 127.0.0.1:8501
facett-demo run --server.port 8502
or ``streamlit run <site-packages>/facett_demo/app.py``.
Mirrors facett-demo's core surfaces (MAP / GRAPH / POPULATION), each driven by a
LIVE korp-server RPC. The sidebar shows this app's version+git-hash AND the
connected server's version+hash, so it is always obvious which build backs the
demo. Every pane shows an honest "server unreachable / empty" panel rather than a
blank false-green when the server cannot answer.
"""
from __future__ import annotations
import os
from typing import Any
import streamlit as st
from facett_demo.buildinfo import build_info, version_string
from facett_demo.client import (
DEFAULT_TARGET,
KORP_EXTRA_HINT,
KorpClient,
KorpUnavailable,
korp_available,
)
from facett_demo.panes import (
gallery_pane,
map_pane,
graph_pane,
population_pane,
errcode_pane,
offline_pane,
)
def _sidebar() -> KorpClient:
st.sidebar.title("facett-demo")
st.sidebar.caption("Python/Streamlit companion to facett-demo")
bi = build_info()
st.sidebar.markdown("**App build**")
st.sidebar.code(f"{bi['version']} ({bi['git_hash']})\nsource: {bi['source']}", language=None)
target = st.sidebar.text_input("korp-server", value=DEFAULT_TARGET, key="server_target")
client = KorpClient(target=target)
st.sidebar.markdown("**Server**")
if not korp_available():
# The [korp] extra is optional: say so instead of probing a wire that is not here.
st.sidebar.info(f"korp wire not installed — offline mode\n\n`{KORP_EXTRA_HINT}`")
else:
try:
h = client.health()
st.sidebar.success(f"connected · {h.status}")
st.sidebar.code(f"korp {h.version}\nuptime {h.uptime_seconds}s", language=None)
except KorpUnavailable as e:
st.sidebar.error("server unreachable")
st.sidebar.code(str(e), language=None)
# Dev mode: every canvas shows its navgraph numbers (1, 1.1, 1.1.~3 …). The wasm
# draws them; Ctrl+Shift+G in a canvas cycles the overlay, Ctrl+G <n> Enter focuses.
# Starts ON when the app runs with FACETT_DEVID=1 (the same switch as a release
# facett binary's dev-id chips).
dev_default = os.environ.get("FACETT_DEVID", "").strip().lower() in ("1", "true", "on", "yes")
st.sidebar.toggle("dev mode · navgraph numbers", value=dev_default, key="dev_mode")
st.sidebar.caption("🎨 Komponenter (every group) → the shipped wasm (no RPC, no server)")
st.sidebar.caption("korp tabs → live RPCs:")
st.sidebar.caption("🗺 Map → Assets + MapQuery")
st.sidebar.caption("🕸 Graph → Graph")
st.sidebar.caption("📊 Population → IcebergTables + TablePreview + Population")
st.sidebar.caption("🩷 Errors → the facett error-code catalog (static, no RPC)")
return client
#: The live korp panes, each filed under the facett-demo tab group its surface belongs
#: to (`gallery_pane.GROUPS`): the map under Kartor, the graph navigator under Grafer,
#: the Iceberg browser + population diagrams under Tabeller & diagram, the error-code
#: catalog under App & UI (where the demo keeps `Errors`).
#:
#: The third field is what the tab mounts WITHOUT the korp wire (the optional
#: `[korp]` extra): the same components on the gallery's generated/fixture data,
#: under a banner that says so (`offline_pane`). `None` = the pane needs no korp.
_LIVE_PANES: dict[str, list[tuple[str, Any, list[str] | None]]] = {
"graph": [("\U0001F578 korp · Graph", graph_pane.render, [graph_pane.COMPONENT])],
"map": [("\U0001F5FA korp · Map", map_pane.render, [map_pane.COMPONENT])],
"data": [(
"\U0001F4CA korp · Population",
population_pane.render,
[population_pane.STAT_COMPONENT, population_pane.TABLE_COMPONENT],
)],
"ui": [("\U0001FA77 Errors", errcode_pane.render, None)],
}
#: The sub-tab every group opens on: the gallery's wasm components for that group.
COMPONENTS_TAB = "\U0001F3A8 Komponenter"
def main() -> None:
st.set_page_config(page_title="facett-demo", page_icon="🧭", layout="wide")
st.title("🧭 facett-demo")
st.caption(
"Grouped like facett-demo's tabs (Grafer, Kartor, …). In each group, "
"**Komponenter** is facett-demo itself: every wrapped component, every pixel drawn "
"by the Rust wasm on WebGPU, no server needed — the graph components with the "
"demo's size + generator (100 → 100k, the universe to 1M, generated in the wasm). "
"The korp tabs beside it mirror facett-demo's live surfaces from the korp-server "
"gRPC control plane. No mocks, and no JavaScript renderer anywhere."
)
client = _sidebar()
# The GALLERY leads every group, and deliberately so. It is the facett-demo showcase
# and it needs NO korp-server, no database and no network: a demo whose first tab
# is empty because a backend is down is a demo nobody can show. The live-RPC panes
# follow it inside their group.
mode = gallery_pane.render_header()
groups = gallery_pane.top_groups()
for tab, (key, _label) in zip(st.tabs([label for _, label in groups]), groups):
with tab:
live = _LIVE_PANES.get(key, [])
if not live:
gallery_pane.render_group(key, mode)
continue
subs = st.tabs([COMPONENTS_TAB] + [name for name, _, _ in live])
with subs[0]:
gallery_pane.render_group(key, mode)
for sub, (name, pane, offline) in zip(subs[1:], live):
with sub:
if offline is not None and not korp_available():
offline_pane.render(name, offline, key=key)
else:
pane(client)
st.divider()
st.caption(f"{version_string()} · server target `{os.environ.get('KORP_SERVER', DEFAULT_TARGET)}`")
# Streamlit (and AppTest) execute this module top-to-bottom on every rerun, so
# main() is called unconditionally — no __main__ guard (there is no double run).
main()