Listening for events…

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:

  1. Drop icon opacity from 0.7 to 1.0 — current renders icons ghost-like at 70%. Full opacity is the single biggest readability win.
  2. Bump icon font-size from 0.7rem (~11px) to responsive text-sm sm:text-base (14px mobile → 16px desktop) with font-bold — gives the glyph visual weight against the chip body.
  3. 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 (was gap-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 speed
  • 12:34 ☢ Forbush 7% — cosmic-ray drop
  • 12: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].

  • lightning kind 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-base with font-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, US cleanly 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) > 60 is 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):

  1. Pick the right kind. If none of the existing kinds fit, extend KIND_STYLES in PulseTicker.astro AND chips.astro AND KIND_SYNONYMS in chip_normalizer.py.
  2. Make title a data-point. NEVER the kind word.
  3. If you have no data-point to report, omit title (empty string). The chip will render glyph + kind label + message.
  4. Keep message to 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.
Live Feed