From 049655fd22913a1ed9b3ee389a18dead6e88218c Mon Sep 17 00:00:00 2001 From: engmung <122682380+engmung@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:35:50 +0900 Subject: [PATCH] feat(web): tell board owners a firmware exists that their board cannot The console's "newer firmware is out" banner was added in v3.4.0, so it only appears once you have already installed v3.4.0. Everyone on v3.1.0 through v3.3.0 can update wirelessly today and has nothing anywhere telling them to. This site cannot fill the gap by reading their board either. A browser refuses to let an https page fetch http at all, and a board is http - verified rather than assumed: Mixed Content: The page at 'https://patternflow.work/update' was loaded over HTTPS, but requested an insecure resource 'http://patternflow.local/api/status'. This request has been blocked. That asymmetry is why every device flow navigates TO the board instead of reaching into it, and it rules out showing somebody their current version here. So the only place left is a page they already visit, which is the community. A thin strip above the header, quiet on purpose - it is news rather than an error, and they came to look at patterns. Dismissal is keyed to the version rather than permanent. Nothing can know whether they went and updated, so asking once per release and then dropping it is the honest shape; a later release brings it back. Co-Authored-By: Claude Opus 5 --- web/src/app/community/layout.tsx | 5 ++ .../components/community/Community.module.css | 73 +++++++++++++++++ .../components/community/FirmwareNotice.tsx | 81 +++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 web/src/components/community/FirmwareNotice.tsx diff --git a/web/src/app/community/layout.tsx b/web/src/app/community/layout.tsx index 6c31a54..ffd5690 100644 --- a/web/src/app/community/layout.tsx +++ b/web/src/app/community/layout.tsx @@ -10,6 +10,7 @@ import AuthStatus from "@/components/community/AuthStatus"; import CommunityNav from "@/components/community/CommunityNav"; import DeckDock from "@/components/community/DeckDock"; import DeckPanel from "@/components/community/DeckPanel"; +import FirmwareNotice from "@/components/community/FirmwareNotice"; import MobileTabBar from "@/components/community/MobileTabBar"; import styles from "@/components/community/Community.module.css"; @@ -66,6 +67,10 @@ export default async function CommunityLayout({ children }: { children: React.Re return (
+ {/* Above the header on purpose. A board owner needs to see this once; + putting it below the nav would let it sit under the fold on the + page they actually came for. */} +
diff --git a/web/src/components/community/Community.module.css b/web/src/components/community/Community.module.css index 386acf0..7671194 100644 --- a/web/src/components/community/Community.module.css +++ b/web/src/components/community/Community.module.css @@ -2873,6 +2873,79 @@ pointer-events: none; } +/* ── Firmware notice ────────────────────────────────────────────────────── + A thin strip above the header. Deliberately quiet: it is news, not an + error, and the person reading it came here to look at patterns. It uses + the LED accent only on the tag and the link, so it registers without + turning the top of every page into a warning. */ +.fwNotice { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + padding: 9px 12px; + margin-bottom: 14px; + border: 1px solid rgba(255, 92, 46, 0.28); + background: rgba(255, 92, 46, 0.06); + font-size: 12.5px; +} + +.fwNoticeTag { + font-family: var(--pf-mono, monospace); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--pfc-led); + flex: none; +} + +.fwNoticeText { + color: var(--pfc-ink-muted); + min-width: 0; +} + +.fwNoticeText strong { + color: var(--pfc-ink); + font-weight: 600; +} + +.fwNoticeLink { + color: var(--pfc-led); + text-decoration: none; + white-space: nowrap; + flex: none; +} + +.fwNoticeLink:hover { + text-decoration: underline; +} + +.fwNoticeClose { + appearance: none; + border: 0; + background: none; + color: var(--pfc-ink-faint); + font-size: 13px; + line-height: 1; + padding: 4px; + cursor: pointer; + margin-left: auto; + flex: none; +} + +.fwNoticeClose:hover { + color: var(--pfc-ink); +} + +@media (max-width: 640px) { + /* The close button keeps its place; the link drops below the text rather + than squeezing it to two words a line. */ + .fwNoticeText { + flex: 1 1 100%; + order: 3; + } +} + /* ── Share panel ────────────────────────────────────────────────────────── The link is shown, not just copied: a button that says "Copied" about an address you never saw asks for trust it has not earned, and the field is diff --git a/web/src/components/community/FirmwareNotice.tsx b/web/src/components/community/FirmwareNotice.tsx new file mode 100644 index 0000000..26c809d --- /dev/null +++ b/web/src/components/community/FirmwareNotice.tsx @@ -0,0 +1,81 @@ +"use client"; + +import Link from "next/link"; +import { useSyncExternalStore } from "react"; +import manifest from "../../../public/flash/manifest.json"; +import styles from "./Community.module.css"; + +// "There is a new firmware" — said here, because there is nowhere else to say +// it. +// +// A board checks for updates from its own console, and that check only exists +// in v3.4.0 and later. Everyone on an earlier build sees nothing: the banner +// telling you to update is in the update you have not installed. Nor can this +// site read their version to find out — patternflow.work is https and a board +// is http, and a browser will not let an https page fetch http at all. The +// direction only runs the other way, which is why every device flow works by +// navigating TO the board rather than reaching into it. +// +// So the only place left to mention it is a page they already visit, which is +// this one: the community is where somebody who owns a Patternflow goes. +// +// Dismissal is per version, not forever. Dismiss this and it stays gone until +// there is a different release to mention — it cannot know whether you updated +// (see above), so the honest fallback is to ask once per version and drop it. + +const LATEST = manifest.version; +const KEY = "pf-firmware-notice"; + +/** Read once per render from localStorage, with a server snapshot of "not + * dismissed" so the markup matches and the banner never flashes in late. */ +function useDismissed(): boolean { + return useSyncExternalStore( + (onChange) => { + window.addEventListener("storage", onChange); + return () => window.removeEventListener("storage", onChange); + }, + () => { + try { + return window.localStorage.getItem(KEY) === LATEST; + } catch { + return false; /* private mode */ + } + }, + () => true, // server: assume dismissed, so nothing appears before hydration + ); +} + +export default function FirmwareNotice() { + const dismissed = useDismissed(); + if (dismissed) return null; + + const dismiss = () => { + try { + window.localStorage.setItem(KEY, LATEST); + } catch { + /* private mode — it will ask again, which is the lesser failure */ + } + window.dispatchEvent(new Event("storage")); + }; + + return ( +
+ Firmware + + {LATEST} is out — patterns install from a link, and a set now ships with + the board. + + + Update over Wi-Fi → + + +
+ ); +}