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
@@ -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>