fix(web): the logs come back after a host restart, and eight more that quietly lied
The Logs page died permanently every time the host restarted — which the console's own update flow does. The host's log ring restarts at seq 1 while the page's cursor stays where it got to, and `GET /logs?after=8000` against a fresh ring is not an error, it is an empty page forever: no error, no dropped badge, stale lines on screen, and nothing short of a full reload to get out. A restart always breaks the poll first, so a failed poll now triggers a re-read from the start of the ring, and a page whose newest entry is older than what we hold is recognised as the sequence having restarted. Follow mode also stopped following at exactly the wrong moment. The autoscroll effect was keyed on the rendered row count, which pins at the 1000-row DOM cap — so once the log got busy enough to matter, the effect never re-ran again. It is keyed on the newest rendered seq now. And pausing now actually pauses: stopping the interval left React Query's focus/reconnect refetches landing, which evicted the very lines the operator had paused on. The rest: - A plugin could white-screen the whole console by registering `icon: "constructor"`. The icon map is a plain object, so the inherited key resolved to `Object`, which is truthy — the fallback never fired and React was handed `Object` as a component, from inside the app shell. - Saving a display arrangement deleted the saved position of every device that was not connected at that moment: the host replaces the whole map, and we only ever sent the displays we could see. - Flipping DDC, PnP or dedicated-game-sessions committed whatever unsaved edits the Custom block was holding, then cleared the "unsaved" badge so there was no trace of it. Those three apply on top of the SAVED policy now. - Saving the Custom block put the streamed-screen pin back to whatever it was when the form was seeded, undoing a change made in the picker below it. - "End now" on a running game calls the host's only stop, which ends EVERY live session; on a grace row with no app id it ended every waiting game. Both say so first now, when there is more than one to lose. - Edit and Delete were offered on library entries owned by a provider plugin, which the host refuses with 409 — silently. They are attributed instead. - An install whose first poll failed never polled again, and one whose host restarted spun forever with no way to dismiss it. - Submitting a second pairing PIN showed the previous attempt's "PIN sent" before a digit was typed, and the paired list it points you at never refreshed. - The streamed-screen picker claimed an env pin during every slow load, and rows that cannot be picked now look that way instead of silently eating the click. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-3
@@ -46,9 +46,19 @@ const ICONS: Record<string, LucideIcon> = {
|
||||
clapperboard: Clapperboard,
|
||||
};
|
||||
|
||||
/** Resolve a registered icon name to a component (Puzzle fallback). */
|
||||
export const pluginIcon = (name?: string): LucideIcon =>
|
||||
(name ? ICONS[name] : undefined) ?? Puzzle;
|
||||
/**
|
||||
* Resolve a registered icon name to a component (Puzzle fallback).
|
||||
*
|
||||
* `name` comes from a plugin's own registration, so it is untrusted input to a lookup on a plain
|
||||
* object — and a plain object inherits from Object.prototype. `ICONS["constructor"]` is `Object`,
|
||||
* which is truthy, so a `?? Puzzle` fallback never fires and React is handed `Object` as a
|
||||
* component: it throws out of render, and because this runs inside the AppShell nav that takes
|
||||
* down every page of the console. `Object.hasOwn` keeps the lookup to keys we actually declared.
|
||||
*/
|
||||
export const pluginIcon = (name?: string): LucideIcon => {
|
||||
if (!name || !Object.hasOwn(ICONS, name)) return Puzzle;
|
||||
return ICONS[name] ?? Puzzle;
|
||||
};
|
||||
|
||||
/** Live plugin registrations, polled (and refetched on window focus) so the nav stays current. */
|
||||
export function usePlugins() {
|
||||
|
||||
Reference in New Issue
Block a user