KyklosIO Claude Opus 5 commited on
Commit
1a47714
·
1 Parent(s): 7719c3b

feat(city): the world model as a place you can walk

Browse files

/ui/world already answered what the mesh is — 72 grid nodes, the frequencies on
them, the provenance edges that fed them, and a coverage block saying which of
those facts are observed and which are not modeled at all. All true, and
unwalkable. You asked to explore the city, not inspect its JSON.

grid node -> district (all 72, including the ones carrying nothing)
frequency -> building (its height is its signal count, nothing else)
signal freshness -> its lights (fresh/dim/dark, from world_state's own
age_s / interval_s — not invented here)
provenance edge -> a road in (from the origin that actually fed it)
agent -> a resident

core/city_skin.py adds no facts to the world model. The rule that keeps it a
map instead of a mockup: an aspect marked not_modeled is drawn as unavailable,
never as empty and never as full. residency and lease are not modeled, so the
city has no tenancy and says so on the page, struck through, in the world
model's own words.

Four skins over one projection, not four datasets: Civic, Radio, Organism,
Skinless. Skinless is the control — no metaphor, the model's own vocabulary —
and the plurals are spelled out because "frequencys" and "72 nucleus" read as a
mockup too.

The skyline is scaled once for the whole city rather than per district, or a
lone 2-signal building draws exactly as tall as a 100-signal one and the shape
says nothing. Projection is cached on the world's generated_at: same world,
same city, no rebuild.

Routes: /ui/city (the map), /ui/city.json (the projection), /ui/skins (what the
skins are and what they refuse to draw).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGo2kuduWZMmEZGwKVNryU

Files changed (2) hide show
  1. core/city_skin.py +289 -0
  2. signalmesh_api.py +248 -0
