Rename Installation to Instance

Follows the portal: the noun is "instance" everywhere the app says it —
UI strings, CLI output, error details, docs and comments. The wire keys
(`instance_id`, `instance_token`) and the hub route this calls move with it.

An existing cloud.json is adopted rather than refused: without the key
alias the dataclass fails to parse, which the caller swallows and reads as
"never enrolled" instead of "reconnect".

`instance_key` on a node type becomes `target_key`. It means the outside
thing a node points at, which is a different sense of the word, and keeping
both would put two meanings of "instance" in one codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
2026-08-31 10:12:01 +02:00
co-authored by Claude Opus 5
parent 6534855492
commit d01a8dad37
101 changed files with 374 additions and 375 deletions
+2 -2
View File
@@ -5,9 +5,9 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#59849b" />
<!-- `%BASE_URL%` is `/` on an installation of its own and `/app-shell/` in
<!-- `%BASE_URL%` is `/` on an instance of its own and `/app-shell/` in
the build the portal serves, where the hub rewrites this line to the
installation's own path so the install is scoped to one house. -->
instance's own path so the install is scoped to one house. -->
<link rel="manifest" href="%BASE_URL%manifest.webmanifest" />
<link rel="apple-touch-icon" href="%BASE_URL%apple-touch-icon.png" />
<title>Fluksio</title>
+3 -3
View File
@@ -5,7 +5,7 @@
* so caching it would hand the next visitor somebody else's session.
*
* `registration.scope` is the app's root in both places it runs — `/` on an
* installation of its own, `/i/{id}/` through the portal — which is why the
* instance of its own, `/i/{id}/` through the portal — which is why the
* push payload does not carry a URL.
*/
@@ -16,7 +16,7 @@ self.addEventListener("push", (event) => {
self.registration.showNotification(alert.title || "Fluksio", {
body: alert.body || "",
// Under the portal the icons are the shared bundle's, not this
// installation's path.
// instance's path.
icon: scope.pathname.startsWith("/i/")
? "/app-shell/icon-192.png"
: `${scope.pathname}icon-192.png`,
@@ -35,7 +35,7 @@ self.addEventListener("notificationclick", (event) => {
self.clients
.matchAll({ type: "window", includeUncontrolled: true })
.then((clients) => {
// A tab on this installation is already open: raise it rather than
// A tab on this instance is already open: raise it rather than
// opening a second one.
const open = clients.find((client) => client.url.startsWith(scope))
if (open) return open.focus()
+3 -3
View File
@@ -2379,7 +2379,7 @@ export const PanelsConfigSchema = {
},
type: 'object',
title: 'PanelsConfig',
description: 'Every panel this installation knows about.'
description: 'Every panel this instance knows about.'
} as const;
export const PanelsPublicSchema = {
@@ -2405,7 +2405,7 @@ The address is the server's own, because the browser's origin is not a
reliable answer to it: an admin working through the portal is on the
portal's origin, and this one is for a screen on this network.
An installation enrolled with a portal has a second address, built by the
An instance enrolled with a portal has a second address, built by the
dialog from what \`\`/cloud/status\`\` reports rather than from here — a panel
is not the thing that knows whether remote access is on.`
} as const;
@@ -3803,7 +3803,7 @@ A colour widget picks what it publishes with \`\`format\`\`, because fixtures
differ and a change node per tile is not the answer:
- \`\`hsv\`\` (the default) — \`\`[h, s, v]\`\`, hue 0-360 degrees, saturation and
value 0-100 percent. What the Node-RED installation this ports feeds its
value 0-100 percent. What the Node-RED instance this ports feeds its
3CH/4CH DMX encoders, which divide by 360 and by 100.
- \`\`rgb\`\`\`\`[r, g, b]\`\`, each 0-255. The conventional range; the
reference's own encoders produce it after converting.
+6 -6
View File
@@ -177,7 +177,7 @@ export class ArtifactsService {
export class CloudService {
/**
* Read Status
* Whether this installation is enrolled, and whether the link is up.
* Whether this instance is enrolled, and whether the link is up.
*
* Readable by any signed-in user: everyone here has a right to know whether
* the machine they are using can be reached from outside.
@@ -196,7 +196,7 @@ export class CloudService {
* Redeem a claim code and start dialling the portal.
*
* The account performing this is mapped to the portal account that owns the
* installation, so the owner's portal sessions arrive here as them. Widening
* instance, so the owner's portal sessions arrive here as them. Widening
* that to anyone else is a local decision made one person at a time, below —
* never something the portal can do from its side.
*
@@ -1710,7 +1710,7 @@ export class PanelsService {
* All this hands out is a code that means nothing until somebody with an
* account approves it, so the worst an unwelcome caller achieves is an entry
* that expires ten minutes later. Reachable from the internet when this
* installation is enrolled with a portal, which is what the cap and the
* instance is enrolled with a portal, which is what the cap and the
* portal's own per-address limits are between.
* @returns PairStarted Successful Response
* @throws ApiError
@@ -1781,8 +1781,8 @@ export class PanelsService {
*
* A credential minted here names the approver, so what the panel does stays
* attributable to a person rather than to nobody. One minted by the portal —
* for a device that reached this installation only through it — names the
* account this installation was enrolled with instead, since that is the one
* for a device that reached this instance only through it — names the
* account this instance was enrolled with instead, since that is the one
* every portal-borne request already acts as.
* @param data The data for the request.
* @param data.panelId
@@ -2228,7 +2228,7 @@ export class SearchService {
* Everything searchable, for the client to match against as it is typed.
*
* The whole index rather than a query: it is a few hundred short rows for an
* installation of any ordinary size, so one fetch when the panel opens beats a
* instance of any ordinary size, so one fetch when the panel opens beats a
* round trip per keystroke — and the client already has a matcher.
*
* Secrets are named only to a superuser, which is who ``/secrets`` answers to.
+3 -3
View File
@@ -852,7 +852,7 @@ export type PanelDef = {
};
/**
* Every panel this installation knows about.
* Every panel this instance knows about.
*/
export type PanelsConfig = {
panels?: Array<PanelDef>;
@@ -865,7 +865,7 @@ export type PanelsConfig = {
* reliable answer to it: an admin working through the portal is on the
* portal's origin, and this one is for a screen on this network.
*
* An installation enrolled with a portal has a second address, built by the
* An instance enrolled with a portal has a second address, built by the
* dialog from what ``/cloud/status`` reports rather than from here — a panel
* is not the thing that knows whether remote access is on.
*/
@@ -1286,7 +1286,7 @@ export type WebPushKey = {
* differ and a change node per tile is not the answer:
*
* - ``hsv`` (the default) — ``[h, s, v]``, hue 0-360 degrees, saturation and
* value 0-100 percent. What the Node-RED installation this ports feeds its
* value 0-100 percent. What the Node-RED instance this ports feeds its
* 3CH/4CH DMX encoders, which divide by 360 and by 100.
* - ``rgb`` — ``[r, g, b]``, each 0-255. The conventional range; the
* reference's own encoders produce it after converting.
@@ -9,7 +9,7 @@ import { isPortal } from "@/lib/portal"
const ID = "connection-offline"
/**
* Says when the installation cannot be reached, and when it was last heard
* Says when the instance cannot be reached, and when it was last heard
* from.
*
* Only ever meaningful under a portal: a local install cannot lose contact with
@@ -39,8 +39,8 @@ export function ConnectionNotice() {
}
notify(
connection.lastSeen
? `Installation offline — last seen ${ago(connection.lastSeen / 1000)}, reconnecting…`
: "Installation offline — reconnecting…",
? `Instance offline — last seen ${ago(connection.lastSeen / 1000)}, reconnecting…`
: "Instance offline — reconnecting…",
"warning",
{ id: ID, persistent: true },
)
@@ -19,8 +19,8 @@ const DEFAULT_COLUMNS = 12
* How many tiles are worth a request of their own.
*
* ponytail: the list endpoint carries no placements, so a footprint means
* reading that dashboard's document — cheap for the handful an installation
* has, and shared with the editor's own cache. The ceiling is an installation
* reading that dashboard's document — cheap for the handful an instance
* has, and shared with the editor's own cache. The ceiling is an instance
* with dozens: the tiles past this show their name and nothing else, and the
* fix would be a stored footprint on `DashboardSummary`.
*/
@@ -58,7 +58,7 @@ function hint(entry: SearchEntry): string {
}
/**
* Everything in this installation, by name, from anywhere.
* Everything in this instance, by name, from anywhere.
*
* The whole index arrives in one fetch and `cmdk` does the matching, so results
* narrow as they are typed without a round trip per keystroke.
@@ -190,7 +190,7 @@ export function GlobalSearch({
</>
) : (
<p className="py-6 text-center text-sm text-muted-foreground">
Start typing to search this installation.
Start typing to search this instance.
</p>
)}
</CommandList>
@@ -3,7 +3,7 @@
*
* A single stack at the top centre, over whatever is below it. Newest on top,
* each card going on its own after a few seconds unless it was raised as
* persistent — an offline notice stays until the installation answers again.
* persistent — an offline notice stays until the instance answers again.
*
* Raised from anywhere through `notify()` in `lib/notificationStore`, which is
* where the semantics live; this only draws them.
@@ -303,10 +303,10 @@ export function ConfirmDelete({
<DialogDescription>
{description ?? (
<>
{names.length === 1 ? "It goes" : "They go"} from the
installation at once. The store's git history keeps what was
there, but nothing in the app brings{" "}
{names.length === 1 ? "it" : "them"} back.
{names.length === 1 ? "It goes" : "They go"} from the instance
at once. The store's git history keeps what was there, but
nothing in the app brings {names.length === 1 ? "it" : "them"}{" "}
back.
</>
)}
</DialogDescription>
@@ -51,7 +51,7 @@ export function PanelsDialog() {
const { data: config } = useQuery(panelsQueryOptions())
const { data: dashboards } = useQuery(dashboardsQueryOptions())
const { user } = useAuth()
// Where a screen that cannot reach this installation pairs instead. The
// Where a screen that cannot reach this instance pairs instead. The
// issuer is the portal as a browser reaches it, which is not always the
// address this machine dialled — enrolment may have named a container.
const { data: cloud } = useQuery({
@@ -60,12 +60,12 @@ export function PanelsDialog() {
(await CloudService.readStatus()) as {
enrolled: boolean
issuer: string | null
installation_id: string | null
instance_id: string | null
},
})
const remoteHost =
cloud?.enrolled && cloud.issuer && cloud.installation_id
? `${cloud.issuer.replace(/\/$/, "")}/i/${cloud.installation_id}`
cloud?.enrolled && cloud.issuer && cloud.instance_id
? `${cloud.issuer.replace(/\/$/, "")}/i/${cloud.instance_id}`
: ""
const save = useSavePanels()
const { showErrorToast } = useCustomToast()
@@ -75,7 +75,7 @@ export function PanelsDialog() {
const panels = config?.panels ?? []
const known = dashboards?.data ?? []
// The installation's own address, not this browser's: administering through
// The instance's own address, not this browser's: administering through
// the portal puts the page on the portal's origin, and a screen cannot be
// sent there — it has no portal session and could not hold a panel
// credential if it had one.
@@ -98,7 +98,7 @@ export function PanelsDialog() {
<DialogTitle>Panels</DialogTitle>
<DialogDescription>
A panel is one screen and the dashboards it shows. Point the device at
a link and it asks for a code you enter here this installation's own
a link and it asks for a code you enter here this instance's own
address for a screen on your network, or the portal's for one hanging
where this machine is not reachable.
</DialogDescription>
@@ -187,9 +187,9 @@ function PanelRow({
onRemove,
}: {
panel: PanelDef
/** Where this installation answers, as it knows itself. */
/** Where this instance answers, as it knows itself. */
host: string
/** Where the portal serves this installation, when it is enrolled. */
/** Where the portal serves this instance, when it is enrolled. */
remoteHost: string
/**
* Whether this account may change anything here. Every control below saves
@@ -335,7 +335,7 @@ function PanelRow({
<Input
readOnly
value={link}
placeholder="This installation has no address set"
placeholder="This instance has no address set"
aria-label={`Link for ${panel.id}`}
className="text-muted-foreground"
onFocus={(event) => event.currentTarget.select()}
+1 -1
View File
@@ -854,7 +854,7 @@ function ParamsForm({
* How much of a machine this node takes, and how long it is expected to take.
*
* A named size is the usual answer: what "gpu-small" means is a property of the
* machines this installation has, and those change. The numbers are still there
* machines this instance has, and those change. The numbers are still there
* for a node that genuinely wants its own.
*/
function ResourcesSection({
+1 -1
View File
@@ -100,7 +100,7 @@ export function useParamSuggestions(
staleTime: SUGGEST_STALE,
})
// ponytail: reads every flow to collect them, sharing the editor's own cache
// entries; an aggregate endpoint if a big installation makes that hurt.
// entries; an aggregate endpoint if a big instance makes that hurt.
const details = useQueries({
queries: (type ? (flows?.data ?? []) : []).map((flow) => ({
...flowQueryOptions(flow.name),
@@ -32,7 +32,7 @@ type FlowEvent =
nodes: { id: string; status: string; error?: string | null }[]
paused?: string[]
logs?: LogLine[]
/** Emission counts per qualified node. Absent on older installations. */
/** Emission counts per qualified node. Absent on older instances. */
emits?: Record<string, number>
}
| {
@@ -194,7 +194,7 @@ function connect() {
liveStore.setStatuses(message.nodes ?? [])
liveStore.setPausedFlows(message.paused ?? [])
liveStore.setLogs(message.logs ?? [])
// Missing against an installation older than this bundle; the graph
// Missing against an instance older than this bundle; the graph
// then starts from zero the way it always did.
liveStore.setEmits(message.emits ?? {})
break
@@ -321,7 +321,7 @@ function connect() {
const payload = JSON.parse(event.data)
if (payload?.type === "batch") {
// A cascade publishes a dozen events at once and the engine coalesces
// them into one frame. An installation older than this bundle sends
// them into one frame. An instance older than this bundle sends
// them one at a time, which is the branch below.
for (const message of payload.events ?? []) handle(message)
} else {
@@ -347,7 +347,7 @@ function connect() {
return
}
}
// 1013 is the portal saying the installation is not attached — the one
// 1013 is the portal saying the instance is not attached — the one
// close code that means "offline" rather than "the socket dropped".
if (event.code === 1013) {
connectionStore.setOffline(null)
@@ -75,7 +75,7 @@ export function HealthOverview({
const queue = (summary?.queue ?? {}) as Record<string, number>
const degraded = summary?.status === "degraded"
const errors = (flows ?? []).reduce((total, row) => total + row.errors, 0)
// An installation nobody has run a batch flow on has no runs tile at all —
// An instance nobody has run a batch flow on has no runs tile at all —
// the same "nothing has been run yet" the Runs screen itself goes by.
const running = (runs ?? []).reduce((total, row) => total + row.running, 0)
const queued = (runs ?? []).reduce((total, row) => total + row.queued, 0)
@@ -23,7 +23,7 @@ const GRACE = 3000
* stop pulsing and the values stop changing, with nothing to say why. Nothing
* at all while the socket is up: a page that works needs no chip saying so.
*
* Quiet too while the offline notification is up, since an installation that cannot
* Quiet too while the offline notification is up, since an instance that cannot
* be reached has no socket either and one explanation of that is enough. Named
* in words rather than coloured, and deliberately not terracotta: the brain
* graph above it already owns that accent for a flow that cannot run.
@@ -33,7 +33,7 @@ import { type Item, Main } from "./Main"
/**
* Where the documentation is published.
*
* A constant rather than something derived: an installation reached over a LAN
* A constant rather than something derived: an instance reached over a LAN
* address, or at localhost, has no domain to build this from, and the docs are
* one site for all of them.
*/
@@ -70,13 +70,13 @@ export function AppSidebar() {
? [...baseItems, { icon: Users, title: "Admin", path: "/admin" }]
: baseItems
// Reached through a portal, the way out is back to the installations list —
// Reached through a portal, the way out is back to the instances list —
// and the session belongs to the portal, so logging out is its business.
const items: Item[] = portal
? [
{
icon: ArrowLeft,
title: "All installations",
title: "All instances",
onClick: () => window.location.assign(portal.portalUrl),
},
...withAdmin,
@@ -31,19 +31,19 @@ type CloudStatus = {
connected: boolean
portal_url: string | null
portal_account: string | null
installation_id: string | null
instance_id: string | null
last_error: string | null
connected_since: number | null
}
/**
* Connecting this installation to a Fluksio portal, or cutting it loose, and
* Connecting this instance to a Fluksio portal, or cutting it loose, and
* admitting other portal accounts to it.
*
* Deliberately blunt about what it grants: the account that enrolled is what
* the portal owner's sessions act as, and this screen says which one. Anyone
* else gets in only by being added here, as a local user of their own.
* Everything here is optional — an installation nobody enrolls never contacts
* Everything here is optional — an instance nobody enrolls never contacts
* anything.
*/
export function RemoteAccess() {
@@ -108,8 +108,8 @@ export function RemoteAccess() {
<CardHeader>
<CardTitle>Remote access</CardTitle>
<CardDescription>
Reach this installation from fluksio.com. Entirely optional without
it, this installation talks to nothing outside your network.
Reach this instance from fluksio.com. Entirely optional without it,
this instance talks to nothing outside your network.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-6">
@@ -133,9 +133,9 @@ export function RemoteAccess() {
own user.
</span>
</Field>
<Field label="Installation">
<Field label="Instance">
<span className="font-mono text-xs">
{status.installation_id ?? "—"}
{status.instance_id ?? "—"}
</span>
</Field>
</dl>
@@ -156,8 +156,8 @@ export function RemoteAccess() {
<div>
<h3 className="font-medium">Remote users</h3>
<p className="text-sm text-muted-foreground">
Let someone else reach this installation through the portal.
They get a user of their own here — not yours, and never a
Let someone else reach this instance through the portal. They
get a user of their own here — not yours, and never a
superuser, so they cannot pass access on.
</p>
</div>
@@ -173,9 +173,9 @@ export function RemoteAccess() {
}
/>
<p className="text-xs text-muted-foreground">
They get a code at fluksio.com → Installations → Join an
installation. Added users appear under Admin → Users; deleting
them there ends their access.
They get a code at fluksio.com → Instances → Join an instance.
Added users appear under Admin → Users; deleting them there
ends their access.
</p>
</div>
<div>
@@ -210,7 +210,7 @@ export function RemoteAccess() {
onChange={(event) => setCode(event.target.value.toUpperCase())}
/>
<p className="text-xs text-muted-foreground">
Get a code at fluksio.com → Installations → Add installation.
Get a code at fluksio.com → Instances → Add instance.
</p>
</div>
<div>
@@ -235,8 +235,8 @@ export function RemoteAccess() {
<DialogTitle>Disconnect from the portal?</DialogTitle>
<DialogDescription>
Remote access ends immediately and the portal's credentials stop
working here. Nothing on this installation is changed or deleted,
and you can connect again with a new code.
working here. Nothing on this instance is changed or deleted, and
you can connect again with a new code.
</DialogDescription>
</DialogHeader>
<DialogFooter>
+5 -5
View File
@@ -1,5 +1,5 @@
/**
* Whether the installation this page talks to is reachable right now.
* Whether the instance this page talks to is reachable right now.
*
* Transport state, kept apart from `liveStore` on purpose: that one holds what
* the engine is doing, this one holds whether we can hear it at all. Only
@@ -11,7 +11,7 @@
type Connection = {
offline: boolean
/** When the portal last heard from the installation, epoch ms. */
/** When the portal last heard from the instance, epoch ms. */
lastSeen: number | null
}
@@ -38,7 +38,7 @@ function emit(next: Connection) {
export const connectionStore = {
setOffline(lastSeen: string | number | null) {
// The socket is up, so the installation is reachable: this was one bad
// The socket is up, so the instance is reachable: this was one bad
// answer rather than a lost tunnel, and the banner would have nothing to
// take it back down again.
if (socketOpen) return
@@ -56,7 +56,7 @@ export const connectionStore = {
/**
* The live socket opened or closed.
*
* An open socket is proof the installation can be heard, so it also clears
* An open socket is proof the instance can be heard, so it also clears
* the banner — and since the socket reconnects on its own, that is the one
* release the offline state can always count on.
*/
@@ -76,7 +76,7 @@ export const connectionStore = {
/**
* The proxy's offline answer: a 503 carrying `{offline, last_seen}`.
*
* Told apart from every other 503 by that body, so an installation that is
* Told apart from every other 503 by that body, so an instance that is
* merely busy is not reported as unreachable.
*/
export function offlineDetail(
+6 -6
View File
@@ -1,26 +1,26 @@
/**
* Runtime configuration injected when this app is served through a portal.
*
* A normal installation ships the same bundle and finds nothing here, so every
* A normal instance ships the same bundle and finds nothing here, so every
* portal-aware branch in the app collapses to its ordinary behaviour. When a
* portal serves the page it writes this object into the document first: the
* API lives under the installation's own path, the credential comes with the
* API lives under the instance's own path, the credential comes with the
* page rather than from storage, and there is somewhere to go "back" to.
*
* Deliberately not localStorage: two installations open in one browser share
* Deliberately not localStorage: two instances open in one browser share
* an origin, and a single `access_token` key would have them overwrite each
* other's session.
*/
export type PortalConfig = {
installationId: string
installationName: string
instanceId: string
instanceName: string
/** Router basepath, e.g. `/i/{id}`. */
basePath: string
/** Origin-relative API base; the SDK appends `/api/v1/...`. */
apiBase: string
/** Where "back to portal" goes. */
portalUrl: string
/** Short-lived token scoped to this installation. */
/** Short-lived token scoped to this instance. */
token: string
}
+4 -4
View File
@@ -1,13 +1,13 @@
/**
* Subscribing this browser to the installation's notifications.
* Subscribing this browser to the instance's notifications.
*
* A push arrives through the service worker, so it reaches a phone with no tab
* open — which is the whole point, and the reason this is not the in-app
* notification stack. The engine decides *what* is worth sending in its alert
* rules; this only says which browsers hear it.
*
* Everything here is per browser and per installation: the subscription is
* stored by the installation it was made against, so a phone that opens two
* Everything here is per browser and per instance: the subscription is
* stored by the instance it was made against, so a phone that opens two
* houses through the portal is two subscriptions and hears each separately.
*/
@@ -62,7 +62,7 @@ function keyBytes(key: string): Uint8Array<ArrayBuffer> {
}
/**
* Ask permission, subscribe, and tell the installation where to reach us.
* Ask permission, subscribe, and tell the instance where to reach us.
*
* Throws with something worth reading if the person says no — the caller puts
* it on screen, because a silently ignored button is worse than a refusal.
+2 -2
View File
@@ -51,7 +51,7 @@ const isPointlessToRetry = (error: unknown) =>
const handleApiError = (error: Error) => {
const offline = offlineDetail(error)
if (offline) {
// The installation is unreachable, not the session invalid: keep the user
// The instance is unreachable, not the session invalid: keep the user
// where they are and let the banner explain.
connectionStore.setOffline(offline.lastSeen)
return
@@ -67,7 +67,7 @@ const handleApiError = (error: Error) => {
if (portal) {
// The portal knows whether they are still signed in; it can mint a fresh
// handoff or send them to the login screen.
window.location.href = `${portal.portalUrl}?reauth=${portal.installationId}`
window.location.href = `${portal.portalUrl}?reauth=${portal.instanceId}`
return
}
safeStorage.remove("access_token")
+1 -1
View File
@@ -97,7 +97,7 @@ function showSetting(value: unknown): string {
/**
* The one channel whose setting is on the device rather than in the config:
* a browser has to ask its own permission, and what it hands back is stored
* against this installation.
* against this instance.
*/
function ThisBrowser() {
const { showSuccessToast, showErrorToast } = useCustomToast()
+1 -1
View File
@@ -33,7 +33,7 @@ const HEADER =
* it has been doing. The brain and the health screens compose in here rather
* than living at routes of their own.
*
* Top to bottom it is a widening lens: the whole installation as a graph, what
* Top to bottom it is a widening lens: the whole instance as a graph, what
* has been built on it, whether it is well, then flow by flow and finally
* moment by moment.
*/
+1 -1
View File
@@ -50,7 +50,7 @@ function UserSettings() {
<motion.div variants={slideUp}>
<Appearance />
</motion.div>
{/* Whether this whole installation can be reached from outside is not
{/* Whether this whole instance can be reached from outside is not
a personal preference, so it only exists for an operator. */}
{currentUser.is_superuser && (
<motion.div variants={slideUp}>
+3 -3
View File
@@ -11,8 +11,8 @@ import { apiToken, appPath, isPortal, openPortalSession } from "@/lib/portal"
/**
* The panel a credential in hand names, or "" when it names none.
*
* Two shapes say it, because two things mint one. This installation writes a
* `panel` claim; the hub, which knows nothing of this installation's users,
* Two shapes say it, because two things mint one. This instance writes a
* `panel` claim; the hub, which knows nothing of this instance's users,
* puts the panel in `sub` and says so with `scope`. A local token carries no
* `scope` at all, so the two cannot be confused — and without the second shape
* a screen adopted through the portal named no panel, and asked the household
@@ -53,7 +53,7 @@ function pairedPanel(): string {
* no session and there is nothing to put around it.
*
* Reached through the portal as well, for a screen hanging where this
* installation is not: the hub serves this one page without a session and
* instance is not: the hub serves this one page without a session and
* forwards these two calls down the tunnel, because a device with no
* credential is what they are for.
*/
+3 -3
View File
@@ -4,13 +4,13 @@ import { ago } from "./components/Health/queries"
import { offlineDetail } from "./lib/connectionStore"
function extractErrorMessage(err: ApiError): string {
// An unreachable installation is not a rejected action: say plainly that
// An unreachable instance is not a rejected action: say plainly that
// nothing was delivered, so nobody is left wondering whether it half-landed.
const offline = offlineDetail(err)
if (offline) {
return offline.lastSeen
? `Not delivered — the installation is offline (last seen ${ago(offline.lastSeen)})`
: "Not delivered — the installation is offline"
? `Not delivered — the instance is offline (last seen ${ago(offline.lastSeen)})`
: "Not delivered — the instance is offline"
}
if (err instanceof AxiosError) {
+1 -1
View File
@@ -17,7 +17,7 @@ import { api, apiPage, deleteAll } from "./utils/api"
const flowName = `test_panel_${Date.now().toString(36)}`
const first = `${flowName}_a`
const second = `${flowName}_b`
/** The installation's own panels, put back by the teardown. */
/** The instance's own panels, put back by the teardown. */
let panels: PanelDef[] | null = null
test.use({ storageState: "playwright/.auth/user.json" })
+1 -1
View File
@@ -6,7 +6,7 @@ import { defineConfig } from "vite"
// https://vitejs.dev/config/
export default defineConfig({
// A portal serves this bundle under a path of its own and caches one copy
// for every installation, so asset URLs have to be absolute under that
// for every instance, so asset URLs have to be absolute under that
// prefix. Unset — every ordinary build — this stays "/".
base: process.env.VITE_BASE || "/",
resolve: {
+1 -1
View File
@@ -7,7 +7,7 @@ import { defineConfig } from "vite"
// https://vitejs.dev/config/
export default defineConfig({
// A portal serves this bundle under a path of its own and caches one copy
// for every installation, so asset URLs have to be absolute under that
// for every instance, so asset URLs have to be absolute under that
// prefix. Unset — every ordinary build — this stays "/".
base: process.env.VITE_BASE || "/",
resolve: {