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.
This commit is contained in:
2026-08-31 10:12:01 +02:00
parent 271d4513eb
commit 6c8a101d24
101 changed files with 374 additions and 375 deletions
@@ -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>