core/city_skin.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ core/city_skin.py — the world model, laid out as a place you can walk.
3
+
4
+ /ui/world already answers what the mesh is: 72 grid nodes, the frequencies
5
+ sitting on them, the provenance edges that fed them, and — the part that
6
+ matters — a coverage block saying which of those facts are *observed* and which
7
+ are *not modeled at all*. That JSON is true and unwalkable.
8
+
9
+ This turns it into a city without adding a single fact to it:
10
+
11
+ grid node → district (all 72, including the empty ones)
12
+ frequency → building (its size is its signal count, nothing else)
13
+ signal freshness → its lights (fresh / dim / dark, from world_state's own
14
+ age_s ÷ interval_s — not invented here)
15
+ provenance edge → a road in (from the origin that actually fed it)
16
+ agent → a resident
17
+
18
+ And the rule that makes it honest: an aspect the world model marks
19
+ `not_modeled` is drawn as unavailable, never as empty and never as full.
20
+ `residency` and `lease` are not modeled, so this city has no residents-per-
21
+ building count and no tenancy — because inventing one is how a map starts lying.
22
+
23
+ Skins are four readings of this one projection, not four datasets.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import time
28
+ from typing import Any, Dict, List, Optional, Tuple
29
+
30
+ SCHEMA = "signalmesh.city-skin/v1"
31
+
32
+ ROWS, COLS = 9, 8
33
+ GRID_CELLS = ROWS * COLS # 72, same grid as /api/grid
34
+ ROW_LETTERS = "ALIODQBGU" # the dashboard's own row naming
35
+
36
+ # Plurals are spelled out, not derived: "frequency" + "s" is "frequencys", and
37
+ # more than one nucleus is nuclei. A map that misspells its own legend reads as
38
+ # a mockup.
39
+ SKINS = {
40
+ "civic": {
41
+ "label": "Civic",
42
+ "district": "district", "building": "building",
43
+ "road": "road", "resident": "resident",
44
+ "plural": {"district": "districts", "building": "buildings",
45
+ "road": "roads", "resident": "residents"},
46
+ "blurb": "Districts and buildings. Lit windows are fresh signals.",
47
+ },
48
+ "radio": {
49
+ "label": "Radio",
50
+ "district": "band", "building": "station",
51
+ "road": "feed", "resident": "listener",
52
+ "plural": {"district": "bands", "building": "stations",
53
+ "road": "feeds", "resident": "listeners"},
54
+ "blurb": "Bands and stations. Loud stations are carrying the most.",
55
+ },
56
+ "organism": {
57
+ "label": "Organism",
58
+ "district": "tissue", "building": "cell",
59
+ "road": "vessel", "resident": "nucleus",
60
+ "plural": {"district": "tissues", "building": "cells",
61
+ "road": "vessels", "resident": "nuclei"},
62
+ "blurb": "Tissues and cells. A dark cell has stopped metabolising.",
63
+ },
64
+ "skinless": {
65
+ "label": "Skinless",
66
+ "district": "grid node", "building": "frequency",
67
+ "road": "provenance edge", "resident": "agent",
68
+ "plural": {"district": "grid nodes", "building": "frequencies",
69
+ "road": "provenance edges", "resident": "agents"},
70
+ "blurb": "No metaphor. The world model's own words.",
71
+ },
72
+ }
73
+
74
+
75
+ def coord_label(index: int) -> str:
76
+ """Grid index → the dashboard's coordinate, e.g. 52 → 'B5'."""
77
+ row, col = divmod(index, COLS)
78
+ return f"{ROW_LETTERS[row % len(ROW_LETTERS)]}{col + 1}"
79
+
80
+
81
+ def _health(entry: dict) -> str:
82
+ state = ((entry or {}).get("health") or {}).get("state")
83
+ return state if state in ("fresh", "dim", "dark") else "unknown"
84
+
85
+
86
+ def _district_state(buildings: List[dict]) -> str:
87
+ """A district is as alive as its liveliest building, and empty if it has none.
88
+
89
+ Deliberately not an average: one fresh frequency on a node means that node
90
+ is receiving, and averaging it against dark neighbours would hide that.
91
+ """
92
+ if not buildings:
93
+ return "empty"
94
+ states = {b["state"] for b in buildings}
95
+ for s in ("fresh", "dim", "dark"):
96
+ if s in states:
97
+ return s
98
+ return "unknown"
99
+
100
+
101
+ def build(world: Dict[str, Any]) -> Dict[str, Any]:
102
+ """Project a world-state document into a walkable city. Adds no facts."""
103
+ frequencies = world.get("frequencies") or []
104
+ nodes = world.get("nodes") or []
105
+ edges = world.get("provenance_edges") or []
106
+ coverage = world.get("coverage") or {}
107
+
108
+ # Roads arrive at a frequency; the frequency knows its district.
109
+ node_of: Dict[str, int] = {f.get("frequency"): f.get("node")
110
+ for f in frequencies if f.get("frequency") is not None}
111
+
112
+ districts: List[Dict[str, Any]] = [{
113
+ "index": i, "coord": coord_label(i),
114
+ "row": i // COLS, "col": i % COLS,
115
+ "buildings": [], "residents": [], "roads": [],
116
+ "signal_count": 0,
117
+ } for i in range(GRID_CELLS)]
118
+
119
+ for f in frequencies:
120
+ idx = f.get("node")
121
+ if not isinstance(idx, int) or not 0 <= idx < GRID_CELLS:
122
+ continue
123
+ count = int(f.get("signal_count") or 0)
124
+ districts[idx]["buildings"].append({
125
+ "name": f.get("frequency"),
126
+ "signals": count,
127
+ "state": _health(f),
128
+ "age_s": f.get("latest_age_s"),
129
+ "kind": f.get("latest_type") or "",
130
+ "source": f.get("source") or "",
131
+ })
132
+ districts[idx]["signal_count"] += count
133
+
134
+ for n in nodes:
135
+ if n.get("role") != "agent":
136
+ continue
137
+ idx = n.get("node")
138
+ if isinstance(idx, int) and 0 <= idx < GRID_CELLS:
139
+ districts[idx]["residents"].append({
140
+ "id": n.get("id"), "state": _health(n),
141
+ "source": n.get("source") or "",
142
+ })
143
+
144
+ inbound = 0
145
+ for e in edges:
146
+ idx = node_of.get(e.get("to"))
147
+ if not isinstance(idx, int) or not 0 <= idx < GRID_CELLS:
148
+ continue
149
+ districts[idx]["roads"].append({
150
+ "from": e.get("from"), "to": e.get("to"), "kind": e.get("kind") or "",
151
+ "evidence": e.get("evidence") or {},
152
+ })
153
+ inbound += 1
154
+
155
+ for d in districts:
156
+ d["buildings"].sort(key=lambda b: (-b["signals"], str(b["name"])))
157
+ d["state"] = _district_state(d["buildings"])
158
+
159
+ # The honesty panel: what this city is allowed to draw, in the world
160
+ # model's own words. `not_modeled` is shown as unavailable, not as zero.
161
+ legend = {
162
+ aspect: {"availability": (info or {}).get("availability", "unknown"),
163
+ "drawable": (info or {}).get("availability") in ("native", "derived"),
164
+ "note": (info or {}).get("caveat") or (info or {}).get("note") or ""}
165
+ for aspect, info in coverage.items()
166
+ }
167
+ undrawable = sorted(a for a, i in legend.items() if not i["drawable"])
168
+
169
+ lit = sum(1 for d in districts if d["state"] in ("fresh", "dim"))
170
+ return {
171
+ "schema": SCHEMA,
172
+ "built_at": time.time(),
173
+ "world_at": world.get("generated_at"),
174
+ "world_schema": world.get("schema"),
175
+ "grid": {"rows": ROWS, "cols": COLS, "cells": GRID_CELLS},
176
+ "districts": districts,
177
+ "skins": SKINS,
178
+ "legend": legend,
179
+ "not_drawable": undrawable,
180
+ "totals": {
181
+ "districts_total": GRID_CELLS,
182
+ "districts_occupied": sum(1 for d in districts if d["buildings"]),
183
+ "districts_lit": lit,
184
+ "buildings": sum(len(d["buildings"]) for d in districts),
185
+ "residents": sum(len(d["residents"]) for d in districts),
186
+ "roads": inbound,
187
+ "roads_unplaced": len(edges) - inbound,
188
+ "signals": sum(d["signal_count"] for d in districts),
189
+ },
190
+ }
191
+
192
+
193
+ # ── cache ────────────────────────────────────────────────────────────────────
194
+ # The projection is pure, so it only has to be redone when the world changes.
195
+ # Keyed on the world's own generated_at: a fresh world is a cache miss, the
196
+ # same world served twice is not.
197
+
198
+ _CACHE: Dict[str, Any] = {}
199
+
200
+
201
+ def build_cached(world: Dict[str, Any]) -> Dict[str, Any]:
202
+ stamp = world.get("generated_at")
203
+ if _CACHE.get("stamp") == stamp and stamp is not None:
204
+ city = dict(_CACHE["city"])
205
+ city["cached"] = True
206
+ return city
207
+ city = build(world)
208
+ city["cached"] = False
209
+ _CACHE.update(stamp=stamp, city=city)
210
+ return city
211
+
212
+
213
+ if __name__ == "__main__":
214
+ assert coord_label(0) == "A1" and coord_label(7) == "A8"
215
+ assert coord_label(52) == "B5", coord_label(52) # 52 = row 6, col 4 → 'B','5'
216
+ assert coord_label(71) == "U8"
217
+
218
+ world = {
219
+ "schema": "signalmesh.world-state/v1",
220
+ "generated_at": 1000.0,
221
+ "coverage": {
222
+ "frequency": {"availability": "native"},
223
+ "route": {"availability": "derived"},
224
+ "residency": {"availability": "not_modeled"},
225
+ "lease": {"availability": "not_modeled"},
226
+ },
227
+ "frequencies": [
228
+ {"frequency": "hn_top", "signal_count": 30, "node": 3,
229
+ "latest_age_s": 10.0, "latest_type": "feed_hydrator",
230
+ "health": {"state": "fresh"}, "source": "/ui/frequencies"},
231
+ {"frequency": "old_news", "signal_count": 2, "node": 3,
232
+ "latest_age_s": 99999.0, "health": {"state": "dark"},
233
+ "source": "/ui/frequencies"},
234
+ {"frequency": "offgrid", "signal_count": 5, "node": 999,
235
+ "health": {"state": "fresh"}},
236
+ ],
237
+ "nodes": [
238
+ {"id": "hn_top", "role": "frequency", "node": 3},
239
+ {"id": "A1", "role": "agent", "node": 3, "health": {"state": "fresh"}},
240
+ ],
241
+ "provenance_edges": [
242
+ {"from": "news.ycombinator.com", "to": "hn_top", "kind": "feed_origin",
243
+ "evidence": {"observed_items": 37}},
244
+ {"from": "nowhere", "to": "not_a_frequency", "kind": "feed_origin"},
245
+ ],
246
+ }
247
+
248
+ city = build(world)
249
+ assert len(city["districts"]) == 72, "every grid node is a district, empty or not"
250
+
251
+ d3 = city["districts"][3]
252
+ assert [b["name"] for b in d3["buildings"]] == ["hn_top", "old_news"], d3["buildings"]
253
+ assert d3["signal_count"] == 32 and d3["coord"] == "A4"
254
+ # one fresh building keeps the district alive even beside a dark one
255
+ assert d3["state"] == "fresh", d3["state"]
256
+ assert [r["id"] for r in d3["residents"]] == ["A1"]
257
+ assert len(d3["roads"]) == 1 and d3["roads"][0]["from"] == "news.ycombinator.com"
258
+
259
+ assert city["districts"][0]["state"] == "empty" and not city["districts"][0]["buildings"]
260
+
261
+ t = city["totals"]
262
+ assert t["buildings"] == 2, "a frequency off the grid is not placed on it"
263
+ assert t["roads"] == 1 and t["roads_unplaced"] == 1, t
264
+ assert t["districts_occupied"] == 1 and t["districts_lit"] == 1
265
+ assert t["signals"] == 32 and t["residents"] == 1
266
+
267
+ # the honesty rule: what the world does not model, the city must not draw
268
+ assert city["not_drawable"] == ["lease", "residency"], city["not_drawable"]
269
+ assert city["legend"]["frequency"]["drawable"] is True
270
+ assert city["legend"]["residency"]["drawable"] is False
271
+
272
+ assert build_cached(world)["cached"] is False
273
+ assert build_cached(world)["cached"] is True, "same world must not be rebuilt"
274
+ moved = dict(world, generated_at=2000.0)
275
+ assert build_cached(moved)["cached"] is False, "a new world is a new city"
276
+
277
+ # every skin must name all four things, singular and plural
278
+ for name, skin in SKINS.items():
279
+ for role in ("district", "building", "road", "resident"):
280
+ assert skin.get(role), (name, role)
281
+ assert skin["plural"].get(role), (name, role, "plural")
282
+ assert SKINS["skinless"]["plural"]["building"] == "frequencies"
283
+ assert SKINS["organism"]["plural"]["resident"] == "nuclei"
284
+
285
+ empty = build({})
286
+ assert len(empty["districts"]) == 72 and empty["totals"]["buildings"] == 0
287
+ assert all(d["state"] == "empty" for d in empty["districts"])
288
+
289
+ print("city_skin self-check OK")
signalmesh_api.py CHANGED
@@ -42,6 +42,7 @@ from core.source_hydrator import source_hydrator, is_fetchable, resolve as resol
42
  from core import mport_articles
