Skip to content

feat(announcements): remote-fetched announcements banner - #78

Merged
frenchie4111 merged 8 commits into
mainfrom
announcements-banner
May 25, 2026
Merged

feat(announcements): remote-fetched announcements banner#78
frenchie4111 merged 8 commits into
mainfrom
announcements-banner

Conversation

@frenchie4111

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a remote-fetched announcements banner that mirrors the visual idiom of the existing update banners. Source is harness.mikelyons.org/announcements.json; fetched on boot, on window focus, and every 6h from main. Network failures stay silent (logged to debug.log).
  • New slice announcements (items + lastFetched + lastError) and two new settings fields (dismissedAnnouncementIds, announcementsMuted) that persist through the existing config.json path, so dismissal + "Hide all" survive a reload.
  • UI shows at most one banner — newest unexpired non-dismissed entry — with × to dismiss this id and a ⋯ menu offering "Hide all announcements." Title is a clickable link that opens externally.

Validation

Strict per-entry validation in fetchAnnouncementsFeed: id / title / href / publishedAt are required strings; href must parse and be http:/https:; publishedAt must be a parseable date; expiresAt optional. Malformed entries are dropped individually (with a debug-log count) — the rest of the feed is preserved.

Test plan

  • npm run typecheck — clean
  • npx electron-vite build — clean
  • npx vitest run — 618 / 618 passing (618 tests across 53 files; new tests cover the slice reducer, the settings event variants, and the validator's accept/drop branches)
  • UI verification not done in this PR: the live feed at harness.mikelyons.org/announcements.json returns 404 today (Mike's site, not yet populated), and I didn't want to wire a throwaway local fixture. Once the feed is populated, the banner should render below the update banners, dismiss to next-newest on ×, and stay hidden across reload when "Hide all" is selected.

🤖 Generated with Claude Code

frenchie4111 and others added 3 commits May 25, 2026 10:04
Adds an announcements slice (items + lastFetched + lastError) wired
into the root reducer alongside existing slices. Settings gets two
new fields, dismissedAnnouncementIds and announcementsMuted, with
events and tests, so the per-banner `×` and the "Hide all" action
can persist across reloads via the existing settings persistence
path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds AnnouncementsPoller that fetches harness.mikelyons.org/announcements.json
on start and every 6h with a 10s AbortController timeout. Each entry is
validated strictly: id/title/href/publishedAt are required strings,
href must parse as an http(s) URL, and publishedAt must be a parseable
date — malformed entries are dropped individually with a debug-log
line, the rest of the feed is preserved. Network failures dispatch
fetchFailed and stay silent in the UI.

dismissedAnnouncementIds and announcementsMuted persist through the
existing config.json path so dismissals survive a reload. Three IPC
handlers (announcements:refresh / :dismiss / :mute) expose the writes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useAnnouncements() exposes the slice; the App-level useMemo filters
expired + dismissed entries, returns null when muted, otherwise picks
the entry with the newest publishedAt. The banner sits below the
update banners using the accent semantic color so it doesn't compete
with update green/info. Title links to href via shell.openExternal;
× dismisses just this id; the ⋯ menu offers "Hide all announcements"
which sets announcementsMuted. Window focus piggybacks an
announcements:refresh alongside the existing PR stale-refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frenchie4111 and others added 5 commits May 25, 2026 11:32
The kebab button's wrapper was sized to the button (~22px), but the
banner row is ~42px tall with py-2.5 padding. items-center on the
row centered the wrapper vertically, leaving the absolute menu's
top edge inside the row — and the row's drag-region captured clicks
on the menu's top ~6px (no-drag isn't reliably inherited by
absolutely-positioned descendants in every Webkit build).

self-stretch + flex items-center stretches the wrapper to the full
row height while keeping the button visually centered, so top-full
now anchors the menu cleanly below the row. Also tagged the menu
container and the menuitem with no-drag as defense in depth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-slice wire-merge introduced in #77 (d1aa7b9) enumerates every
AppState slice; the announcements slice that landed on this branch
needs the same merge line so the AppState object literal still
satisfies the type. TypeScript catches this by design — that's the
guard the new helper documents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The welcome entry was test data for verifying the banner renders
end-to-end during development. Empty the array so the feature ships
with no banner showing by default — future announcements get added
deliberately, not because a placeholder slipped through.

The file stays in place (rather than getting deleted) so the URL
returns valid JSON and the poller's success path runs cleanly
instead of logging a 404 on every refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@frenchie4111
frenchie4111 merged commit 2d0c064 into main May 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant