Pulse ticker — chip design
Working design notes for the chip layout in the homepage pulse ticker (web/src/components/PulseTicker.astro). Goal: instant readability at a glance while preserving the scientific-instrument feel of the source.
Direction (revised 2026-05-14)
Keep the scientific symbols, make them bolder, make the chips bigger. Earlier draft of this doc proposed color emoji; that was the wrong move. The obscure-but-correct glyphs (⏚ for ground/quake, ☼ for solar flare, ☄ for space weather, ⛈ for storm) signal real instrument provenance — they're a credibility asset, not visual debt. Emoji would have flattened the brand into "another aggregator."
Principle
Each chip has up to four fields, time is mandatory:
[time] [glyph] [data-point?] [location?]
- time — UTC HH:MM (always present, dim color so it doesn't compete for attention)
- glyph — the scientific symbol for the event kind, rendered bold + full-opacity + bumped font-size so it pops without needing a color bubble
- data-point — the headline measurement (magnitude, X-class, speed, Kp) when one is the actual story; bold weight; rendered only when non-empty (e.g. lightning has no data-point, just emoji + location)
- location — resolved place name via offline_geo lookup (not raw lat/lon — see #166)
Glyph set (current + revised)
The current glyphs are largely good; only a few weak ones to upgrade:
- quake ⏚ — IEC ground symbol; keep
- flare ☼ — astronomical sun; keep
- fireball ★ — generic but fine in context; keep
- space_weather ☄ — comet; keep
- solar_wind ~ — keep (flow-notation, recognized)
- meteor ↘ — keep (trajectory arrow)
- cme ◎ — keep (target/concentric circles, recognizable)
- forbush ↓ → upgrade to ☢ (radiation symbol; Forbush is a cosmic-ray decrease)
- alert ⚠ — keep (universal)
- storm ⛈ — keep (explicit)
- volcano ▲ — keep, but render bold so it doesn't look generic
- system ✓ — keep
- lightning (new) → ⚡ — fits alongside ⚠ aesthetically; unicode lightning bolt
Chip rendering — bolder + bigger
Three levers applied together:
- Drop icon opacity from 0.7 to 1.0 — current renders icons ghost-like at 70%. Full opacity is the single biggest readability win.
- Bump icon font-size from 0.7rem (~11px) to responsive
text-sm sm:text-base(14px mobile → 16px desktop) withfont-bold— gives the glyph visual weight against the chip body. - Responsive chip padding and text size — chips are bigger overall, but more compact on mobile so the ticker bar doesn't eat too much screen on small viewports.
Specifically:
- Chip body:
text-xs sm:text-sm px-2 py-0.5 sm:px-3 sm:py-1— ~22px height on mobile, ~30px on desktop - Icon span:
text-sm sm:text-base font-bold(full opacity, no class) - Gap between chip elements:
gap-2(wasgap-1.5)
The chip's tinted background (bg-{kind}-500/20) and ring (ring-{kind}-500/40) stay as-is — they already provide the kind-specific color signal, so a separate "color bubble" around the icon would compete rather than add.
Information hierarchy per chip type
12:34 ⚡ Pennsylvania, US— lightning (location is the headline; no data-point)12:34 ⏚ M5.2 Andreanof Is, AK— earthquake (magnitude + where)12:34 ☼ X1.4— solar flare (class is the entire story)12:34 ~ 850 km/s— solar wind speed12:34 ☢ Forbush 7%— cosmic-ray drop12:34 ⚠ Tornado Warning Davenport, IA— NWS alert (type + where)12:34 ▲ Mt. Spurr Watch— volcano (name + alert level)12:34 ⛈ Severe Thunderstorm Davenport, IA— severe convective
Open question — mobile vertical real estate
The bumped chip size adds ~8-10px to the ticker bar height (current ~32px → ~40-44px). That's a real chunk of mobile screen on small viewports. Worth eyeballing on an actual phone before locking in.
If it turns out too tall, the fallback is: keep current mobile sizing (text-[0.7rem] px-2 py-0.5), only bump on sm: and up. That's a single CSS class swap if it comes to that.
What's shipped today (prototype)
Visible at terrapulse.info. Commit [to fill in].
lightningkind added to KIND_STYLES with ⚡ glyph- Forbush glyph upgraded to ☢
- Icon opacity dropped from 0.7 to 1.0
- Icon font-size bumped to
text-sm sm:text-basewithfont-bold - Chip text size:
text-xs sm:text-sm - Chip padding:
px-2 py-0.5 sm:px-3 sm:py-1 - Inter-element gap:
gap-2 - Title span now renders conditionally (skips when empty so lightning chips read
⚡ Pennsylvania, UScleanly without a phantom bold-empty between glyph and location) - Blitzortung listener: title field dropped (was "Lightning", now empty) since the ⚡ carries the meaning
Eyeball on mobile + desktop. Tune from there.
Chip content rules (codified 2026-05-26)
The above sections cover visual design. These rules cover content —
what goes in title vs message, what's forbidden, and how lengths
are bounded.
Field roles
kind— phenomenon family. One-word lowercase token (quake,cyclone,cme,alert, ...). Glyph + kind label both signal this on the chip.title— data-point only. The headline measurement or event identifier (magnitude, X-class, speed, alert type, etc.). No length cap — trust source-side discipline.message— location, or a one-line technical state for non-spatial events. ≤ 60 chars, enforced at broadcast/persist time (not at display).
Forbidden in title
- The kind word or any kind synonym. The glyph + kind label
already carry that information; repeating it in the title is
visual noise. Examples that are dropped/stripped by the normalizer:
"CME 1980 km/s"→"1980 km/s""Cyclone Yasi"→"Yasi""Forbush"(whole title) →""(omitted; glyph carries it)"Flood"(whole title) →""(omitted)"fireball"(whole title) →""(omitted)
- When stripping leaves the title empty, the chip renders without a title — the glyph + kind label + message carry the chip.
alert-kind titles ("Tornado Warning", "Heat", "Gale", "Severe
Thunderstorm Warning") are NEVER stripped — kind="alert" has no
synonyms in the table, so the phenomenon name in the title stays.
Message bounds
len(message) > 60is truncated at the latest sentence boundary (. ! ?) within the budget if one exists in the back half of the budget; otherwise at the latest word boundary with a"…"suffix; otherwise hard-cut with"…".
Enforcement
A single helper terrapulse.api.chip_normalizer.normalize_chip()
runs server-side inside broadcast_message() in
src/terrapulse/api/routes/ws.py, BEFORE dedup / ring-buffer /
persist / WS broadcast. One chokepoint normalizes every chip from
every source — current and future. Same payload lands in the
in-memory ring, pulse_events, and every connected WebSocket
client.
When a rewrite happens, the helper logs a warning with the source name and before/after values:
WARNING chip_normalize kind=cme source='NASA DONKI'
title='CME 1980 km/s'->'1980 km/s'
message='...80 chars...'->'...60 chars...'
Watch journalctl -u terrapulse.service -f | grep chip_normalize to
see which fetchers still need source-side cleanup. The normalizer
is a backstop, not an excuse — fixing the upstream fetcher is
preferred so the warning goes silent.
Both surfaces read the same strings
The homepage ticker (web/src/components/PulseTicker.astro via
/api/v1/pulse/recent) and the incident monitor
(web/src/pages/incidents.astro via /api/v1/events/today) both
read the normalized title and message from pulse_events. There
is no separate "raw" representation kept anywhere — if you need
the full upstream payload, it's in extra_json on the row, but
that is normalized too so replays match what was on the ticker
at broadcast time.
Adding a new data source
When adding a new fetcher, follow these rules at the source (don't rely on the normalizer):
- Pick the right
kind. If none of the existing kinds fit, extendKIND_STYLESinPulseTicker.astroANDchips.astroANDKIND_SYNONYMSinchip_normalizer.py. - Make
titlea data-point. NEVER the kind word. - If you have no data-point to report, omit
title(empty string). The chip will render glyph + kind label + message. - Keep
messageto a location string (preferred) or a one-line technical state. Aim for ≤ 60 chars at the source so the normalizer doesn't have to truncate.