43
  from core import binder
44
  from core import world_state
 
45
  from core import broadcast_journal
46
  from core.antenna import AntennaForge, TARGETS as ANTENNA_TARGETS
47
  from core.manifest_parser import manifest_registry, ManifestParser
@@ -904,6 +905,253 @@ def ui_world_state():
904
  """
905
  return _do_world_state()
906
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  @app.get("/identity", tags=["Core"], summary="Which copy of SignalMesh is canonical")
908
  def public_identity():
909
  """Keyless. The centrally adjudicated record for SignalMesh itself.
 
42
  from core import mport_articles
43
  from core import binder
44
  from core import world_state
45
+ from core import city_skin
46
  from core import broadcast_journal
47
  from core.antenna import AntennaForge, TARGETS as ANTENNA_TARGETS
48
  from core.manifest_parser import manifest_registry, ManifestParser
 
905
  """
906
  return _do_world_state()
907
 
908
+
909
+ _CITY_HTML = """<!DOCTYPE html><html lang="en" data-skin="civic"><head>
910
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
911
+ <title>SignalMesh &middot; The City</title>
912
+ <style>
913
+ *{margin:0;padding:0;box-sizing:border-box}
914
+ :root{
915
+ --bg:#070a10; --panel:#0d1219; --line:#1e2836; --ink:#e8eef7; --dim:#7f8 da0;
916
+ --dim:#7d8ba0; --lit:#ffce6b; --lit2:#ff9d3c; --dark:#243040; --road:#2b3a52;
917
+ --accent:#ffce6b; --glow:rgba(255,206,107,.30); --tile:#101823; --grid-gap:6px;
918
+ --radius:4px;
919
+ }
920
+ html[data-skin=radio]{--lit:#5df2a0;--lit2:#22c98a;--accent:#5df2a0;
921
+ --glow:rgba(93,242,160,.28);--tile:#0a1512;--road:#1d4438;--radius:2px}
922
+ html[data-skin=organism]{--lit:#7ad8ff;--lit2:#3aa8e0;--accent:#7ad8ff;
923
+ --glow:rgba(122,216,255,.28);--tile:#0a1520;--road:#1c3a52;--radius:50%}
924
+ html[data-skin=skinless]{--lit:#cbd5e1;--lit2:#94a3b8;--accent:#cbd5e1;
925
+ --glow:transparent;--tile:#0e131a;--road:#2a3340;--radius:0}
926
+ body{background:var(--bg);color:var(--ink);
927
+ font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
928
+ min-height:100vh;display:flex;flex-direction:column}
929
+ a{color:var(--accent)}
930
+ header{display:flex;gap:14px;align-items:center;flex-wrap:wrap;
931
+ padding:12px 18px;border-bottom:1px solid var(--line);background:var(--panel)}
932
+ h1{font-size:15px;font-weight:650;letter-spacing:.02em}
933
+ h1 small{display:block;font-size:10.5px;color:var(--dim);font-weight:400;letter-spacing:.06em;
934
+ text-transform:uppercase}
935
+ .skins{display:flex;gap:5px;margin-left:auto;flex-wrap:wrap}
936
+ .skins button{background:transparent;border:1px solid var(--line);color:var(--dim);
937
+ padding:6px 13px;border-radius:99px;font-size:12px;cursor:pointer;font-family:inherit}
938
+ .skins button[aria-pressed=true]{border-color:var(--accent);color:var(--accent);
939
+ box-shadow:0 0 14px var(--glow)}
940
+ .blurb{width:100%;font-size:11.5px;color:var(--dim);order:9}
941
+ .totals{display:flex;gap:16px;flex-wrap:wrap;padding:9px 18px;
942
+ border-bottom:1px solid var(--line);font-size:11px;color:var(--dim)}
943
+ .totals b{color:var(--ink);font-weight:600}
944
+ main{flex:1;display:grid;grid-template-columns:minmax(340px,1.15fr) minmax(300px,.85fr);
945
+ gap:0;align-items:start}
946
+ @media(max-width:880px){main{grid-template-columns:1fr}}
947
+ #map{padding:18px}
948
+ .grid{display:grid;grid-template-columns:repeat(8,1fr);gap:var(--grid-gap)}
949
+ .district{background:var(--tile);border:1px solid var(--line);border-radius:var(--radius);
950
+ aspect-ratio:1;padding:5px;cursor:pointer;position:relative;overflow:hidden;
951
+ display:flex;flex-direction:column;justify-content:flex-end;gap:2px;
952
+ transition:transform .12s,border-color .12s,box-shadow .12s}
953
+ .district:hover{transform:translateY(-2px);border-color:var(--accent)}
954
+ .district[aria-current=true]{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent),0 0 18px var(--glow)}
955
+ .district .coord{position:absolute;top:4px;left:5px;font-size:8.5px;color:var(--dim);
956
+ letter-spacing:.05em;font-family:ui-monospace,monospace}
957
+ .district.empty{opacity:.42}
958
+ .bars{display:flex;align-items:flex-end;gap:2px;height:100%;padding-top:12px}
959
+ .bar{flex:1;min-width:2px;background:var(--dark);border-radius:1px 1px 0 0}
960
+ .bar.fresh{background:linear-gradient(180deg,var(--lit),var(--lit2));box-shadow:0 0 7px var(--glow)}
961
+ .bar.dim{background:var(--lit2);opacity:.5}
962
+ .bar.dark{background:var(--dark)}
963
+ html[data-skin=organism] .bar{border-radius:99px}
964
+ #side{padding:18px 18px 18px 0;position:sticky;top:0;max-height:100vh;overflow:auto}
965
+ @media(max-width:880px){#side{padding:0 18px 18px;position:static;max-height:none}}
966
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;
967
+ padding:14px;margin-bottom:12px}
968
+ .card h2{font-size:12px;letter-spacing:.07em;text-transform:uppercase;color:var(--dim);
969
+ font-weight:600;margin-bottom:9px}
970
+ .bldg{display:flex;align-items:baseline;gap:8px;padding:6px 0;border-bottom:1px solid var(--line);
971
+ font-size:12.5px}
972
+ .bldg:last-child{border-bottom:0}
973
+ .bldg .nm{flex:1;font-family:ui-monospace,monospace;overflow:hidden;text-overflow:ellipsis;
974
+ white-space:nowrap}
975
+ .bldg .ct{color:var(--dim);font-size:11px}
976
+ .dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dark)}
977
+ .dot.fresh{background:var(--lit);box-shadow:0 0 7px var(--glow)}
978
+ .dot.dim{background:var(--lit2);opacity:.6}
979
+ .road{font-size:11.5px;color:var(--dim);padding:4px 0;font-family:ui-monospace,monospace;
980
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
981
+ .road b{color:var(--ink);font-weight:500}
982
+ .pill{display:inline-block;font-size:10px;padding:1px 7px;border-radius:99px;
983
+ border:1px solid var(--line);color:var(--dim);margin:2px 3px 2px 0}
984
+ .unavail{opacity:.55;text-decoration:line-through}
985
+ .empty-note{color:var(--dim);font-size:12px;font-style:italic}
986
+ .legend-row{display:flex;justify-content:space-between;gap:10px;font-size:11.5px;
987
+ padding:3px 0;border-bottom:1px solid var(--line)}
988
+ .legend-row:last-child{border-bottom:0}
989
+ .legend-row .v{color:var(--dim);font-family:ui-monospace,monospace;font-size:10.5px}
990
+ .key{display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:var(--dim);margin-top:10px}
991
+ .key span{display:flex;align-items:center;gap:5px}
992
+ </style></head><body>
993
+ <header>
994
+ <h1>SignalMesh<small id="skin-sub">The City</small></h1>
995
+ <nav class="skins" id="skins"></nav>
996
+ <div class="blurb" id="blurb"></div>
997
+ </header>
998
+ <div class="totals" id="totals"></div>
999
+ <main>
1000
+ <div id="map">
1001
+ <div class="grid" id="grid"></div>
1002
+ <div class="key">
1003
+ <span><i class="dot fresh"></i> fresh</span>
1004
+ <span><i class="dot dim"></i> dim</span>
1005
+ <span><i class="dot"></i> dark</span>
1006
+ <span>faded = no <b id="k-bldg">buildings</b></span>
1007
+ <span id="k-src"></span>
1008
+ </div>
1009
+ </div>
1010
+ <aside id="side"></aside>
1011
+ </main>
1012
+ <script>
1013
+ const $=id=>document.getElementById(id);
1014
+ const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1015
+ let CITY=null, SKIN='civic', PICKED=null;
1016
+
1017
+ const age=s=>{ if(s==null)return '';
1018
+ s=Math.round(s); if(s<60)return s+'s'; if(s<3600)return Math.round(s/60)+'m';
1019
+ if(s<86400)return Math.round(s/3600)+'h'; return Math.round(s/86400)+'d'; };
1020
+ const noun=k=>(CITY&&CITY.skins[SKIN]&&CITY.skins[SKIN][k])||k;
1021
+ const plural=k=>((CITY&&CITY.skins[SKIN]&&CITY.skins[SKIN].plural||{})[k])||noun(k);
1022
+
1023
+ function paintSkins(){
1024
+ $('skins').innerHTML=Object.entries(CITY.skins).map(([k,v])=>
1025
+ `<button data-skin="${k}" aria-pressed="${k===SKIN}">${esc(v.label)}</button>`).join('');
1026
+ $('skins').querySelectorAll('button').forEach(b=>b.onclick=()=>{
1027
+ SKIN=b.dataset.skin; document.documentElement.dataset.skin=SKIN;
1028
+ paintSkins(); paintTotals(); paintGrid(); paintSide();
1029
+ });
1030
+ $('blurb').textContent=CITY.skins[SKIN].blurb;
1031
+ $('skin-sub').textContent=CITY.skins[SKIN].label;
1032
+ $('k-bldg').textContent=plural('building');
1033
+ }
1034
+
1035
+ function paintTotals(){
1036
+ const t=CITY.totals;
1037
+ $('totals').innerHTML=[
1038
+ [`<b>${t.districts_lit}</b>/${t.districts_total}`, plural('district')+' lit'],
1039
+ [`<b>${t.buildings}</b>`, plural('building')],
1040
+ [`<b>${t.signals.toLocaleString()}</b>`, 'signals'],
1041
+ [`<b>${t.roads}</b>`, plural('road')],
1042
+ [`<b>${t.residents}</b>`, plural('resident')],
1043
+ ].map(([v,l])=>`<span>${v} ${esc(l)}</span>`).join('')
1044
+ + `<span style="margin-left:auto">world @ ${new Date((CITY.world_at||0)*1000)
1045
+ .toLocaleTimeString()}${CITY.cached?' · cached':''}</span>`;
1046
+ }
1047
+
1048
+ function paintGrid(){
1049
+ // One scale for the whole city, not one per district — otherwise a lone
1050
+ // 2-signal building draws exactly as tall as a 100-signal one and the
1051
+ // skyline says nothing. sqrt so the long tail stays visible next to the peaks.
1052
+ const peak=Math.max(1,...CITY.districts.flatMap(d=>d.buildings.map(b=>b.signals)));
1053
+ const h=v=>Math.max(7,Math.round(Math.sqrt(Math.max(v,0))/Math.sqrt(peak)*100));
1054
+ $('grid').innerHTML=CITY.districts.map(d=>{
1055
+ const bars=d.buildings.slice(0,7);
1056
+ const inner=bars.length
1057
+ ? `<div class="bars">`+bars.map(b=>
1058
+ `<i class="bar ${b.state}" style="height:${h(b.signals)}%"
1059
+ title="${esc(b.name)} · ${b.signals}"></i>`).join('')+`</div>`
1060
+ : '';
1061
+ return `<div class="district ${d.buildings.length?'':'empty'}" data-i="${d.index}"
1062
+ aria-current="${PICKED===d.index}" tabindex="0"
1063
+ title="${esc(d.coord)} · ${d.buildings.length} ${esc(plural('building'))}">
1064
+ <span class="coord">${esc(d.coord)}</span>${inner}</div>`;
1065
+ }).join('');
1066
+ $('grid').querySelectorAll('.district').forEach(el=>{
1067
+ const go=()=>{PICKED=+el.dataset.i;paintGrid();paintSide();
1068
+ $('side').scrollIntoView({block:'nearest',behavior:'smooth'})};
1069
+ el.onclick=go;
1070
+ el.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();go()}};
1071
+ });
1072
+ }
1073
+
1074
+ function paintSide(){
1075
+ const legend=`<div class="card"><h2>What this map may draw</h2>`+
1076
+ Object.entries(CITY.legend).map(([k,v])=>
1077
+ `<div class="legend-row ${v.drawable?'':'unavail'}">
1078
+ <span>${esc(k)}</span><span class="v">${esc(v.availability)}</span></div>`).join('')+
1079
+ (CITY.not_drawable.length
1080
+ ? `<p style="margin-top:9px;font-size:11.5px;color:var(--dim)">
1081
+ Struck through = the world model does not carry it, so nothing here
1082
+ invents it: <b>${CITY.not_drawable.map(esc).join(', ')}</b>.</p>` : '')+
1083
+ `</div>`;
1084
+
1085
+ if(PICKED==null){
1086
+ $('side').innerHTML=`<div class="card"><h2>Pick a ${esc(noun('district'))}</h2>
1087
+ <p class="empty-note">All ${CITY.grid.cells} grid nodes are here, including the
1088
+ ${CITY.totals.districts_total-CITY.totals.districts_occupied} carrying nothing.</p></div>`+legend;
1089
+ return;
1090
+ }
1091
+ const d=CITY.districts[PICKED];
1092
+ const bl=d.buildings.length?d.buildings.map(b=>
1093
+ `<div class="bldg"><i class="dot ${b.state}"></i>
1094
+ <span class="nm">${esc(b.name)}</span>
1095
+ <span class="ct">${b.signals} · ${esc(age(b.age_s))}</span></div>`).join('')
1096
+ : `<p class="empty-note">No ${esc(plural('building'))} on this node — it is
1097
+ addressable and silent, not missing.</p>`;
1098
+ const rd=d.roads.length?d.roads.map(r=>
1099
+ `<div class="road">${esc(r.from)} <b>&rarr;</b> ${esc(r.to)}
1100
+ <span class="pill">${esc(r.kind)}</span></div>`).join('')
1101
+ : `<p class="empty-note">No observed inbound ${esc(plural('road'))}.</p>`;
1102
+ const rs=d.residents.length?d.residents.map(r=>
1103
+ `<span class="pill">${esc(r.id)}</span>`).join('')
1104
+ : `<p class="empty-note">No ${esc(plural('resident'))} recorded here.</p>`;
1105
+
1106
+ $('side').innerHTML=`
1107
+ <div class="card">
1108
+ <h2>${esc(noun('district'))} ${esc(d.coord)} <span style="color:var(--accent)">·
1109
+ ${esc(d.state)}</span></h2>
1110
+ <div style="font-size:11.5px;color:var(--dim);margin-bottom:8px">
1111
+ grid node ${d.index} · row ${d.row} col ${d.col} · ${d.signal_count} signals</div>
1112
+ ${bl}
1113
+ </div>
1114
+ <div class="card"><h2>${esc(plural('road'))} in</h2>${rd}</div>
1115
+ <div class="card"><h2>${esc(plural('resident'))}</h2>${rs}</div>`+legend;
1116
+ }
1117
+
1118
+ async function load(){
1119
+ try{
1120
+ const r=await fetch('/ui/city.json');
1121
+ CITY=await r.json();
1122
+ }catch(e){
1123
+ $('map').innerHTML='<p class="empty-note">Could not reach the mesh.</p>';return;
1124
+ }
1125
+ $('k-src').innerHTML='every fact from <a href="/ui/world">/ui/world</a>';
1126
+ paintSkins(); paintTotals(); paintGrid(); paintSide();
1127
+ }
1128
+ load();
1129
+ setInterval(load,30000);
1130
+ </script></body></html>"""
1131
+
1132
+
1133
+ # ── The city: the same world model, as a place you can walk ──────────────────
1134
+
1135
+ @app.get("/ui/skins", tags=["Core"], summary="Skins available over the world model")
1136
+ def ui_skins():
1137
+ """Four readings of one projection — and what every one of them refuses to draw."""
1138
+ city = city_skin.build_cached(_do_world_state())
1139
+ return {"schema": city_skin.SCHEMA, "skins": city_skin.SKINS,
1140
+ "legend": city["legend"], "not_drawable": city["not_drawable"],
1141
+ "totals": city["totals"]}
1142
+
1143
+
1144
+ @app.get("/ui/city.json", tags=["Core"], summary="The world model, laid out as a city")
1145
+ def ui_city_json():
1146
+ """Districts, buildings, roads, residents — every fact traceable to /ui/world."""
1147
+ return city_skin.build_cached(_do_world_state())
1148
+
1149
+
1150
+ @app.get("/ui/city", response_class=HTMLResponse, include_in_schema=False)
1151
+ def ui_city():
1152
+ """Walk the mesh instead of reading its JSON."""
1153
+ return HTMLResponse(_CITY_HTML)
1154
+
1155
  @app.get("/identity", tags=["Core"], summary="Which copy of SignalMesh is canonical")
1156
  def public_identity():
1157
  """Keyless. The centrally adjudicated record for SignalMesh itself.