{ "$schema": "homarr-widget-package-v3", "manifest": { "id": "homarr.reference.overview", "version": "1.0.0", "sdkVersion": "1", "name": "Composed homelab overview", "description": "Three independently authored components share one rich tile using SDK scopes for isolated state, commands, storage and handler routing.", "entrypoints": { "tile": "widget.tsx", "advanced": "widget.tsx", "server": "server.ts" }, "handlers": { "rack.systems": { "kind": "query", "permission": "view" }, "downloads.queue": { "kind": "query", "permission": "view", "inputSchema": { "type": "object", "additionalProperties": false, "required": ["connection"], "properties": { "connection": { "type": "string", "enum": ["client", "secondary"] } } } }, "downloads.control": { "kind": "action", "permission": "modify", "inputSchema": { "type": "object", "additionalProperties": false, "required": ["connection", "operation"], "properties": { "connection": { "type": "string", "enum": ["client", "secondary"] }, "operation": { "type": "string", "enum": ["pause", "resume", "pauseItem", "resumeItem", "deleteItem"] }, "itemId": { "type": "string", "minLength": 1 } } }, "guestAccess": true }, "media.sessions": { "kind": "query", "permission": "view" }, "downloads.sources": { "kind": "query", "permission": "view" }, "downloads.controlBatch": { "kind": "action", "permission": "modify", "guestAccess": true, "inputSchema": { "type": "object", "additionalProperties": false, "required": ["connections", "operation"], "properties": { "connections": { "type": "array", "minItems": 1, "maxItems": 2, "uniqueItems": true, "items": { "type": "string", "enum": ["client", "secondary"] } }, "operation": { "type": "string", "enum": ["pause", "resume"] } } } } } }, "files": { "downloads-server.ts": "import type { BatchControl, BatchResult, ControlResult, DownloadConnection, DownloadControl, DownloadQueue, DownloadSource, SourceQueue } from \"./download-types\";\nimport type { WidgetServerContext } from \"@homarr/widget-sdk/server\";\nimport { callWidgetIntegration, defineWidgetServer, getWidgetServerContext } from \"@homarr/widget-sdk/server\";\n\nconst declared:DownloadConnection[] = [\"client\",\"secondary\"];\nconst allowed = new Set([\"pause\",\"resume\",\"pauseItem\",\"resumeItem\",\"deleteItem\"]);\nasync function sources(context:WidgetServerContext):Promise {\n const {bindingNames,options} = await getWidgetServerContext(context);\n return declared.filter((connection) => bindingNames.includes(connection)).map((connection) => {\n let label = options.primaryLabel;\n if (connection === \"secondary\") label = options.secondaryLabel;\n if (typeof label !== \"string\" || !label.trim()) label = connection === \"client\" ? \"Primary client\" : \"Second client\";\n return {connection,label:String(label)};\n });\n}\nasync function source(context:WidgetServerContext,connection:DownloadConnection) {\n const result = (await sources(context)).find((candidate) => candidate.connection === connection);\n if (!result) throw new Error(\"Choose a declared, bound download client\");\n return result;\n}\nasync function queue(context:WidgetServerContext,connection:DownloadConnection):Promise {\n const selected = await source(context,connection);\n const result = await callWidgetIntegration(context,{connection,capability:\"downloads.queue\",input:{limit:100}});\n return {...selected,data:result.data};\n}\nasync function control(context:WidgetServerContext,input:DownloadControl):Promise {\n if (!allowed.has(input?.operation)) throw new Error(\"Unknown download operation\");\n await source(context,input.connection);\n if (input.operation === \"pause\" || input.operation === \"resume\") {\n await callWidgetIntegration(context,{connection:input.connection,capability:`downloads.${input.operation}`,input:{}});\n } else {\n if (typeof input.itemId !== \"string\" || !input.itemId) throw new Error(\"Choose a download from this client\");\n const current = await queue(context,input.connection);\n const item = current.data.items.find((candidate) => candidate.id === input.itemId);\n if (!item) throw new Error(\"The download is no longer in this client's queue; refresh and try again\");\n // Resolve the canonical item in the selected client. Never trust a browser-supplied client index or item object.\n await callWidgetIntegration(context,{connection:input.connection,capability:`downloads.${input.operation}`,input:{item,fromDisk:false}});\n }\n return {connection:input.connection,operation:input.operation,ok:true};\n}\nexport default defineWidgetServer({\n sources: (_input:unknown,context) => sources(context),\n queue: (input:{connection:DownloadConnection},context) => queue(context,input.connection),\n control: (input:DownloadControl,context) => control(context,input),\n controlBatch: async (input:BatchControl,context):Promise => {\n if (![\"pause\",\"resume\"].includes(input?.operation)) throw new Error(\"Batch actions support pause or resume only\");\n if (!Array.isArray(input.connections) || !input.connections.length || input.connections.length > 2 || new Set(input.connections).size !== input.connections.length)\n throw new Error(\"Choose one or two different download clients\");\n const available = await sources(context);\n if (input.connections.some((connection) => !available.some((candidate) => candidate.connection === connection)))\n throw new Error(\"Every batch target must be a declared, bound client\");\n const results = await Promise.all(input.connections.map(async (connection):Promise => {\n try { return await control(context,{connection,operation:input.operation}); }\n catch (error) { return {connection,operation:input.operation,ok:false,error:error instanceof Error ? error.message : String(error)}; }\n }));\n const succeeded = results.filter((result) => result.ok).length;\n return {results,succeeded,failed:results.length-succeeded};\n },\n});\n", "rack-server.ts": "import type { NativeResult, SystemRow } from \"./beszel-types\";\nimport { callWidgetIntegration, defineWidgetServer } from \"@homarr/widget-sdk/server\";\nexport default defineWidgetServer({ systems: (_input, context) => callWidgetIntegration>(context, {connection:\"beszel\",capability:\"beszel.systems\"}) });", "server.ts": "import type { WidgetServerHandler } from \"@homarr/widget-sdk/server\";\nimport { defineWidgetServer } from \"@homarr/widget-sdk/server\";\nimport rack from \"./rack-server.ts\";\nimport downloads from \"./downloads-server.ts\";\nimport media from \"./media-server.ts\";\nconst prefix = (name:string, handlers:Record>) => Object.fromEntries(Object.entries(handlers).map(([key, handler]) => [`${name}.${key}`, handler]));\nexport default defineWidgetServer({...prefix(\"rack\", rack), ...prefix(\"downloads\", downloads), ...prefix(\"media\", media)});", "download-types.ts": "export type DownloadConnection = \"client\" | \"secondary\";\nexport type DownloadOperation = \"pause\" | \"resume\" | \"pauseItem\" | \"resumeItem\" | \"deleteItem\";\nexport interface DownloadItem {\n id:string;name:string;state:string;progress:number;downSpeed?:number;\n index:number;type:\"torrent\"|\"usenet\"|\"miscellaneous\";size:number;time:number;\n}\nexport interface DownloadQueue {data:{items:DownloadItem[];status:{paused:boolean;rates:{down:number;up?:number}}}}\nexport interface DownloadSource {connection:DownloadConnection;label:string}\nexport interface SourceQueue extends DownloadSource {data:DownloadQueue[\"data\"]}\nexport interface DownloadControl {connection:DownloadConnection;operation:DownloadOperation;itemId?:string}\nexport interface BatchControl {connections:DownloadConnection[];operation:\"pause\"|\"resume\"}\nexport interface ControlResult {connection:DownloadConnection;operation:DownloadOperation;ok:boolean;error?:string}\nexport interface BatchResult {results:ControlResult[];succeeded:number;failed:number}\nexport interface DownloadOptions {primaryLabel:string;secondaryLabel:string}\n", "media.tsx": "import type { MediaSessions } from \"./media-types\";\nimport { Alert, Badge, Button, Card, Group, Progress, SimpleGrid, Stack, Text } from \"@mantine/core\";\nimport { useWidgetHost, useWidgetQuery, useWidgetServices } from \"@homarr/widget-sdk\";\nexport default function Sessions() {\n const host = useWidgetHost();\n const services = useWidgetServices();\n const query = useWidgetQuery(\"sessions\", {}, { refetchInterval: 10000 });\n const rows = query.data?.data ?? [];\n const columns = Math.max(1, Math.floor(host.visibleWidth / 240));\n return Now playing \u00b7 {rows.length}\n {query.error && {query.error.message}}\n {rows.map((session) => { const playing = session.currentlyPlaying; const playback = playing?.playback; return {session.user.username}{playback?.state ?? \"idle\"}{playing?.name ?? session.sessionName}{playing?.episodeName && {playing.episodeName}}{typeof playback?.durationMs === \"number\" && playback.durationMs > 0 && }; })}\n {!query.isPending && !query.error && query.data?.configured !== false && rows.length === 0 && No active media sessions.}\n {query.data?.configured === false && Bind a media server integration to show now-playing sessions.}\n ;\n}", "widget.tsx": "import { SimpleGrid } from \"@mantine/core\";\nimport { useWidgetHost, WidgetScope } from \"@homarr/widget-sdk\";\nimport Rack from \"./rack.tsx\";\nimport Downloads from \"./downloads.tsx\";\nimport Media from \"./media.tsx\";\nexport default function Overview() {\n const host = useWidgetHost();\n return = 900 ? 3 : 1} h=\"100%\" style={{overflow:\"auto\"}}>\n \n \n \n ;\n}", "media-types.ts": "export type Camera = {name:string;path:string};\nexport type MediaOptions = {title:string;showSessions:boolean;cameras:Camera[];imageCount:number;snapshotInterval:number};\nexport interface GalleryItem {kind:\"camera\"|\"immich\";id:string;name:string;takenAt?:string;}\nexport type GalleryImageInput = Pick;\nexport interface ImmichAsset {id:string;originalFileName?:string;localDateTime?:string;fileCreatedAt?:string;}\nexport interface MediaSession {sessionId:string;sessionName:string;user:{username:string};currentlyPlaying:{name:string;episodeName?:string|null;location:\"lan\"|\"wan\"|null;playback:{state:\"playing\"|\"paused\"|\"buffering\"|null;positionMs:number|null;durationMs:number|null};metadata:{bitrateKbps:number|null;transcoding:{isVideoDirect:boolean;isAudioDirect:boolean}}|null}|null;}\nexport interface MediaSessions {data:MediaSession[];configured?:boolean;}\n", "media-server.ts": "import type { MediaSessions } from \"./media-types\";\nimport { callWidgetIntegration, defineWidgetServer } from \"@homarr/widget-sdk/server\";\nexport default defineWidgetServer({ sessions: (_input, context) => callWidgetIntegration(context, { connection: \"media\", capability: \"media.sessions\", input: {showOnlyPlaying: false} }) });", "rack.tsx": "import type { SystemRow, NativeResult } from \"./beszel-types\";\nimport { Alert, Button, Group, Stack, Text } from \"@mantine/core\";\nimport { useWidgetHost, useWidgetQuery, useWidgetServices, useWidgetState } from \"@homarr/widget-sdk\";\nimport \"./rack.css\";\nexport default function Rack() {\n const host = useWidgetHost();\n const services = useWidgetServices();\n const query = useWidgetQuery>(\"systems\", {}, {refetchInterval: 15000});\n const [selected, setSelected] = useWidgetState(\"selected\", \"\");\n const systems = query.data?.data ?? [];\n const rows = host.displayMode === \"advanced\" ? systems : systems.slice(0, 8);\n const inspect = (system:SystemRow) => { setSelected(system.id); services.openDetail({title: system.name, content: {system.hostname} \u00b7 {system.osName}CPU: {system.cpu.toFixed(1)}% \u00b7 Memory: {system.memory.toFixed(1)}% \u00b7 Disk: {system.disk.toFixed(1)}%{system.cpuModel} \u00b7 {system.cores} coresUptime: {Math.floor(system.uptime / 3600)} hours}); };\n return \n Server rack{systems.length} systems\n {query.error && {query.error.message}}\n \n \n {rows.map((system, index) => inspect(system)} onKeyDown={(event) => { if (event.key === \"Enter\" || event.key === \" \") { event.preventDefault(); inspect(system); } }} className=\"reference-rack__unit\">\n \n \n {system.name.slice(0, 35)}\n CPU {system.cpu.toFixed(1)}% \u00b7 RAM {system.memory.toFixed(1)}% \u00b7 {system.status}\n \n {[0,1,2,3].map((slot) => )}\n )}\n \n {!query.isPending && !query.error && !systems.length && No Beszel systems are available.}\n ;\n}", "beszel-types.ts": "// Public result fields used from the Homarr Beszel capability adapter.\nexport interface SystemRow {\n id:string; name:string; status:\"up\"|\"down\"|\"paused\"|\"pending\";\n cpu:number; memory:number; disk:number; gpu:number; netBytes:number; uptime:number;\n hostname:string; cpuModel:string; cores:number; osName:string; agentVersion:string;\n}\nexport interface AttributedSystem extends SystemRow {connection:string;rowKey:string}\nexport interface NativeResult {data:T;timestamp?:string}\nexport interface SystemStats {cpu:number;mu:number;mb:number;du:number;dio?:[number,number];dr?:number;dw?:number;b?:[number,number];ns?:number;nr?:number}\nexport interface ContainerStats {n:string;c:number;m:number;b?:[number,number];ns?:number;nr?:number}\nexport interface StatsRecord {id:string;created:string;stats:T}\nexport type SystemRecord = StatsRecord;\nexport type ContainerRecord = StatsRecord;\nexport interface StatsResult {systemStats:SystemRecord[];containerStats:ContainerRecord[]}\nexport type LiveStatsEvent = {type:\"system_stats\";record:SystemRecord}|{type:\"container_stats\";record:ContainerRecord};\nexport interface StatsInput {connection?:string;systemId:string;timePeriod?:string;includeDocker?:boolean}\n", "downloads.tsx": "import type { BatchControl, BatchResult, DownloadConnection, DownloadSource } from \"./download-types\";\nimport { Alert, Button, Group, MultiSelect, Select, Stack, Text, TextInput } from \"@mantine/core\";\nimport { useWidgetAction, useWidgetCapabilities, useWidgetCommands, useWidgetHost, useWidgetQuery, useWidgetState } from \"@homarr/widget-sdk\";\nimport { DownloadSourceRegion } from \"./download-source\";\nexport default function Downloads() {\n const host = useWidgetHost();\n const sources = useWidgetQuery(\"sources\",{}, {staleTime:60000});\n const capabilities = useWidgetCapabilities();\n const batch = useWidgetAction(\"controlBatch\");\n const [filter,setFilter] = useWidgetState(\"filter\",\"\");\n const [sort,setSort] = useWidgetState(\"sort\",\"queue\");\n const [selection,setSelection] = useWidgetState(\"batch-clients\",[]);\n const available = sources.data ?? [];\n const targets = selection.filter((name) => available.some((source) => source.connection === name));\n const all = available.map((source) => source.connection);\n const canRun = (input:BatchControl) => input.connections.length > 0 && !host.isEditMode && !batch.isPending && capabilities.canRun(\"controlBatch\",input);\n const run = (input:BatchControl) => {void batch.mutateAsync(input).catch(() => undefined);};\n const pauseAll:BatchControl = {connections:all,operation:\"pause\"};\n const resumeAll:BatchControl = {connections:all,operation:\"resume\"};\n const pauseSelected:BatchControl = {connections:targets,operation:\"pause\"};\n const resumeSelected:BatchControl = {connections:targets,operation:\"resume\"};\n useWidgetCommands([{id:\"pause-all\",label:\"Pause all download clients\",disabled:!canRun(pauseAll),run:() => run(pauseAll)},{id:\"resume-all\",label:\"Resume all download clients\",disabled:!canRun(resumeAll),run:() => run(resumeAll)}]);\n return \n Downloads \u00b7 {available.length} clients\n {sources.error && {sources.error.message}}\n {host.displayMode === \"advanced\" && <>\n setFilter(event.currentTarget.value)} />