feat(announcements): remote-fetched announcements banner - #78
Merged
Conversation
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>
3 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
harness.mikelyons.org/announcements.json; fetched on boot, on window focus, and every 6h from main. Network failures stay silent (logged todebug.log).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.×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/publishedAtare required strings;hrefmust parse and behttp:/https:;publishedAtmust be a parseable date;expiresAtoptional. Malformed entries are dropped individually (with a debug-log count) — the rest of the feed is preserved.Test plan
npm run typecheck— cleannpx electron-vite build— cleannpx 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)harness.mikelyons.org/announcements.jsonreturns 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