feat(ui): add recent updates dialogue (#89) #92
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/issue-89-recent-updates-dialogue"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Closes #89.
Adds a "Recent updates" dialog so end users can discover what changed in
meshmap-litesince they last used it. The dialog is never auto-pushed: it opens only on explicit click of the existing "i" button (which now also shows a small number badge = sum of unread release counts across all registered update-check sources). The auto-open rule for the static site information flow is preserved unchanged.The backend is designed as a generic multi-source release checker so the same component will later power Meshtastic firmware update notifications (a second source on GitHub).
Backend
internal/updatecheck/package:Sourceinterface (Name,FetchReleases,ReleasesPageURL) — library-shaped, no UI concerns.Cache(own file) — in-memory, mutex-protected, owned by the Manager.Manager— registers sources, runs one immediate fetch + anintervalticker per source, exposesSnapshot/Names/Labels/Subscribe/SubscribeAll.ReleaseInfo,UpdateSnapshot,SourceSpectypes in their own file.sources/forgejo(MVP, port of meshgo's checker) andsources/github(stub for the follow-up Meshtastic firmware feature —panicbody so the package compiles and is discoverable).SourceSpec.Label), not onSource— keeps the platform adapter a pure data fetcher.ReleasesPageURLis built by the adapter frombase_url + /<repo>/releases.GET /api/v1/updates?source=<name>&format=html|markdownendpoint:format=html(default) pre-renders each release body viagoldmark(reusessiteinfo.RenderMarkdown).{format, source, source_hash, releases: [{version, published_at, html_url, body}]}.update_check_not_configuredwhen the feature is disabled.update_check_source_not_foundfor an unknown source name.update_check_not_readywhen the source has not completed its first fetch yet./api/v1/metaextension: new top-levelupdate_check_available: boolandupdate_check_sources?: SourceSummary[](each entry carries version/date metadata only — no body — so the page-load payload stays light).update_check:config block (enabled,interval,timeout,sources[]withname/label/type/base_url/repository/current_version_source/limit). Auto-registers the defaultmeshmap-liteforgejo source whenenabled=trueand the sources block is absent; declaring it in config (or via ENV) replaces the default.current_version_source: buildinfo→buildinfo.Versionat registration time;current_version_source: none→ no comparison, snapshot just shows latest.Frontend
InfoModal→HtmlModalgeneralization (renamed file:InfoModal.tsx→HtmlModal.tsx): adds atitleprop (default"Information") and atabsslot for the header. Existing props (content,error,loading,showUpdatedNotice,onClose,onDismiss) and behavior are preserved.AppModalwrapper: renders the tabbed variant. Header layout is[tabs centered] [close right]. Each tab is a button; source tabs show a small number badge with that source's unread count. Active tab hasaria-current="page". Tabs: first is always Information (the static site-info flow), then one per source frommeta.update_check_sources(e.g. Map for MVP; will become Information + Map + Meshtastic once the firmware source is added — no further wiring needed).UpdatesPanel: per-source release list component (NOT hardcoded to meshmap-lite). Renders newest-first, shows a small NEW pill on releases newer than the user's dismissed point, and a visible<hr class="updates-separator">+ caption "You last read up to vX.Y.Z" after the latest release the user has already read. Footer has a "View all releases →" link (usessource.releasesPageUrl) and a "Got it" button.infoCookie.tsrefactored into amakeCookie(name)factory exporting bothinfoDismissedCookie(the existing cookie) and a newupdatesDismissedCookie. Both keep the 10y Max-Age +Path=/+SameSite=Laxcontract.web/src/api/client.ts— newapi.updates(source, format = 'html')method. Lazy: bodies are fetched only when the user opens a source tab.web/src/stores/meta.ts— surfaces the new meta fields. Pure helpercountNewerReleasesinmetaSelectors.tsfor unread-count logic.web/src/stores/updates.ts— new per-source store, keyed by source name; caches the latest fetched response so re-opening a tab is instant.web/src/App.tsxwiring:appModalOpen+appModalActiveTabstate; URL fragments#/info(always opens on Information) and#/updates/<sourceName>(deep-links to that source's tab). Auto-open rule: site-info source-hash change still opens on Information; source tabs never auto-open, only the badge. Dismiss handler is tab-aware (writes the appropriate cookie); the "×" close never marks anything as read.Header.tsx: small<span class="header-badge">over the "i" icon when the cross-source total > 0.aria-labelbecomes "Site information (N new updates)" when shown.styles.css: new rules for.app-modal-tabs,.app-modal-tab,.header-badge,.updates-release,.updates-separator,.updates-new-pill— with dark-theme overrides usingcolor-mix(matching the pattern from the recentshow-all-topologydark-theme work).Tests
internal/updatecheck/cache_test.go— Get/Set roundtrip, overwrite, concurrency safety.internal/updatecheck/manager_test.go— registration/dedupe, ticker behavior, immediate fetch, per-source failure isolation,UpdateAvailablesemantics (newer/equal/older/dev/invalid-latest),Subscribe/SubscribeAllbroker.internal/updatecheck/sources/forgejo/forgejo_test.go— URL construction, DTO decode,ReleasesPageURLshape.internal/api/http/updates_test.go— meta extension (disabled/enabled), 404 not configured, 404 unknown source, 503 not ready, HTML by default, markdown format, default-source fallback.HtmlModal.test.tsx(port of the old InfoModal test + newtitleprop andtabsslot coverage).AppModal.test.tsx— tab switching, badge rendering, close/escape.UpdatesPanel.test.tsx— empty, all-newer, mixed (separator + NEW pill), "Got it", "View all" link (present/absent), loading, error.infoCookie.test.ts— both cookies, read empty, write+read roundtrip, 10y Max-Age,Path=/,SameSite=Lax.client.test.ts—api.updates()200/404/404-source/503.metaSelectors.test.ts—countNewerReleases(empty cookie, future cookie, mixed).Verification (all green on
feat/issue-89-recent-updates-dialogue)go build ./...+go build ./cmd/servergo vet ./...go test ./...(20 packages)gofmt -l .golangci-lint run ./...npx tsc --noEmitnpx eslint srcnpm test(vitest)npx vite buildDecisions
limit: 15). Visible separator after the latest release the user has already read, with a "you last read up to vX.Y.Z" caption.api.updates(source).meshmap-lite.info.dismissed_source_hash(unchanged) + new flatmeshmap-lite.updates.dismissed_published_at. Both come from amakeCookie(name)factory.InfoModalgeneralized toHtmlModal(header slot).AppModalis the tabbed variant.Sourceinterface (no UI concerns) + per-platform sub-packages. Labels live in the Manager.Cacheis centralised. MVP ships onlyforgejo;githubis a stub.interval. Manager does an immediate fetch on startup.Subscribe(name)/SubscribeAll()on the Manager for future WebSocket push, modeled oninternal/api/ws/hub.go(R/W mutex, non-blocking, drop slow consumers).current_version_source: buildinfo→buildinfo.Version;current_version_source: none→ no comparison.meshmap-liteauto-registered withlabel: Map, forgejo,base_url: https://git.skobk.in,repository: skobkin/meshmap-lite,current_version_source: buildinfo,limit: 15whenupdate_check.enabled=trueand the source is absent from config.🤖 Generated with Claude Code