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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user