put the monitor scope in the url
/system-monitor/:scope, the same shape as /photos: route pair, one Navigate guard after the hooks, scope list as react-router NavLinks, and both panels reading useParams instead of agreeing over a `monitor:scope` channel. The Dock's isActive is a startsWith, so its highlight survives the redirect off the bare route.
This commit is contained in:
@@ -79,7 +79,7 @@ are good building blocks. The **Workspace/Panel framework** contains **zero** ro
|
|||||||
| M7 | `workspaces/components/Combobox.tsx:53` | caller-supplied route | (existing) | shared widget: `href`-bearing options do `navigate(option.href)`; render them as `<Link>`. Every caller inherits the opaque click. |
|
| M7 | `workspaces/components/Combobox.tsx:53` | caller-supplied route | (existing) | shared widget: `href`-bearing options do `navigate(option.href)`; render them as `<Link>`. Every caller inherits the opaque click. |
|
||||||
| ~~M8~~ | `Layout/Header/UserMenu.tsx` | — | — | **Done.** Removed rather than routed: nothing had ever been built behind `/settings/resources`, so the item was a bounce to `/` dressed as navigation. Its `header.userMenu.resources` locale keys went with it. |
|
| ~~M8~~ | `Layout/Header/UserMenu.tsx` | — | — | **Done.** Removed rather than routed: nothing had ever been built behind `/settings/resources`, so the item was a bounce to `/` dressed as navigation. Its `header.userMenu.resources` locale keys went with it. |
|
||||||
| M9 | `Screens/Dashboard/Plans/index.tsx:36` | a plan document | `/plans/:name` | native `<select>` → local state; a plan is a real addressable doc. Have the select `navigate()` or use a link list. |
|
| M9 | `Screens/Dashboard/Plans/index.tsx:36` | a plan document | `/plans/:name` | native `<select>` → local state; a plan is a real addressable doc. Have the select `navigate()` or use a link list. |
|
||||||
| M10 | `SystemMonitor/ScopeList.tsx:14` | monitor scope (btop/pm2/docker) | `/system-monitor/:scope` | small fixed set; low urgency but genuinely addressable. |
|
| ~~M10~~ | `SystemMonitor/ScopeList.tsx` | monitor scope (btop/pm2/docker) | `/system-monitor/:scope` | **Done.** Route pair + `Navigate` guard; the scope buttons are `NavLink`s and `useMonitorScope` reads `useParams` instead of the channel. The Dock's hand-rolled `isActive` is a `startsWith`, so its highlight survives the redirect. |
|
||||||
|
|
||||||
**Music** (M-music) and **Soulseek** (M-slsk) are whole-workspace channel apps — pulled out below because each
|
**Music** (M-music) and **Soulseek** (M-slsk) are whole-workspace channel apps — pulled out below because each
|
||||||
is **one design decision** that cascades across many files:
|
is **one design decision** that cascades across many files:
|
||||||
@@ -122,7 +122,6 @@ This is the primary surface to convert to URL-driven selection.
|
|||||||
| `music:cwd` | library location (album/artist/folder) | `/music?path=` | `MusicBrowser/MusicDetail/FavoritesView` |
|
| `music:cwd` | library location (album/artist/folder) | `/music?path=` | `MusicBrowser/MusicDetail/FavoritesView` |
|
||||||
| `soulseek:section` | active Soulseek section | `/soulseek/:section` | `SoulseekNav/View/Dashboard/UserMenu` |
|
| `soulseek:section` | active Soulseek section | `/soulseek/:section` | `SoulseekNav/View/Dashboard/UserMenu` |
|
||||||
| `soulseek:user` | selected peer | `/soulseek/users/:name` | `UserMenu.tsx:28`, `SoulseekUsers.tsx:34` |
|
| `soulseek:user` | selected peer | `/soulseek/users/:name` | `UserMenu.tsx:28`, `SoulseekUsers.tsx:34` |
|
||||||
| `monitor:scope` | monitor scope | `/system-monitor/:scope` | `ScopeList.tsx` (via `shared.ts:15`) |
|
|
||||||
| local `useState` selections | search / room / conversation / peer / open file / preview slug | respective nested routes | `SearchView`, `SoulseekRooms`, `SoulseekChat`, `SoulseekUsers`, `CodeEditor/useEditorState`, `PreviewProvider` |
|
| local `useState` selections | search / room / conversation / peer / open file / preview slug | respective nested routes | `SearchView`, `SoulseekRooms`, `SoulseekChat`, `SoulseekUsers`, `CodeEditor/useEditorState`, `PreviewProvider` |
|
||||||
|
|
||||||
**Excluded — event-bus / refresh signals, NOT selection:** `files:refresh-signal`,
|
**Excluded — event-bus / refresh signals, NOT selection:** `files:refresh-signal`,
|
||||||
@@ -203,7 +202,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri
|
|||||||
### Phase 3 — Whole-workspace routing decisions (needs a design call first)
|
### Phase 3 — Whole-workspace routing decisions (needs a design call first)
|
||||||
- [ ] **Music** — decide `/music?path=` (or nested); back `music:cwd` with the URL; rows → `<Link>` (M-music).
|
- [ ] **Music** — decide `/music?path=` (or nested); back `music:cwd` with the URL; rows → `<Link>` (M-music).
|
||||||
- [ ] **Soulseek** — decide the nested tree `/soulseek/:section` + `/soulseek/{users,search,rooms,chat}/:x`; **S1 decides S2–S7** (M-slsk).
|
- [ ] **Soulseek** — decide the nested tree `/soulseek/:section` + `/soulseek/{users,search,rooms,chat}/:x`; **S1 decides S2–S7** (M-slsk).
|
||||||
- [ ] **M10** SystemMonitor scope → `/system-monitor/:scope` (`ScopeList.tsx:14`).
|
- [x] **M10** SystemMonitor scope → `/system-monitor/:scope`; `monitor:scope` channel deleted. **Needs runtime test.**
|
||||||
- [ ] **M9** Plans → `/plans/:name` (`Plans/index.tsx:36`).
|
- [ ] **M9** Plans → `/plans/:name` (`Plans/index.tsx:36`).
|
||||||
|
|
||||||
### Phase 4 — Polish + borderline decisions
|
### Phase 4 — Polish + borderline decisions
|
||||||
|
|||||||
@@ -644,8 +644,10 @@ All the same bug: an app guessing "am I being closed?" from an unmount, or payin
|
|||||||
could never have worked: the link set the section, the tab stayed on Enterprise, and the content
|
could never have worked: the link set the section, the tab stayed on Enterprise, and the content
|
||||||
pane rendered "Select a section" for a section that existed. The tab is derived from the section
|
pane rendered "Select a section" for a section that existed. The tab is derived from the section
|
||||||
key now, so there is nothing left to disagree with.
|
key now, so there is nothing left to disagree with.
|
||||||
Remaining here: Music, Soulseek, SystemMonitor, Email, Browser. **Chat detail
|
**SystemMonitor is done too** (audit M10) — `/system-monitor/:scope`, scope list as `NavLink`s,
|
||||||
(`chat:selected-session`) is chat-owned — leave it; it is written up in `COMMS/`.**
|
`monitor:scope` deleted. Smallest of the seven and the same shape as `/photos`.
|
||||||
|
Remaining here: Music, Soulseek, Email, Browser. **Chat detail (`chat:selected-session`) is
|
||||||
|
chat-owned — leave it; it is written up in `COMMS/`.**
|
||||||
|
|
||||||
### 5.9 The context has grown an app-config section — _(found 2026-08-07)_
|
### 5.9 The context has grown an app-config section — _(found 2026-08-07)_
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export function App() {
|
|||||||
<Route path="/wallet" element={<Dashboard.WalletScreen />} />
|
<Route path="/wallet" element={<Dashboard.WalletScreen />} />
|
||||||
<Route path="/wallet/:section" element={<Dashboard.WalletScreen />} />
|
<Route path="/wallet/:section" element={<Dashboard.WalletScreen />} />
|
||||||
<Route path="/system-monitor" element={<Dashboard.SystemMonitorScreen />} />
|
<Route path="/system-monitor" element={<Dashboard.SystemMonitorScreen />} />
|
||||||
|
<Route path="/system-monitor/:scope" element={<Dashboard.SystemMonitorScreen />} />
|
||||||
{/* TEMPORARY / EXPERIMENTAL — offline file transfer over animated QR codes */}
|
{/* TEMPORARY / EXPERIMENTAL — offline file transfer over animated QR codes */}
|
||||||
<Route path="/qr-transfer" element={<Dashboard.QrTransferScreen />} />
|
<Route path="/qr-transfer" element={<Dashboard.QrTransferScreen />} />
|
||||||
<Route path="/activity" element={<Dashboard.ActivityScreen />} />
|
<Route path="/activity" element={<Dashboard.ActivityScreen />} />
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
import { Navigate, useParams } from 'react-router';
|
||||||
import type { LayoutNode } from 'officerdev';
|
import type { LayoutNode } from 'officerdev';
|
||||||
import { WorkspaceView } from 'officerdev';
|
import { WorkspaceView, DEFAULT_MONITOR_SCOPE, monitorScopePath, isMonitorScope } from 'officerdev';
|
||||||
import { useDashboardState } from 'state/useDashboardState';
|
import { useDashboardState } from 'state/useDashboardState';
|
||||||
import { defaultLayout } from './defaultLayout';
|
import { defaultLayout } from './defaultLayout';
|
||||||
|
|
||||||
// /system-monitor uses the Workspace/Panel system (like /music): a horizontal split with an (empty for
|
// /system-monitor uses the Workspace/Panel system (like /photos): the scope list on the left, the scope
|
||||||
// now) left panel and the system snapshot on the right.
|
// itself on the right. Which scope is open is `:scope` in the URL, not a panel channel.
|
||||||
|
|
||||||
export const SystemMonitorScreen = () => {
|
export const SystemMonitorScreen = () => {
|
||||||
|
const { scope } = useParams();
|
||||||
const workspace = useDashboardState<LayoutNode>('screens/system-monitor', defaultLayout);
|
const workspace = useDashboardState<LayoutNode>('screens/system-monitor', defaultLayout);
|
||||||
|
|
||||||
|
// After the hooks — it returns early. Bare /system-monitor and a scope that doesn't exist both
|
||||||
|
// canonicalise, so the list's router-driven highlight can never disagree with the pane.
|
||||||
|
if (!isMonitorScope(scope)) return <Navigate to={monitorScopePath(DEFAULT_MONITOR_SCOPE)} replace />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full pt-2">
|
<div className="h-full w-full pt-2">
|
||||||
<WorkspaceView
|
<WorkspaceView
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { BtopView } from './BtopView';
|
|||||||
import { Pm2View } from './Pm2View';
|
import { Pm2View } from './Pm2View';
|
||||||
import { DockerView } from './DockerView';
|
import { DockerView } from './DockerView';
|
||||||
|
|
||||||
// Right panel — renders whichever scope the left ScopeList selected (default: bTop).
|
// Right panel — renders whichever scope the URL names. It does not hear from the ScopeList; both read
|
||||||
|
// `/system-monitor/:scope`.
|
||||||
export const MonitorMain = ({ panelId: _panelId }: { panelId: string }) => {
|
export const MonitorMain = ({ panelId: _panelId }: { panelId: string }) => {
|
||||||
const [scope] = useMonitorScope();
|
const scope = useMonitorScope();
|
||||||
if (scope === 'pm2') return <Pm2View />;
|
if (scope === 'pm2') return <Pm2View />;
|
||||||
if (scope === 'docker') return <DockerView />;
|
if (scope === 'docker') return <DockerView />;
|
||||||
return <BtopView />;
|
return <BtopView />;
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
|
import { NavLink } from 'react-router';
|
||||||
import { Cpu, Server, Boxes } from 'lucide-react';
|
import { Cpu, Server, Boxes } from 'lucide-react';
|
||||||
import { useMonitorScope, SCOPES, type MonitorScope } from './shared';
|
import { SCOPES, monitorScopePath, type MonitorScope } from './shared';
|
||||||
|
|
||||||
// Left panel — a single-column list of scopes; selecting one drives the right panel via the channel.
|
// Left panel — a single-column list of scopes. Each is a react-router NavLink to /system-monitor/<scope>,
|
||||||
|
// so active state comes from the router and a scope can be linked, bookmarked and cmd-clicked.
|
||||||
const ICONS: Record<MonitorScope, typeof Cpu> = { btop: Cpu, pm2: Server, docker: Boxes };
|
const ICONS: Record<MonitorScope, typeof Cpu> = { btop: Cpu, pm2: Server, docker: Boxes };
|
||||||
|
|
||||||
export const ScopeList = ({ panelId: _panelId }: { panelId: string }) => {
|
export const ScopeList = ({ panelId: _panelId }: { panelId: string }) => (
|
||||||
const [scope, setScope] = useMonitorScope();
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col overflow-y-auto p-3">
|
<div className="flex h-full flex-col overflow-y-auto p-3">
|
||||||
{SCOPES.map((s) => {
|
{SCOPES.map((s) => {
|
||||||
const Icon = ICONS[s.id];
|
const Icon = ICONS[s.id];
|
||||||
return (
|
return (
|
||||||
<button
|
<NavLink
|
||||||
key={s.id}
|
key={s.id}
|
||||||
type="button"
|
to={monitorScopePath(s.id)}
|
||||||
onClick={() => setScope(s.id)}
|
className={({ isActive }) =>
|
||||||
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm ${
|
`flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm ${
|
||||||
scope === s.id ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
isActive ? 'bg-muted text-foreground' : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
||||||
}`}
|
}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon size={15} className="shrink-0" />
|
<Icon size={15} className="shrink-0" />
|
||||||
<span className="truncate">{s.label}</span>
|
<span className="truncate">{s.label}</span>
|
||||||
</button>
|
</NavLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
// The left panel picks a scope; the right panel renders it. Coordinated over a panel channel, like the
|
// The left panel picks a scope and the right panel renders it, and neither tells the other: the scope is
|
||||||
// /music browser → detail split.
|
// `/system-monitor/:scope`. It used to be a `monitor:scope` panel channel, which meant the URL said
|
||||||
|
// `/system-monitor` however deep you were — nothing to link, nothing to bookmark, and the list came back
|
||||||
|
// on whatever you last clicked in some other tab.
|
||||||
export type MonitorScope = 'btop' | 'pm2' | 'docker';
|
export type MonitorScope = 'btop' | 'pm2' | 'docker';
|
||||||
|
|
||||||
export const MONITOR_SCOPE_CHANNEL = 'monitor:scope';
|
|
||||||
|
|
||||||
export const SCOPES: { id: MonitorScope; label: string }[] = [
|
export const SCOPES: { id: MonitorScope; label: string }[] = [
|
||||||
{ id: 'btop', label: 'bTop' },
|
{ id: 'btop', label: 'bTop' },
|
||||||
{ id: 'pm2', label: 'pm2 processes' },
|
{ id: 'pm2', label: 'pm2 processes' },
|
||||||
{ id: 'docker', label: 'dockers' },
|
{ id: 'docker', label: 'dockers' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const useMonitorScope = () => usePanelChannel<MonitorScope>(MONITOR_SCOPE_CHANNEL, 'btop');
|
/** Where /system-monitor lands, and where an unrecognised scope redirects to. */
|
||||||
|
export const DEFAULT_MONITOR_SCOPE: MonitorScope = 'btop';
|
||||||
|
|
||||||
|
export const isMonitorScope = (value: string | undefined): value is MonitorScope => SCOPES.some((s) => s.id === value);
|
||||||
|
|
||||||
|
export const monitorScopePath = (scope: MonitorScope) => `/system-monitor/${scope}`;
|
||||||
|
|
||||||
|
// SystemMonitorScreen redirects anything unrecognised, so the fallback here only covers the instant before
|
||||||
|
// that lands.
|
||||||
|
export const useMonitorScope = (): MonitorScope => {
|
||||||
|
const { scope } = useParams();
|
||||||
|
return isMonitorScope(scope) ? scope : DEFAULT_MONITOR_SCOPE;
|
||||||
|
};
|
||||||
|
|
||||||
export function fmtBytes(n: number): string {
|
export function fmtBytes(n: number): string {
|
||||||
if (!n) return '0 B';
|
if (!n) return '0 B';
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export type { PhotosSectionId } from './apps/Photos/shared';
|
|||||||
export { DEFAULT_JELLYFIN_SECTION, jellyfinSectionPath, isJellyfinSection } from './apps/Jellyfin/shared';
|
export { DEFAULT_JELLYFIN_SECTION, jellyfinSectionPath, isJellyfinSection } from './apps/Jellyfin/shared';
|
||||||
export type { JellyfinSectionId } from './apps/Jellyfin/shared';
|
export type { JellyfinSectionId } from './apps/Jellyfin/shared';
|
||||||
|
|
||||||
|
// Same for /system-monitor, where the param is the scope rather than a section.
|
||||||
|
export { DEFAULT_MONITOR_SCOPE, monitorScopePath, isMonitorScope } from './apps/SystemMonitor/shared';
|
||||||
|
export type { MonitorScope } from './apps/SystemMonitor/shared';
|
||||||
|
|
||||||
// Same for /transmission.
|
// Same for /transmission.
|
||||||
export {
|
export {
|
||||||
DEFAULT_TRANSMISSION_SECTION,
|
DEFAULT_TRANSMISSION_SECTION,
|
||||||
|
|||||||
Reference in New Issue
Block a user