From d01a8dad37c5eb930ddf3a1a5277840534a341ea Mon Sep 17 00:00:00 2001 From: stroblme Date: Mon, 31 Aug 2026 10:12:01 +0200 Subject: [PATCH] Rename Installation to Instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj --- .env.example | 2 +- Makefile | 2 +- README.md | 2 +- .../versions/c7e2b9f34a15_flavor_table.py | 2 +- backend/fluksio/api/deps.py | 6 +-- backend/fluksio/api/routes/cloud.py | 20 +++---- backend/fluksio/api/routes/panels.py | 26 ++++----- backend/fluksio/api/routes/search.py | 6 +-- backend/fluksio/api/routes/secrets.py | 2 +- backend/fluksio/cli.py | 31 +++++------ backend/fluksio/cloud/__init__.py | 2 +- backend/fluksio/cloud/config.py | 22 ++++---- backend/fluksio/cloud/connector.py | 28 +++++----- backend/fluksio/cloud/enroll.py | 18 +++---- backend/fluksio/core/bootstrap.py | 6 +-- backend/fluksio/core/config.py | 10 ++-- backend/fluksio/core/db.py | 6 +-- backend/fluksio/flow/alerts.py | 2 +- backend/fluksio/flow/controller.py | 10 ++-- backend/fluksio/flow/dashboards.py | 8 +-- backend/fluksio/flow/executor.py | 2 +- backend/fluksio/flow/modules.py | 2 +- backend/fluksio/flow/nodes/base.py | 2 +- backend/fluksio/flow/nodes/http.py | 2 +- backend/fluksio/flow/nodes/influx.py | 4 +- backend/fluksio/flow/nodes/inject.py | 2 +- backend/fluksio/flow/nodes/logic.py | 4 +- backend/fluksio/flow/nodes/mqtt.py | 4 +- backend/fluksio/flow/panels.py | 2 +- backend/fluksio/flow/pipeline.py | 2 +- backend/fluksio/flow/placement.py | 4 +- backend/fluksio/flow/provision.py | 2 +- backend/fluksio/flow/runs.py | 4 +- backend/fluksio/flow/webpush.py | 14 ++--- backend/fluksio/main.py | 4 +- backend/fluksio/mcp/__init__.py | 2 +- backend/fluksio/models.py | 6 +-- backend/fluksio/sdk/cli.py | 4 +- backend/fluksio/sdk/client.py | 20 +++---- backend/fluksio/tui/app.py | 4 +- backend/tests/api/routes/test_flavors.py | 4 +- backend/tests/api/routes/test_panels.py | 6 +-- backend/tests/conftest.py | 10 ++-- backend/tests/flow/test_brain_graph.py | 2 +- backend/tests/flow/test_dashboards.py | 4 +- backend/tests/flow/test_provision.py | 2 +- backend/tests/flow/test_runtime_control.py | 2 +- backend/tests/flow/test_vocabulary.py | 2 +- backend/tests/flow/test_webpush.py | 2 +- backend/tests/sdk/test_paths.py | 10 ++-- backend/tests/test_cli.py | 4 +- backend/tests/test_cloud.py | 22 ++++---- backend/tests/utils/portal.py | 4 +- development.md | 2 +- docker/compose.yml | 4 +- docs/code/agents.md | 2 +- docs/code/api.md | 8 +-- docs/code/cli.md | 34 ++++++------ docs/code/nodes.md | 2 +- docs/concepts/flows.md | 2 +- docs/getting-started/data-science.md | 12 ++--- docs/getting-started/facility-automation.md | 6 +-- docs/getting-started/index.md | 4 +- docs/index.md | 2 +- docs/interface/dashboards.md | 12 ++--- docs/interface/index.md | 20 +++---- docs/interface/operations.md | 10 ++-- docs/interface/portal.md | 54 +++++++++---------- docs/llms.txt | 2 +- docs/reference/configuration.md | 8 +-- docs/reference/node-types.md | 4 +- frontend/index.html | 4 +- frontend/public/sw.js | 6 +-- frontend/src/client/schemas.gen.ts | 6 +-- frontend/src/client/sdk.gen.ts | 12 ++--- frontend/src/client/types.gen.ts | 6 +-- .../components/Common/ConnectionNotice.tsx | 6 +-- .../src/components/Common/DashboardMosaic.tsx | 4 +- .../src/components/Common/GlobalSearch.tsx | 4 +- .../src/components/Common/Notifications.tsx | 2 +- .../src/components/Common/OverviewToolbar.tsx | 8 +-- .../src/components/Dashboard/PanelsDialog.tsx | 18 +++---- frontend/src/components/Flow/NodePanel.tsx | 2 +- frontend/src/components/Flow/queries.ts | 2 +- frontend/src/components/Flow/useFlowSocket.ts | 8 +-- .../src/components/Health/HealthOverview.tsx | 2 +- .../src/components/Health/LiveIndicator.tsx | 2 +- .../src/components/Sidebar/AppSidebar.tsx | 6 +-- .../components/UserSettings/RemoteAccess.tsx | 30 +++++------ frontend/src/lib/connectionStore.ts | 10 ++-- frontend/src/lib/portal.ts | 12 ++--- frontend/src/lib/webpush.ts | 8 +-- frontend/src/main.tsx | 4 +- frontend/src/routes/_layout/alerts.tsx | 2 +- frontend/src/routes/_layout/index.tsx | 2 +- frontend/src/routes/_layout/settings.tsx | 2 +- frontend/src/routes/panel.index.tsx | 6 +-- frontend/src/utils.ts | 6 +-- frontend/tests/panel.spec.ts | 2 +- frontend/vite.config.js | 2 +- frontend/vite.config.ts | 2 +- 101 files changed, 374 insertions(+), 375 deletions(-) diff --git a/.env.example b/.env.example index 2058ce7..a457867 100644 --- a/.env.example +++ b/.env.example @@ -36,7 +36,7 @@ SMTP_TLS=True SMTP_SSL=False SMTP_PORT=587 -# Where this installation keeps everything: the database, flows, secrets, +# Where this instance keeps everything: the database, flows, secrets, # artifacts and the user venv. The compose stack sets it to the /data volume. DATA_DIR=flow-data # Any SQLAlchemy URL. Empty means SQLite in the data directory, which is what diff --git a/Makefile b/Makefile index 035710a..e46160c 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ help: ## Show available targets # Hostname the local stack is served under. Never .env's DOMAIN: a checkout # configured for a deployment carries that deployment's domain, and a local -# stack started under it answers to the same names the live installation does. +# stack started under it answers to the same names the live instance does. # Target-specific on purpose — an exported DOMAIN outranks --env-file in # compose interpolation, which would put the *production* targets on localhost. # `make dev DOMAIN=fluksio.com` still wins. diff --git a/README.md b/README.md index a679644..47c975b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ pip install fluksio fluksio serve ``` -Then, head over to [fluksio.com](https://fluksio.com), sign up and add a new installation. +Then, head over to [fluksio.com](https://fluksio.com), sign up and add a new instance. Using the code provided, run ```sh diff --git a/backend/fluksio/alembic/versions/c7e2b9f34a15_flavor_table.py b/backend/fluksio/alembic/versions/c7e2b9f34a15_flavor_table.py index 2f9424b..efb361f 100644 --- a/backend/fluksio/alembic/versions/c7e2b9f34a15_flavor_table.py +++ b/backend/fluksio/alembic/versions/c7e2b9f34a15_flavor_table.py @@ -2,7 +2,7 @@ A named amount of machine — cores, cards and memory — so a node can ask for one by name. Seeded with a few sizes on first start, and editable afterwards: what -"gpu-small" means is a property of the machines an installation has, and those +"gpu-small" means is a property of the machines an instance has, and those change. Revision ID: c7e2b9f34a15 diff --git a/backend/fluksio/api/deps.py b/backend/fluksio/api/deps.py index 6f9940e..c346562 100644 --- a/backend/fluksio/api/deps.py +++ b/backend/fluksio/api/deps.py @@ -51,7 +51,7 @@ def _panel_for(payload: dict[str, Any]) -> panels.PanelDef: effect at once. Or the panel's nonce moved on, which revokes exactly one screen's and leaves the panel, its dashboards and their arrangement alone. - The nonce is only held against a credential this installation signed. One + The nonce is only held against a credential this instance signed. One the portal minted for a remote screen carries none — the portal names the panel and nothing else — and is revoked at the hub instead. """ @@ -182,11 +182,11 @@ def decode_token(token: str, request: Request | None = None) -> dict[str, Any]: guards. The last branch is the seam a hosted deployment widens: a portal this - installation was enrolled with signs tokens with a key pinned at + instance was enrolled with signs tokens with a key pinned at enrolment, and they name the portal account holding them, which resolves to whichever local account was mapped to it — the superuser who enrolled, or a remote user one of them admitted since. With no enrolment the branch - raises immediately, so an offline installation pays nothing for the + raises immediately, so an offline instance pays nothing for the possibility. A screen that paired through that portal arrives there too, naming a panel — which is why the gate below is applied to whichever branch produced the claims rather than to one of them: where a diff --git a/backend/fluksio/api/routes/cloud.py b/backend/fluksio/api/routes/cloud.py index cb7d641..1dd7c36 100644 --- a/backend/fluksio/api/routes/cloud.py +++ b/backend/fluksio/api/routes/cloud.py @@ -1,8 +1,8 @@ -"""Connecting this installation to a Fluksio portal, and cutting it loose. +"""Connecting this instance to a Fluksio portal, and cutting it loose. Entirely optional, and superuser-only to change: enrolling grants a remote party the rights of the account that performed it, which is not a decision an -ordinary user of this installation gets to make on everyone else's behalf. +ordinary user of this instance gets to make on everyone else's behalf. Admitting further portal accounts is the same decision made again, so it is guarded the same way. A person let in this way gets an ordinary local account @@ -47,7 +47,7 @@ class EnrollBody(BaseModel): @router.get("/status", dependencies=[Depends(get_current_user)]) def read_status(request: Request) -> dict[str, Any]: - """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. @@ -61,7 +61,7 @@ def read_status(request: Request) -> dict[str, Any]: "portal_url": config.portal_url if config else None, "issuer": config.issuer if config else None, "portal_account": config.portal_account if config else None, - "installation_id": config.installation_id if config else None, + "instance_id": config.instance_id if config else None, "last_error": None, "connected_since": None, } @@ -80,7 +80,7 @@ async def enroll( """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. @@ -121,11 +121,11 @@ def add_remote_user(session: SessionDep, body: RemoteUserBody) -> Any: if config is None: raise HTTPException( status_code=409, - detail="This installation is not connected to a portal", + detail="This instance is not connected to a portal", ) try: response = httpx.post( - f"{config.portal_url.rstrip('/')}/api/v1/installation-members/", + f"{config.portal_url.rstrip('/')}/api/v1/instance-members/", headers={"Authorization": f"Bearer {config.token}"}, json={"code": body.code.strip()}, timeout=15.0, @@ -140,7 +140,7 @@ def add_remote_user(session: SessionDep, body: RemoteUserBody) -> Any: ) if response.status_code == 409: raise HTTPException( - status_code=409, detail="That code belongs to this installation's owner" + status_code=409, detail="That code belongs to this instance's owner" ) if response.status_code != 200: raise HTTPException( @@ -183,14 +183,14 @@ def forget_remote_user(portal_sub: str) -> None: Best effort on purpose: the local account is what grants access, so it is already over by the time this runs. A portal that cannot be reached keeps a - row that opens nothing — the installation refuses the session either way. + row that opens nothing — the instance refuses the session either way. """ config = cloud_config.load() if config is None: return try: response = httpx.delete( - f"{config.portal_url.rstrip('/')}/api/v1/installation-members/{portal_sub}", + f"{config.portal_url.rstrip('/')}/api/v1/instance-members/{portal_sub}", headers={"Authorization": f"Bearer {config.token}"}, timeout=15.0, ) diff --git a/backend/fluksio/api/routes/panels.py b/backend/fluksio/api/routes/panels.py index 85e21e7..5ec9e25 100644 --- a/backend/fluksio/api/routes/panels.py +++ b/backend/fluksio/api/routes/panels.py @@ -5,7 +5,7 @@ shows it on the wall, and somebody with an account types that code into the panels dialog to say which panel the device is. The device polls, collects the credential the approval minted, and never asks again. -A screen hanging somewhere this installation is not reachable from does the +A screen hanging somewhere this instance is not reachable from does the same thing through the portal, which forwards those two calls down the tunnel without a session — a device with no credential is the whole point of them — and mints the credential itself when the approval comes. Which side minted it @@ -81,7 +81,7 @@ class _Pending(BaseModel): #: Self-reported and worth what that is worth. device: str = "" #: Whether it came down the tunnel. A device that reached the portal - #: cannot reach this installation, so its credential has to be minted + #: cannot reach this instance, so its credential has to be minted #: where it can collect it. remote: bool = False @@ -99,7 +99,7 @@ class _PendingStore: A device's poll lands on whichever API worker the proxy picked, and a screen pairing through the portal lands on whichever one holds the tunnel — so a code minted by one worker has to be findable from the next. Redis - is where this installation already keeps what must outlive a process. + is where this instance already keeps what must outlive a process. Without one there is one process by definition (the pip install, and the tests), and a dictionary is the same thing for it. @@ -224,8 +224,8 @@ def _describe(request: Request) -> str: def _mint_at_hub(panel_id: str) -> str: """Ask the portal for this panel's credential. - A device that arrived through the portal cannot reach this installation, so - a token this installation signed would be one it could never present: the + A device that arrived through the portal cannot reach this instance, so + a token this instance signed would be one it could never present: the portal verifies what crosses its tunnel, and it verifies against its own key. It mints, we say which panel — and the scope check here decides the rest, on this call and on every later one. @@ -234,7 +234,7 @@ def _mint_at_hub(panel_id: str) -> str: if config is None: raise HTTPException( status_code=409, - detail="That device came through a portal this installation is no " + detail="That device came through a portal this instance is no " "longer enrolled with", ) try: @@ -264,14 +264,14 @@ class PanelsPublic(BaseModel): 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. """ panels: list[PanelDef] = Field(default_factory=list) - #: Whatever this installation was told it is reachable at. The same setting - #: the password-reset links are built from, so an installation that has it + #: Whatever this instance was told it is reachable at. The same setting + #: the password-reset links are built from, so an instance that has it #: wrong has it wrong in both places. frontend_host: str = "" @@ -310,7 +310,7 @@ async def save_panels(body: PanelsConfig) -> Any: def _save() -> None: # Read and write under one lock. The nonce belongs to this - # installation, not to whoever is writing the panels back: a client + # instance, not to whoever is writing the panels back: a client # holding an older copy must not be able to undo a revocation by # saving an arrangement — and reading it in a separate step from # writing it is exactly how an unpair in between was undone. @@ -335,7 +335,7 @@ def start_pairing(request: Request) -> Any: 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. """ if _pending.count() >= MAX_PENDING: @@ -412,8 +412,8 @@ def approve_pairing(panel_id: str, body: PairRequest, current_user: CurrentUser) 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. """ panel = find(panel_id) diff --git a/backend/fluksio/api/routes/search.py b/backend/fluksio/api/routes/search.py index 2cdd5c5..5933532 100644 --- a/backend/fluksio/api/routes/search.py +++ b/backend/fluksio/api/routes/search.py @@ -1,4 +1,4 @@ -"""One index of everything in this installation worth jumping to by name.""" +"""One index of everything in this instance worth jumping to by name.""" from typing import Any, Literal @@ -20,7 +20,7 @@ from fluksio.flow.secrets import get_secrets from fluksio.flow.store import FlowNotFound # A wall panel never reaches this route: ``deps._panel_may`` is a whitelist that -# ends in a 403, and a whole-installation index is the opposite of what a screen +# ends in a 403, and a whole-instance index is the opposite of what a screen # on a wall is allowed to read. router = APIRouter( prefix="/search", tags=["search"], dependencies=[Depends(get_current_user)] @@ -144,7 +144,7 @@ async def read_search_index( """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. diff --git a/backend/fluksio/api/routes/secrets.py b/backend/fluksio/api/routes/secrets.py index 635398f..20c78d5 100644 --- a/backend/fluksio/api/routes/secrets.py +++ b/backend/fluksio/api/routes/secrets.py @@ -13,7 +13,7 @@ from fluksio.api.deps import get_current_active_superuser from fluksio.flow.secrets import SecretNotFound, get_secrets from fluksio.models import Message -# Superuser, not merely signed in. The names alone say what this installation +# Superuser, not merely signed in. The names alone say what this instance # talks to, and `PUT /{name}` takes any name — so an ordinary account could # overwrite the credential a flow authenticates with. `/search` already gates # secret names this way and said so; this router was the half that did not. diff --git a/backend/fluksio/cli.py b/backend/fluksio/cli.py index 20f43df..2877f1c 100644 --- a/backend/fluksio/cli.py +++ b/backend/fluksio/cli.py @@ -28,7 +28,7 @@ from typing import Any import fluksio -#: Where an installation keeps everything, unless it is told otherwise. +#: Where an instance keeps everything, unless it is told otherwise. DEFAULT_HOME = Path("~/.fluksio") #: The portal a claim code is redeemed at, unless another is named. Having a @@ -52,7 +52,7 @@ def _say(message: str = "") -> None: def _data_dir(raw: str | None, shared: bool = False) -> Path: - """Which installation this command is for. + """Which instance this command is for. A repository with its own venv wants its own engine too — its own flows, its own run history, its own token — so the default is a `.fluksio` beside @@ -73,12 +73,12 @@ def _data_dir(raw: str | None, shared: bool = False) -> Path: return path.resolve() -def _mention_other_installation(data_dir: Path) -> None: +def _mention_other_instance(data_dir: Path) -> None: """Say when there is a second engine, so neither goes looking lost.""" shared = DEFAULT_HOME.expanduser().resolve() if data_dir == shared or not (shared / "fluksio.db").exists(): return - _say(f" Note {shared} holds another installation; this one is separate.") + _say(f" Note {shared} holds another instance; this one is separate.") _say(" `fluksio serve --global` runs that one instead.") @@ -217,8 +217,7 @@ def _enroll(portal: str, code: str, as_email: str | None) -> int: return 1 _say( - f"Connected to {config.portal_url} as {email} " - f"(installation {config.installation_id})." + f"Connected to {config.portal_url} as {email} (instance {config.instance_id})." ) return 0 @@ -340,7 +339,7 @@ def probe_engine(url: str, token: str, client: Any = None) -> str: makes stopping it something this command may offer. The proof is the token: it is signed with this directory's secret key, so an engine that accepts it is one reading this directory's database. A Fluksio belonging - to another installation answers the health check and refuses the token, + to another instance answers the health check and refuses the token, and is only ever named — never stopped from here. """ import logging @@ -443,7 +442,7 @@ def cmd_serve(args: argparse.Namespace) -> int: if port != DEFAULT_PORT: # Moving off the port quietly makes starting a second engine for # one directory look like it worked. Two of them on one SQLite - # file is not a supported shape — two *installations* on one + # file is not a supported shape — two *instances* on one # machine is — so the one already up is named instead. url = f"http://{reachable}:{DEFAULT_PORT}" who = probe_engine(url, _token_for(data_dir)) @@ -455,7 +454,7 @@ def cmd_serve(args: argparse.Namespace) -> int: return 0 if who == "foreign": _say( - f"Port {DEFAULT_PORT} holds another installation's Fluksio; " + f"Port {DEFAULT_PORT} holds another instance's Fluksio; " f"serving on {port} instead." ) else: @@ -480,15 +479,15 @@ def cmd_serve(args: argparse.Namespace) -> int: _say(f" Nodes {modules.venv_dir() / 'bin' / 'python'}") _say(" a venv of its own; the Modules screen installs into it.") if config is not None: - _say(f" Portal {config.portal_url}, installation {config.installation_id}") + _say(f" Portal {config.portal_url}, instance {config.instance_id}") _say(" The dashboard is served by the portal; nothing is served here.") else: - _say(" No portal. Pair this installation with:") + _say(" No portal. Pair this instance with:") _say(" fluksio enroll ") _say(f" Signed in as {admin_email}") _say(f" token in {token_path}") _mention_undeclared_cards() - _mention_other_installation(data_dir) + _mention_other_instance(data_dir) # One process: it holds the flow engine, and a second worker would be a # second engine — duplicated subscriptions, cron ticks and webhooks. @@ -558,13 +557,13 @@ def _parser() -> argparse.ArgumentParser: sub.add_argument( "--data-dir", default=os.environ.get("FLUKSIO_HOME"), - help="where this installation keeps everything (default: ./.fluksio)", + help="where this instance keeps everything (default: ./.fluksio)", ) sub.add_argument( "--global", dest="shared", action="store_true", - help=f"use the shared installation at {DEFAULT_HOME} instead", + help=f"use the shared instance at {DEFAULT_HOME} instead", ) serve = subparsers.add_parser("serve", help="run the engine") @@ -622,9 +621,7 @@ def _parser() -> argparse.ArgumentParser: ) serve.set_defaults(func=cmd_serve) - enroll = subparsers.add_parser( - "enroll", help="pair this installation with a portal" - ) + enroll = subparsers.add_parser("enroll", help="pair this instance with a portal") enroll.add_argument("code", help="the claim code minted on the portal") enroll.add_argument( "--portal", diff --git a/backend/fluksio/cloud/__init__.py b/backend/fluksio/cloud/__init__.py index efefeef..0c47b72 100644 --- a/backend/fluksio/cloud/__init__.py +++ b/backend/fluksio/cloud/__init__.py @@ -1,6 +1,6 @@ """Optional remote access through a Fluksio portal. -Nothing in here runs unless someone enrolled this installation: with no +Nothing in here runs unless someone enrolled this instance: with no ``cloud.json`` the connector never starts and the portal's tokens are refused like any other bad credential. Turning it off is deleting that one file. """ diff --git a/backend/fluksio/cloud/config.py b/backend/fluksio/cloud/config.py index f70d7cb..e8abf47 100644 --- a/backend/fluksio/cloud/config.py +++ b/backend/fluksio/cloud/config.py @@ -1,4 +1,4 @@ -"""What this installation knows about the portal it is enrolled with. +"""What this instance knows about the portal it is enrolled with. One file on the data volume, beside the OAuth keypair and for the same reason: it is a credential, it must survive a rebuild, and it must never be in the @@ -9,7 +9,7 @@ portal still believes. The portal's public keys are stored here rather than fetched: they are pinned at enrolment, when a person was holding a claim code they had just read off the portal's own screen. A hub whose DNS or TLS is later hijacked cannot re-key an -installation that already enrolled; it can only fail to verify. +instance that already enrolled; it can only fail to verify. """ from __future__ import annotations @@ -42,11 +42,11 @@ VIA_PORTAL = secrets.token_urlsafe(16) class CloudConfig: portal_url: str ws_url: str - installation_id: str + instance_id: str token: str issuer: str jwks: dict[str, Any] - #: The local account this installation acts as on its own behalf: what the + #: The local account this instance acts as on its own behalf: what the #: health summary is collected as, and what a screen paired through the #: portal borrows for want of a person. Recorded at enrolment from whoever #: performed it. Portal *sessions* no longer come through here — they name @@ -72,6 +72,10 @@ def _read() -> CloudConfig | None: return _cache[1] try: data = json.loads(path.read_text()) + # A file written before the installation/instance rename. Adopt it + # rather than failing to parse, which would read as "never enrolled". + if "installation_id" in data: + data["instance_id"] = data.pop("installation_id") config = CloudConfig(**data) except (OSError, ValueError, TypeError): logger.exception("Could not read %s; remote access stays off", path) @@ -109,15 +113,15 @@ def delete() -> None: def decode_portal_token(token: str) -> dict[str, Any]: - """Validate a token the portal minted for this installation. + """Validate a token the portal minted for this instance. Raises ``InvalidTokenError`` for everything else, including the ordinary case of not being enrolled at all — which is what makes this branch free - for an installation nobody connected. + for an instance nobody connected. """ config = _read() if config is None: - raise InvalidTokenError("this installation is not enrolled with a portal") + raise InvalidTokenError("this instance is not enrolled with a portal") try: key = jwt.PyJWKSet.from_dict(config.jwks).keys[0] @@ -128,9 +132,9 @@ def decode_portal_token(token: str) -> dict[str, Any]: token, key, algorithms=["RS256"], - # The audience is this installation's own id, so a token the portal + # The audience is this instance's own id, so a token the portal # minted for somebody else's machine fails here. - audience=config.installation_id, + audience=config.instance_id, issuer=config.issuer, ) scope = claims.get("scope") diff --git a/backend/fluksio/cloud/connector.py b/backend/fluksio/cloud/connector.py index 3cbfdb7..3045c9e 100644 --- a/backend/fluksio/cloud/connector.py +++ b/backend/fluksio/cloud/connector.py @@ -1,4 +1,4 @@ -"""The link this installation opens to its portal. +"""The link this instance opens to its portal. Same shape as the remote-worker agent, with the direction of the favour reversed: there, a GPU box dials the engine so the engine can give it work; @@ -50,7 +50,7 @@ MAX_WS_MESSAGE = 1024 * 1024 #: Only the versioned API is served over the tunnel. The MCP mount and the #: OAuth endpoints live outside it and stay local-only. ALLOWED_PREFIX = "/api/v1/" -#: Proxied calls, and bridged streams, this installation will run at once. +#: Proxied calls, and bridged streams, this instance will run at once. #: Neither dict was bounded, and an id the hub reused silently dropped the #: reference to a task that was still running. MAX_IN_FLIGHT = 256 @@ -76,7 +76,7 @@ async def watch_enrolment(app: FastAPI) -> None: """Notice an enrolment that happened outside this process. ``fluksio enroll`` writes the config against the database directly, with no - idea whether an engine is running — so without this, pairing an installation + idea whether an engine is running — so without this, pairing an instance that is already serving would take a restart to come into effect. Enrolling through the API starts the link itself and this sees a task already there. """ @@ -124,7 +124,7 @@ class CloudConnector: # this process does: enrolment may have named a container. "issuer": config.issuer if config else None, "portal_account": config.portal_account if config else None, - "installation_id": config.installation_id if config else None, + "instance_id": config.instance_id if config else None, "last_error": self._last_error, "connected_since": self._connected_since, } @@ -165,7 +165,7 @@ class CloudConnector: delay = BASE_BACKOFF_S if attached else min(MAX_BACKOFF_S, delay * 2) # Jittered, so a portal coming back up is not met by every - # installation it serves in the same instant. + # instance it serves in the same instant. wait = delay * (0.75 + random.random() * 0.5) logger.info("Reconnecting to the portal in %.0fs", wait) await asyncio.sleep(wait) @@ -197,9 +197,7 @@ class CloudConnector: self._connected = True self._connected_since = time.time() self._last_error = None - logger.info( - "Attached to the portal as installation %s", config.installation_id - ) + logger.info("Attached to the portal as instance %s", config.instance_id) keepalive = asyncio.create_task(self._keepalive(socket, config)) try: @@ -270,7 +268,7 @@ class CloudConnector: @staticmethod def _local_account(session: Any, config: cloud_config.CloudConfig) -> Any: - """The account this installation acts as, or the one that now stands for it. + """The account this instance acts as, or the one that now stands for it. ``local_user_id`` names whoever redeemed the claim code. A database restored from a backup, or rebuilt under an enrolment that outlived it, @@ -301,7 +299,7 @@ class CloudConnector: ) if len(superusers) != 1: logger.warning( - "The account this installation enrolled as is gone, and there " + "The account this instance enrolled as is gone, and there " "%s to adopt unambiguously — enrol again from Settings.", "is no superuser" if not superusers else "are several superusers", ) @@ -309,7 +307,7 @@ class CloudConnector: adopted = superusers[0] cloud_config.save(replace(config, local_user_id=str(adopted.id))) logger.warning( - "The account this installation enrolled as is gone; acting as %s instead", + "The account this instance enrolled as is gone; acting as %s instead", adopted.email, ) return adopted @@ -334,14 +332,14 @@ class CloudConnector: async def _health_summary( self, config: cloud_config.CloudConfig ) -> dict[str, Any] | None: - """This installation's own health, as the dashboard reads it. + """This instance's own health, as the dashboard reads it. Fetched through the same in-process transport as everything else, with a short-lived local token: the observability API is authenticated, and this process is entitled to mint one for the enrolling user. ``failures_24h`` is added here rather than by ``/summary``: it is the - portal's tile and nothing on this installation reads it, and summing + portal's tile and nothing on this instance reads it, and summing the same rollups over the same window the app's own Home tile sums keeps the two from drifting apart the way they already did once. """ @@ -376,7 +374,7 @@ class CloudConnector: def _client(self) -> httpx.AsyncClient: return httpx.AsyncClient( transport=httpx.ASGITransport(app=self._app), - base_url="http://installation.local", + base_url="http://instance.local", timeout=httpx.Timeout(300.0, connect=5.0), ) @@ -404,7 +402,7 @@ class CloudConnector: try: if not path.startswith(ALLOWED_PREFIX): # Defence in depth: the hub only ever forwards API paths, and - # an installation that trusted anything else would be an SSRF + # an instance that trusted anything else would be an SSRF # gadget aimed at its own process. await socket.send( _dump({"op": "err", "id": call_id, "message": "path not allowed"}) diff --git a/backend/fluksio/cloud/enroll.py b/backend/fluksio/cloud/enroll.py index 4a06d90..ae82dd5 100644 --- a/backend/fluksio/cloud/enroll.py +++ b/backend/fluksio/cloud/enroll.py @@ -1,7 +1,7 @@ """Redeeming a claim code, from the dashboard or from the command line. The work is the same either way — ask the portal, keep what it answers, and -map the account that asked to the portal identity that owns the installation — +map the account that asked to the portal identity that owns the instance — so it lives here rather than in the route. The command line matters because a machine on a cluster has no browser pointed at it: `fluksio enroll` does this before the engine has started, holding nothing but the database. @@ -32,7 +32,7 @@ class EnrollError(Exception): class AlreadyEnrolled(EnrollError): def __init__(self) -> None: - super().__init__(409, "This installation is already connected to a portal") + super().__init__(409, "This instance is already connected to a portal") def _is_local(url: str) -> bool: @@ -44,9 +44,9 @@ def _is_local(url: str) -> bool: def redeem_claim( portal_url: str, claim_code: str, *, timeout: float = 15.0 ) -> dict[str, Any]: - """Trade a claim code for this installation's credential and the portal's keys.""" + """Trade a claim code for this instance's credential and the portal's keys.""" base = portal_url.rstrip("/") - # The claim code and, from here on, this installation's credential go to + # The claim code and, from here on, this instance's credential go to # this address. Over plain http both are readable by anything on the path, # so refuse rather than enrol insecurely — bar a loopback portal, which is # how the stack is developed against itself. @@ -70,12 +70,12 @@ def redeem_claim( data: dict[str, Any] = response.json() if not data.get("owner_id"): # A portal older than remote users does not say who owns the - # installation, and without that the enrolling account cannot be mapped + # instance, and without that the enrolling account cannot be mapped # to anyone — which would leave the portal connected but refused here. raise EnrollError( 502, - "That portal is too old for this installation: it did not say " - "which account owns the installation", + "That portal is too old for this instance: it did not say " + "which account owns the instance", ) return data @@ -91,8 +91,8 @@ def enroll( config = cloud_config.CloudConfig( portal_url=portal_url.rstrip("/"), ws_url=data["ws_url"], - installation_id=data["installation_id"], - token=data["installation_token"], + instance_id=data["instance_id"], + token=data["instance_token"], issuer=data["issuer"], # Pinned here, at the one moment the claim code proves who we are # talking to. Nothing refreshes this. diff --git a/backend/fluksio/core/bootstrap.py b/backend/fluksio/core/bootstrap.py index 3db093c..530fa0e 100644 --- a/backend/fluksio/core/bootstrap.py +++ b/backend/fluksio/core/bootstrap.py @@ -1,6 +1,6 @@ """First run: an account to sign in with. -An installation started from the command line is given no environment, so the +An instance started from the command line is given no environment, so the superuser a deployment sets in `.env` has to come from somewhere. The session key is the CLI's own business — it has to be set before this module can be imported at all. @@ -27,7 +27,7 @@ def ensure_superuser( """The account to sign in with, made on first run. Returns the user and, when it was just created, the password in clear — - the caller prints it once. An installation that already has a superuser is + the caller prints it once. An instance that already has a superuser is left alone: this is a first run, not a password reset. """ existing = session.exec( @@ -57,7 +57,7 @@ def pick_superuser(session: Session, email: str | None = None) -> User: select(User).where(User.is_superuser == True) # noqa: E712 ).all() if not users: - raise LookupError("This installation has no superuser to enrol as") + raise LookupError("This instance has no superuser to enrol as") if len(users) > 1: addresses = ", ".join(sorted(u.email for u in users)) raise LookupError(f"Several superusers here — name one with --as: {addresses}") diff --git a/backend/fluksio/core/config.py b/backend/fluksio/core/config.py index 024c426..1a2710b 100644 --- a/backend/fluksio/core/config.py +++ b/backend/fluksio/core/config.py @@ -54,7 +54,7 @@ class Settings(BaseSettings): FRONTEND_HOST: str = "http://localhost:5173" ENVIRONMENT: Literal["local", "staging", "production"] = "local" - #: Everything this installation keeps: the database, the flow repository, + #: Everything this instance keeps: the database, the flow repository, #: secrets, artifacts and the user venv. The paths below derive from it #: unless they are set explicitly. DATA_DIR: Path = Path("flow-data") @@ -72,13 +72,13 @@ class Settings(BaseSettings): # Which failures reach which channel. Beside the flows, not in them: # alerting is the deployment's concern, not any one flow's. ALERTS_FILE: Path = Path("flow-data/alerts.json") - # This installation's web push keypair and the browsers subscribed to it. + # This instance's web push keypair and the browsers subscribed to it. # Beside the alerts it serves; deleting it makes every device subscribe # again. WEBPUSH_FILE: Path = Path("flow-data/webpush.json") # Where machines can be started from when a node needs one and nothing that # could take it is attached. Operator-authored, like the alerts beside it, - # and absent on an installation that has nowhere to start one. + # and absent on an instance that has nowhere to start one. PROVISIONERS_FILE: Path = Path("flow-data/provisioners.json") # Which dashboards each device shows. Beside the flows for the same reason # alerting is: where a screen hangs is the deployment's concern rather than @@ -99,7 +99,7 @@ class Settings(BaseSettings): PRIVATE_API_ENABLED: bool = False DOMAIN: str = "localhost" OAUTH_PRIVATE_KEY_FILE: Path = Path("flow-data/oauth-key.pem") - # Written only when someone enrols this installation with a portal. + # Written only when someone enrols this instance with a portal. # Its absence is what keeps remote access off. CLOUD_CONFIG_FILE: Path = Path("flow-data/cloud.json") OAUTH_CODE_EXPIRE_SECONDS: int = 60 @@ -113,7 +113,7 @@ class Settings(BaseSettings): # because a limit somebody set and did not get is the worse surprise. FLOW_MAX_WORKERS: PositiveInt = 4 # How many cascades may be in flight at once. Sustained throughput is this - # over the mean cascade time, so an installation whose nodes wait on the + # over the mean cascade time, so an instance whose nodes wait on the # network rather than on a CPU wants it higher than the core count. FLOW_MAX_CASCADES: PositiveInt = 4 # How many batch runs are driven at once. A different limit from the one diff --git a/backend/fluksio/core/db.py b/backend/fluksio/core/db.py index c24ea76..0b2d91a 100644 --- a/backend/fluksio/core/db.py +++ b/backend/fluksio/core/db.py @@ -142,9 +142,9 @@ def init_db(session: Session) -> None: user = crud.create_user(session=session, user_create=user_in) -#: The sizes an installation starts with. Written once, when there are none, +#: The sizes an instance starts with. Written once, when there are none, #: and editable from there — what a name means is a property of the machines -#: this installation has, and nothing here knows what those are. +#: this instance has, and nothing here knows what those are. SEED_FLAVORS: tuple[dict[str, Any], ...] = ( {"name": "small", "cpus": 1, "ram": 2048, "description": "A poll, a threshold"}, {"name": "medium", "cpus": 4, "ram": 8192, "description": "A step that computes"}, @@ -160,7 +160,7 @@ SEED_FLAVORS: tuple[dict[str, Any], ...] = ( def seed_flavors(session: Session) -> None: - """Give a new installation sizes to pick from, once. + """Give a new instance sizes to pick from, once. Only when there are none at all: they are editable, and re-adding one that somebody deliberately removed would be an argument nobody can win. diff --git a/backend/fluksio/flow/alerts.py b/backend/fluksio/flow/alerts.py index 8d31cb1..36f6635 100644 --- a/backend/fluksio/flow/alerts.py +++ b/backend/fluksio/flow/alerts.py @@ -358,7 +358,7 @@ class AlertManager: await asyncio.to_thread(self.publish, message, alert.model_dump()) async def _send_webpush(self, alert: Alert) -> None: - """Wake every browser that subscribed to this installation. + """Wake every browser that subscribed to this instance. No settings of its own: a browser subscribes by pressing a button on the alerts screen, and this channel goes to whichever ones did. Saying diff --git a/backend/fluksio/flow/controller.py b/backend/fluksio/flow/controller.py index 5b57dfe..fe9533a 100644 --- a/backend/fluksio/flow/controller.py +++ b/backend/fluksio/flow/controller.py @@ -98,7 +98,7 @@ HOOK_PREFIX = "/hooks" NODE_STOP_TIMEOUT = 5.0 # How long a rebuild asked for by a request waits for one already running. -# Generous on purpose: a rebuild of a populated installation reconnects every +# Generous on purpose: a rebuild of a populated instance reconnects every # node and takes the better part of ten seconds, and a caller queued behind a # healthy one of those should not be turned away. Past that the controller is # wedged rather than busy, and an error the caller can act on beats a request @@ -106,7 +106,7 @@ NODE_STOP_TIMEOUT = 5.0 REBUILD_WAIT = 15.0 # How long a caller waits for a *flow* rebuild that is already running. A -# per-flow rebuild reconnects one flow's nodes rather than the installation's, +# per-flow rebuild reconnects one flow's nodes rather than the instance's, # so this is a queueing budget — several of them back to back, which is what # seeding does — not the room a single one needs. Fifteen seconds was sized for # the whole-pipeline rebuild and would let a wedge sit unreported. @@ -680,7 +680,7 @@ class FlowController: another flow owns — but the wiring is derived from message names, so swapping one flow's nodes into the graph and deriving the edges again is enough. What that saves is the reconnecting: the cost of a rebuild - on a populated installation is every node opening its socket again, + on a populated instance is every node opening its socket again, and only one flow's have changed. A flow the store no longer has is taken out instead of replaced. @@ -1522,12 +1522,12 @@ class FlowController: key = None if node_type is not None: try: - key = node_type.cls.instance_key(node_def.params) + key = node_type.cls.target_key(node_def.params) except Exception: # A plugin's own grouping is not worth the whole graph; # this node just stands on its own. logger.exception( - "instance_key failed for node type '%s'", node_def.type + "target_key failed for node type '%s'", node_def.type ) gid = f"{node_def.type}:{key}" if key else member gid_of[member] = gid diff --git a/backend/fluksio/flow/dashboards.py b/backend/fluksio/flow/dashboards.py index ee79451..314b5c6 100644 --- a/backend/fluksio/flow/dashboards.py +++ b/backend/fluksio/flow/dashboards.py @@ -81,7 +81,7 @@ WidgetType = Literal[ INPUT_WIDGETS = {"button", "switch", "slider", "input", "dropdown", "color"} #: What a colour widget puts on the wire, by the format it was configured for. -#: The default is what the Node-RED installation this ports already sends its +#: The default is what the Node-RED instance this ports already sends its #: DMX encoders — ``[h, s, v]``, hue in degrees and the other two in percent — #: and the two alternatives exist because fixtures differ. Mirrored in the #: client (``frontend/src/components/Dashboard/ColorWidget.tsx``). @@ -197,7 +197,7 @@ class WidgetDef(BaseModel): 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. @@ -479,7 +479,7 @@ class DashboardDef(BaseModel): def _flatten(cls, data: Any) -> Any: """Read a document written as pages and sections as one grid. - Stored dashboards live in each installation's git repository, so the + Stored dashboards live in each instance's git repository, so the old shape is normalised on the way in rather than migrated: an untouched document keeps working, and the next save writes it flat. """ @@ -502,7 +502,7 @@ class DashboardDef(BaseModel): picker records the payload type beside the name, so neither the editor nor a wall panel has to fetch the catalogue to know the wiring is wrong. A name this build does not know is left alone rather than - refused — an older installation reading a newer document simply does + refused — an older instance reading a newer document simply does not act on it. """ for name, setting in self.settings.items(): diff --git a/backend/fluksio/flow/executor.py b/backend/fluksio/flow/executor.py index ab9a2dd..0778dfb 100644 --- a/backend/fluksio/flow/executor.py +++ b/backend/fluksio/flow/executor.py @@ -49,7 +49,7 @@ DUE_RESERVE = 2 # check whether a cascade slot has freed. DUE_CLAIM_BLOCK_MS = 200 #: How many cascades may be in flight, unless the service is given a number. -#: Sustained throughput is this over the mean cascade time, so an installation +#: Sustained throughput is this over the mean cascade time, so an instance #: whose nodes wait on a network rather than a CPU may want more of them — #: `FLOW_MAX_CASCADES` is where that is said. MAX_CASCADES = 4 diff --git a/backend/fluksio/flow/modules.py b/backend/fluksio/flow/modules.py index c3af6b2..cbb9598 100644 --- a/backend/fluksio/flow/modules.py +++ b/backend/fluksio/flow/modules.py @@ -97,7 +97,7 @@ def venv_dir() -> Path: def venv_python() -> str: """The interpreter node code runs on. - Falls back to the engine's own when there is no venv — an installation that + Falls back to the engine's own when there is no venv — an instance that could not build one still runs python nodes, it just cannot add packages to them. """ diff --git a/backend/fluksio/flow/nodes/base.py b/backend/fluksio/flow/nodes/base.py index b46a57d..a222143 100644 --- a/backend/fluksio/flow/nodes/base.py +++ b/backend/fluksio/flow/nodes/base.py @@ -179,7 +179,7 @@ class Node: idempotent: bool = True @classmethod - def instance_key(cls, params: dict[str, Any]) -> str | None: + def target_key(cls, params: dict[str, Any]) -> str | None: """Which outside thing these parameters point at, if any. Two nodes with the same key talk to the same broker topic, URL or diff --git a/backend/fluksio/flow/nodes/http.py b/backend/fluksio/flow/nodes/http.py index 7a4e19c..d9b13c8 100644 --- a/backend/fluksio/flow/nodes/http.py +++ b/backend/fluksio/flow/nodes/http.py @@ -189,7 +189,7 @@ class HttpNode(Node): ) @classmethod - def instance_key(cls, params: dict[str, Any]) -> str | None: + def target_key(cls, params: dict[str, Any]) -> str | None: # ponytail: a webhook's stored url is the path before the flow name is # prefixed onto it, so two flows both receiving on "/tick" merge into # one neuron. Key on mode as well if that ever misleads. diff --git a/backend/fluksio/flow/nodes/influx.py b/backend/fluksio/flow/nodes/influx.py index bf28b40..3043439 100644 --- a/backend/fluksio/flow/nodes/influx.py +++ b/backend/fluksio/flow/nodes/influx.py @@ -18,7 +18,7 @@ def _influxdb() -> Any: """The client library, which is a `fluksio[server]` extra. Imported per use rather than at module level, because the node type is - registered at boot and an installation with no InfluxDB behind it should + registered at boot and an instance with no InfluxDB behind it should not have to carry the library to start. """ try: @@ -198,7 +198,7 @@ class InfluxDbNode(Node): queries: dict[str, dict[str, Any]] = {} @classmethod - def instance_key(cls, params: dict[str, Any]) -> str | None: + def target_key(cls, params: dict[str, Any]) -> str | None: """The bucket, which is the thing several flows share.""" url, bucket = params.get("url"), params.get("bucket") return f"{url}/{bucket}" if url and bucket else None diff --git a/backend/fluksio/flow/nodes/inject.py b/backend/fluksio/flow/nodes/inject.py index 28b7c50..466989c 100644 --- a/backend/fluksio/flow/nodes/inject.py +++ b/backend/fluksio/flow/nodes/inject.py @@ -1,6 +1,6 @@ """Inject: the node that starts something, on a timer or on request. -Node-RED's *inject* is the most placed trigger in a real installation — mostly +Node-RED's *inject* is the most placed trigger in a real instance — mostly as a button someone presses, sometimes on an interval, occasionally once when everything comes up. """ diff --git a/backend/fluksio/flow/nodes/logic.py b/backend/fluksio/flow/nodes/logic.py index 3857575..b69cce1 100644 --- a/backend/fluksio/flow/nodes/logic.py +++ b/backend/fluksio/flow/nodes/logic.py @@ -2,7 +2,7 @@ Anything here could be written as a `python` node — that is what the function node is for. These exist because the same handful of shapes account for most of -a real installation, and a rule you fill in is easier to read on a canvas, and +a real instance, and a rule you fill in is easier to read on a canvas, and to change, than five lines of code repeated eighty times. """ @@ -115,7 +115,7 @@ class ChangeNode(Node): """Reshape a value on its way past: scale, offset, map, or replace. Node-RED's *change*, which is the second most common node in a real - installation after the function. + instance after the function. """ class Params(BaseModel): diff --git a/backend/fluksio/flow/nodes/mqtt.py b/backend/fluksio/flow/nodes/mqtt.py index b6763ea..7a3303f 100644 --- a/backend/fluksio/flow/nodes/mqtt.py +++ b/backend/fluksio/flow/nodes/mqtt.py @@ -24,7 +24,7 @@ def _aiomqtt() -> Any: """The client library, which is a `fluksio[server]` extra. Imported per use rather than at module level, because the node type is - registered at boot and an installation that talks to no broker should not + registered at boot and an instance that talks to no broker should not have to carry the library to start. """ try: @@ -218,7 +218,7 @@ class MqttNode(Node): json_key: str | dict[str, str] = "" @classmethod - def instance_key(cls, params: dict[str, Any]) -> str | None: + def target_key(cls, params: dict[str, Any]) -> str | None: """The broker and topic, which is one physical thing. A publisher and a subscriber on the same topic get the same key on diff --git a/backend/fluksio/flow/panels.py b/backend/fluksio/flow/panels.py index fdf93c1..1ce25bf 100644 --- a/backend/fluksio/flow/panels.py +++ b/backend/fluksio/flow/panels.py @@ -45,7 +45,7 @@ class PanelDef(BaseModel): class PanelsConfig(BaseModel): - """Every panel this installation knows about.""" + """Every panel this instance knows about.""" panels: list[PanelDef] = Field(default_factory=list) diff --git a/backend/fluksio/flow/pipeline.py b/backend/fluksio/flow/pipeline.py index db779e1..4746b1a 100644 --- a/backend/fluksio/flow/pipeline.py +++ b/backend/fluksio/flow/pipeline.py @@ -210,7 +210,7 @@ def _derive( The two lookups come out of the same walk: who consumes a message, and which node an id names. Both were linear scans over every node in the - installation, on the per-message path. + instance, on the per-message path. """ # A message may have several producers; every one of them is upstream # of the nodes consuming it. diff --git a/backend/fluksio/flow/placement.py b/backend/fluksio/flow/placement.py index da1111a..3ff24e3 100644 --- a/backend/fluksio/flow/placement.py +++ b/backend/fluksio/flow/placement.py @@ -10,7 +10,7 @@ counted at all. One question, then, asked once here: of every machine attached — this one and each worker — which could grant what this node asked for, and which of those has it free right now. A worker reports its inventory when it attaches, so the -answer covers the whole installation rather than the host the engine happens to +answer covers the whole instance rather than the host the engine happens to be on. Two properties are worth stating because they are what make the waiting safe: @@ -178,7 +178,7 @@ class Placer: """Every machine's size, as something to measure a request against. A provisioner's job shapes count: a machine it can start on demand is - one this installation has, even when nothing is attached yet. + one this instance has, even when nothing is attached yet. """ shapes = [] for target in self.targets(): diff --git a/backend/fluksio/flow/provision.py b/backend/fluksio/flow/provision.py index 204d4eb..3b0a71d 100644 --- a/backend/fluksio/flow/provision.py +++ b/backend/fluksio/flow/provision.py @@ -348,7 +348,7 @@ class SlurmProvisioner: def load_provisioners(path: Path, events: EventBus | None = None) -> list[Any]: - """Read the configured clusters. An installation with none has no file.""" + """Read the configured clusters. An instance with none has no file.""" if not path.exists(): return [] try: diff --git a/backend/fluksio/flow/runs.py b/backend/fluksio/flow/runs.py index 7c95f8a..cfb4d6d 100644 --- a/backend/fluksio/flow/runs.py +++ b/backend/fluksio/flow/runs.py @@ -296,7 +296,7 @@ def _from_run( if artifacts is not None and artifacts.path(row.digest) is None: raise RunRejected( f"Parameter '{key}': run '{run_id}' made '{output}', but its " - "bytes are gone from this installation's store" + "bytes are gone from this instance's store" ) return { "digest": row.digest, @@ -327,7 +327,7 @@ def _from_digest( if artifacts is not None and artifacts.path(digest) is None: raise RunRejected( f"Parameter '{key}': '{digest}' is known but its bytes are gone " - "from this installation's store" + "from this instance's store" ) return { "digest": row.digest, diff --git a/backend/fluksio/flow/webpush.py b/backend/fluksio/flow/webpush.py index 5c9913c..790a7f5 100644 --- a/backend/fluksio/flow/webpush.py +++ b/backend/fluksio/flow/webpush.py @@ -1,11 +1,11 @@ -"""Web Push: an alert reaching a phone that has this installation installed. +"""Web Push: an alert reaching a phone that has this instance installed. The browser hands us a subscription — an endpoint URL at its own push service, plus two keys — and from then on the engine can wake that device without it holding a connection open. Two pieces of crypto are involved and both are specified: the payload is encrypted to the subscription's keys (RFC 8291, aes128gcm) so the push service carries something it cannot read, and the -request is signed with this installation's own keypair (VAPID, RFC 8292) so the +request is signed with this instance's own keypair (VAPID, RFC 8292) so the service knows who is sending. Only ``http-ece`` is new here; the signing is `pyjwt` and the request is @@ -13,7 +13,7 @@ Only ``http-ece`` is new here; the signing is `pyjwt` and the request is this in one call, but brings `requests` *and* `aiohttp` with it — two more HTTP stacks on a machine that may well be a Raspberry Pi. -The keypair is this installation's identity to the push services and lives with +The keypair is this instance's identity to the push services and lives with the subscriptions in one file. Losing it means every browser has to subscribe again; it is regenerated on the spot if the file goes missing. """ @@ -62,7 +62,7 @@ class Subscription(BaseModel): class _Store(BaseModel): - #: This installation's VAPID private key: the raw P-256 scalar, base64url. + #: This instance's VAPID private key: the raw P-256 scalar, base64url. private_key: str = "" subscriptions: list[Subscription] = Field(default_factory=list) @@ -113,7 +113,7 @@ def _public_bytes(key: ec.EllipticCurvePrivateKey) -> bytes: def public_key() -> str: - """This installation's VAPID public key, generating the pair on first ask. + """This instance's VAPID public key, generating the pair on first ask. Base64url of the uncompressed point, which is the shape `pushManager.subscribe` wants for `applicationServerKey`. Blocking. @@ -126,7 +126,7 @@ def public_key() -> str: key.private_numbers().private_value.to_bytes(32, "big") ) _write(store) - logger.info("Generated this installation's web push keypair") + logger.info("Generated this instance's web push keypair") return _b64url(_public_bytes(_private_key(store))) @@ -156,7 +156,7 @@ def subscriptions() -> list[Subscription]: def _vapid_headers(key: ec.EllipticCurvePrivateKey, endpoint: str) -> dict[str, str]: - """Prove to the push service which installation is sending (RFC 8292).""" + """Prove to the push service which instance is sending (RFC 8292).""" origin = urlparse(endpoint) token = jwt.encode( { diff --git a/backend/fluksio/main.py b/backend/fluksio/main.py index 781aafc..4d408a6 100644 --- a/backend/fluksio/main.py +++ b/backend/fluksio/main.py @@ -307,7 +307,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: ) ) started.append(lambda: run_in_threadpool(close_shared_client)) - # Optional, and off unless someone enrolled this installation: the + # Optional, and off unless someone enrolled this instance: the # connector dials the portal, nothing dials in. cloud_task: asyncio.Task[None] | None = None app.state.cloud_connector = None @@ -346,7 +346,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: await aclose() -# The schema enumerates every endpoint this installation serves, including the +# The schema enumerates every endpoint this instance serves, including the # paths trigger nodes mount at runtime. That is exactly what a developer wants # and exactly what an internet-facing deployment should not hand out, so it # follows the environment — the same rule the portal's backend uses. The diff --git a/backend/fluksio/mcp/__init__.py b/backend/fluksio/mcp/__init__.py index 4cdc0bc..e3e59bf 100644 --- a/backend/fluksio/mcp/__init__.py +++ b/backend/fluksio/mcp/__init__.py @@ -2,6 +2,6 @@ ``server`` holds the tools, ``http`` wires them to the streamable-HTTP transport and the OAuth resource-server checks. Both are imported lazily, only -when ``MCP_ENABLED`` is set, so an installation that does not want an agent +when ``MCP_ENABLED`` is set, so an instance that does not want an agent endpoint does not carry one. """ diff --git a/backend/fluksio/models.py b/backend/fluksio/models.py index bf89a6e..726d721 100644 --- a/backend/fluksio/models.py +++ b/backend/fluksio/models.py @@ -53,9 +53,9 @@ class User(UserBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) hashed_password: str #: The portal account this local user stands for, if any. Set for the - #: superuser who enrolled this installation and for every remote user one + #: superuser who enrolled this instance and for every remote user one #: of them admitted; None for a purely local account, which is what an - #: installation nobody enrolled has only. + #: instance nobody enrolled has only. portal_sub: str | None = Field(default=None, max_length=64, unique=True, index=True) created_at: datetime | None = Field( default_factory=get_datetime_utc, @@ -491,7 +491,7 @@ class FlavorBase(SQLModel): """A named amount of machine, so a node can ask for one by name. The point is not to save typing. Cores and megabytes are a property of the - machines an installation actually has, and they change when those machines + machines an instance actually has, and they change when those machines do — so a node saying "gpu-small" keeps meaning something after the cluster is replaced, where a node saying 8 and 16384 quietly stops. """ diff --git a/backend/fluksio/sdk/cli.py b/backend/fluksio/sdk/cli.py index 4191a8f..2ece23a 100644 --- a/backend/fluksio/sdk/cli.py +++ b/backend/fluksio/sdk/cli.py @@ -682,7 +682,7 @@ def _flow_state(flow: dict[str, Any]) -> str: def _portal_phrase(portal: dict[str, Any]) -> tuple[str, str]: - """Where this installation stands with its portal, and how to colour it. + """Where this instance stands with its portal, and how to colour it. Three states worth telling apart: never paired, paired and linked, and paired but not reaching it — the last being the one somebody needs to know @@ -734,7 +734,7 @@ def _status_screen(client: Client) -> Any: summary = client.summary() flows = client.flows() - # Both, because an installation is usually one or the other: a live flow + # Both, because an instance is usually one or the other: a live flow # fails as an engine event, while a batch run fails on its own row. failures = client.events(kind="failure", limit=5) runs = client.runs(limit=5) diff --git a/backend/fluksio/sdk/client.py b/backend/fluksio/sdk/client.py index 4ddd9ec..30b5ddf 100644 --- a/backend/fluksio/sdk/client.py +++ b/backend/fluksio/sdk/client.py @@ -57,7 +57,7 @@ RETRY_STATUS = frozenset({502, 503, 504}) WAIT_TOLERANCE = 5 -#: What a project-local installation is called, beside `.venv` and `.git`. +#: What a project-local instance is called, beside `.venv` and `.git`. DATA_DIR_NAME = ".fluksio" #: The shared one, for a machine that wants a single engine rather than one @@ -66,10 +66,10 @@ GLOBAL_DATA_DIR = Path("~/.fluksio") def find_data_dir(start: Path | None = None) -> Path | None: - """The nearest project-local installation, walking up from ``start``. + """The nearest project-local instance, walking up from ``start``. The same search `.git` and `.venv` get, and for the same reason: which - installation you mean is a fact about where you are standing, not about + instance you mean is a fact about where you are standing, not about which machine you are on. Several repositories on one device each keep their own flows, runs and token this way rather than sharing one. """ @@ -82,16 +82,16 @@ def find_data_dir(start: Path | None = None) -> Path | None: def data_dir(start: Path | None = None) -> Path: - """The installation this working directory belongs to.""" + """The instance this working directory belongs to.""" found = find_data_dir(start) return found if found is not None else GLOBAL_DATA_DIR.expanduser() def config_path(directory: Path | None = None) -> Path: - """Where the token for an installation lives — beside the data it opens. + """Where the token for an instance lives — beside the data it opens. Not a single file per machine: a token is for one engine, and with an - installation per repository there is more than one. Keeping it inside the + instance per repository there is more than one. Keeping it inside the data directory means the client finds the credential for the engine whose directory it is standing in, without either having to be told. """ @@ -99,7 +99,7 @@ def config_path(directory: Path | None = None) -> Path: def _legacy_config_path() -> Path: - """Where `fluksio login` used to write, before installations were local.""" + """Where `fluksio login` used to write, before instances were local.""" base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") return Path(base) / "fluksio" / "client.json" @@ -290,7 +290,7 @@ class Client: return result def cloud_status(self) -> dict[str, Any]: - """Whether this installation is enrolled with a portal, and linked.""" + """Whether this instance is enrolled with a portal, and linked.""" result: dict[str, Any] = self._call("GET", "/cloud/status") return result @@ -585,7 +585,7 @@ class RunHandle: def ignore_self(directory: Path) -> None: - """Keep an installation out of the repository it sits in. + """Keep an instance out of the repository it sits in. It holds a token and a database, neither of which belongs in anybody's history. A `.gitignore` of `*` inside the directory ignores it from within, @@ -595,7 +595,7 @@ def ignore_self(directory: Path) -> None: marker = directory / ".gitignore" if not marker.exists(): directory.mkdir(parents=True, exist_ok=True) - marker.write_text("# A Fluksio installation: a database, and a token.\n*\n") + marker.write_text("# A Fluksio instance: a database, and a token.\n*\n") def write_config(url: str, token: str, directory: Path | None = None) -> Path: diff --git a/backend/fluksio/tui/app.py b/backend/fluksio/tui/app.py index 068bf86..b5bd13d 100644 --- a/backend/fluksio/tui/app.py +++ b/backend/fluksio/tui/app.py @@ -71,7 +71,7 @@ class Enroll(ModalScreen[tuple[str, str] | None]): def compose(self) -> ComposeResult: with Vertical(id="enroll"): - yield Label("Pair this installation with a portal") + yield Label("Pair this instance with a portal") yield Input(placeholder="claim code", id="code") yield Input(value=DEFAULT_PORTAL, id="portal") with Horizontal(): @@ -253,7 +253,7 @@ class ServeApp(App[int]): self.connect() return if who == "foreign": - self.note(f"Port {wanted} holds another installation's Fluksio.") + self.note(f"Port {wanted} holds another instance's Fluksio.") self.child = subprocess.Popen( # noqa: S603 child_argv(sys.argv[1:]), diff --git a/backend/tests/api/routes/test_flavors.py b/backend/tests/api/routes/test_flavors.py index 01bbfe1..8c2fce4 100644 --- a/backend/tests/api/routes/test_flavors.py +++ b/backend/tests/api/routes/test_flavors.py @@ -9,7 +9,7 @@ from fluksio.core.db import seed_flavors from fluksio.models import Flavor -def test_an_installation_starts_with_sizes_to_pick_from( +def test_an_instance_starts_with_sizes_to_pick_from( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: response = client.get( @@ -20,7 +20,7 @@ def test_an_installation_starts_with_sizes_to_pick_from( assert {"small", "medium", "large", "gpu-small"} <= names -def test_seeding_leaves_an_installation_that_has_its_own_alone(db: Session) -> None: +def test_seeding_leaves_an_instance_that_has_its_own_alone(db: Session) -> None: """Re-adding a size somebody deliberately removed is an argument to avoid.""" before = {row.name for row in db.exec(select(Flavor)).all()} seed_flavors(db) diff --git a/backend/tests/api/routes/test_panels.py b/backend/tests/api/routes/test_panels.py index b6f3bee..8f69ffc 100644 --- a/backend/tests/api/routes/test_panels.py +++ b/backend/tests/api/routes/test_panels.py @@ -544,7 +544,7 @@ def test_removing_the_panel_revokes_its_credential( # -------------------------------------------------------------------------- -# A screen that reached the portal but not this installation +# A screen that reached the portal but not this instance # -------------------------------------------------------------------------- @@ -592,7 +592,7 @@ def test_a_remote_device_is_paired_at_the_portal( ) -> None: """A device that arrived through the tunnel gets the portal's credential. - It could never present one this installation signed: the portal verifies + It could never present one this instance signed: the portal verifies what crosses it, and it verifies against its own key. """ import httpx @@ -627,7 +627,7 @@ def test_a_remote_device_is_paired_at_the_portal( assert approved.status_code == 200, approved.text assert calls[0]["url"].endswith("/api/v1/panel-tokens/") # type: ignore[union-attr] - assert calls[0]["headers"]["Authorization"] == "Bearer installation-token" # type: ignore[index] + assert calls[0]["headers"]["Authorization"] == "Bearer instance-token" # type: ignore[index] assert calls[0]["json"] == {"panel": "hallway"} # type: ignore[index] collected = client.get( diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index f606e90..fffdbaf 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -25,7 +25,7 @@ from fluksio.models import ( RunNode, User, ) -from tests.utils.portal import INSTALLATION_ID, ISSUER, jwks +from tests.utils.portal import INSTANCE_ID, ISSUER, jwks from tests.utils.user import authentication_token_from_email from tests.utils.utils import get_superuser_token_headers @@ -129,7 +129,7 @@ def enrolled( portal_key: rsa.RSAPrivateKey, db: Session, ) -> Any: - """Enrol this installation with a fake portal, then undo it.""" + """Enrol this instance with a fake portal, then undo it.""" local_user = db.exec( select(User).where(User.email == settings.FIRST_SUPERUSER) ).one() @@ -141,8 +141,8 @@ def enrolled( cloud_config.CloudConfig( portal_url=ISSUER, ws_url=f"{ISSUER}/api/v1/tunnel/attach", - installation_id=INSTALLATION_ID, - token="installation-token", + instance_id=INSTANCE_ID, + token="instance-token", issuer=ISSUER, jwks=jwks(portal_key), local_user_id=str(local_user.id), @@ -151,7 +151,7 @@ def enrolled( ) ) # Enrolment also maps the enrolling account to the portal identity that - # owns the installation; without it a portal session resolves to nobody. + # owns the instance; without it a portal session resolves to nobody. local_user.portal_sub = "portal-user-1" db.add(local_user) db.commit() diff --git a/backend/tests/flow/test_brain_graph.py b/backend/tests/flow/test_brain_graph.py index 8cf982c..72876d7 100644 --- a/backend/tests/flow/test_brain_graph.py +++ b/backend/tests/flow/test_brain_graph.py @@ -78,7 +78,7 @@ def test_an_edge_carries_the_qualified_message(controller: FlowController): def test_a_credential_never_reaches_the_key(): # Stored params, so a secret is still a reference. Neither its name nor its # value belongs in something the browser gets to see. - key = MqttNode.instance_key({**BROKER, "password": {"$secret": "broker_pw"}}) + key = MqttNode.target_key({**BROKER, "password": {"$secret": "broker_pw"}}) assert key == "mosquitto:1883/sensors/temp" diff --git a/backend/tests/flow/test_dashboards.py b/backend/tests/flow/test_dashboards.py index de75df9..466e4e2 100644 --- a/backend/tests/flow/test_dashboards.py +++ b/backend/tests/flow/test_dashboards.py @@ -423,7 +423,7 @@ def test_an_unbound_setting_is_just_its_value(): No message means nothing to type-check and nothing for a panel to be entitled to — and a name this build does not know is left alone rather - than refused, so an older installation reads a newer document. + than refused, so an older instance reads a newer document. """ defn = DashboardDef( name="house", @@ -523,7 +523,7 @@ def test_a_chart_of_runs_must_say_which_runs_and_which_metric(): def test_a_document_written_as_pages_is_read_as_one_grid(): - """Stored dashboards live in each installation's repository. + """Stored dashboards live in each instance's repository. So the old shape is normalised on the way in rather than migrated, and a placed second section keeps its arrangement instead of piling onto the diff --git a/backend/tests/flow/test_provision.py b/backend/tests/flow/test_provision.py index 816eb39..c86f736 100644 --- a/backend/tests/flow/test_provision.py +++ b/backend/tests/flow/test_provision.py @@ -192,7 +192,7 @@ def test_a_node_waiting_for_a_machine_asks_for_one_once(monkeypatch): assert ran.is_set() -def test_an_installation_with_nowhere_to_start_one_has_no_file(tmp_path): +def test_an_instance_with_nowhere_to_start_one_has_no_file(tmp_path): assert load_provisioners(tmp_path / "provisioners.json") == [] diff --git a/backend/tests/flow/test_runtime_control.py b/backend/tests/flow/test_runtime_control.py index a6fa4aa..9634a9f 100644 --- a/backend/tests/flow/test_runtime_control.py +++ b/backend/tests/flow/test_runtime_control.py @@ -237,7 +237,7 @@ def lifecycle(monkeypatch: pytest.MonkeyPatch) -> list[str]: def test_rebuilding_one_flow_leaves_another_flows_node_running( tmp_path: Path, lifecycle: list[str] ): - """The whole point: reconnecting one flow's nodes, not the installation's.""" + """The whole point: reconnecting one flow's nodes, not the instance's.""" store = FlowStore(tmp_path / "flows") for name in ("a", "b"): store.write_flow(FlowDef(name=name, nodes=[NodeDef(id="io", type="lifecycle")])) diff --git a/backend/tests/flow/test_vocabulary.py b/backend/tests/flow/test_vocabulary.py index e36f352..c78a4f0 100644 --- a/backend/tests/flow/test_vocabulary.py +++ b/backend/tests/flow/test_vocabulary.py @@ -1,6 +1,6 @@ """What the flow-logic nodes actually do. -These are the shapes a Node-RED installation is mostly made of, so their +These are the shapes a Node-RED instance is mostly made of, so their behaviour is worth pinning rather than just their construction. """ diff --git a/backend/tests/flow/test_webpush.py b/backend/tests/flow/test_webpush.py index a83d46c..8ea4595 100644 --- a/backend/tests/flow/test_webpush.py +++ b/backend/tests/flow/test_webpush.py @@ -16,7 +16,7 @@ from fluksio.flow import webpush @pytest.fixture(autouse=True) def store(tmp_path, monkeypatch): - """A store per test, so nothing writes into the real installation.""" + """A store per test, so nothing writes into the real instance.""" monkeypatch.setattr(settings, "WEBPUSH_FILE", tmp_path / "webpush.json") return tmp_path / "webpush.json" diff --git a/backend/tests/sdk/test_paths.py b/backend/tests/sdk/test_paths.py index 852ecdc..1f15112 100644 --- a/backend/tests/sdk/test_paths.py +++ b/backend/tests/sdk/test_paths.py @@ -1,4 +1,4 @@ -"""Which installation a command is for, and where its token lives. +"""Which instance a command is for, and where its token lives. A repository with its own venv gets its own engine, so "which one" is a fact about the working directory rather than about the machine. @@ -14,14 +14,14 @@ from fluksio.sdk import client @pytest.fixture def elsewhere(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """A working directory with no installation above it.""" + """A working directory with no instance above it.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg")) monkeypatch.setattr(client, "GLOBAL_DATA_DIR", tmp_path / "home" / ".fluksio") return tmp_path -def test_a_project_local_installation_is_found_from_below(elsewhere: Path): +def test_a_project_local_instance_is_found_from_below(elsewhere: Path): (elsewhere / ".fluksio").mkdir() deep = elsewhere / "src" / "pkg" / "sub" deep.mkdir(parents=True) @@ -31,7 +31,7 @@ def test_a_project_local_installation_is_found_from_below(elsewhere: Path): def test_the_nearest_one_wins(elsewhere: Path): - """An installation inside another belongs to the directory it is in.""" + """An instance inside another belongs to the directory it is in.""" (elsewhere / ".fluksio").mkdir() inner = elsewhere / "inner" (inner / ".fluksio").mkdir(parents=True) @@ -63,7 +63,7 @@ def test_the_credential_beside_the_data_is_the_one_used(elsewhere: Path): assert client._stored()["token"] == "local" -def test_a_login_from_before_installations_were_local_still_works(elsewhere: Path): +def test_a_login_from_before_instances_were_local_still_works(elsewhere: Path): """`fluksio login` wrote to XDG once; that must not stop answering.""" legacy = client._legacy_config_path() legacy.parent.mkdir(parents=True) diff --git a/backend/tests/test_cli.py b/backend/tests/test_cli.py index 9940acd..7403567 100644 --- a/backend/tests/test_cli.py +++ b/backend/tests/test_cli.py @@ -106,7 +106,7 @@ def test_a_sweeps_param_spelling_is_refused_by_name() -> None: _params(definition, ["--lr", "fast"]) -def test_serve_uses_the_installation_the_directory_belongs_to( +def test_serve_uses_the_instance_the_directory_belongs_to( tmp_path: Path, monkeypatch ) -> None: """A repository with its own venv gets its own engine, not the machine's.""" @@ -713,7 +713,7 @@ def test_who_holds_the_port_is_told_apart_by_the_token() -> None: The token is signed with this directory's secret key, so an engine that accepts it is one reading this directory's database. Another - installation's Fluksio answers the health check and refuses it. + instance's Fluksio answers the health check and refuses it. """ import httpx diff --git a/backend/tests/test_cloud.py b/backend/tests/test_cloud.py index 5e35fc8..fcdbeb2 100644 --- a/backend/tests/test_cloud.py +++ b/backend/tests/test_cloud.py @@ -1,7 +1,7 @@ """Remote access: off by default, and only ever as much as somebody granted. The property worth pinning down is the one everything else rests on — a -portal's token is worth nothing here until somebody at this installation +portal's token is worth nothing here until somebody at this instance enrolled it, and even then it grants exactly the rights of the local account the portal identity holding it was mapped to. An identity nobody mapped gets nothing, which is what makes deleting that local account a revocation. @@ -31,7 +31,7 @@ from tests.utils.portal import ISSUER, portal_token def test_portal_token_is_refused_when_not_enrolled( portal_key: rsa.RSAPrivateKey, tmp_path_factory: pytest.TempPathFactory ) -> None: - """An installation nobody connected trusts no portal at all.""" + """An instance nobody connected trusts no portal at all.""" original = settings.CLOUD_CONFIG_FILE settings.CLOUD_CONFIG_FILE = tmp_path_factory.mktemp("empty") / "cloud.json" try: @@ -55,11 +55,11 @@ def test_portal_token_resolves_by_portal_identity( assert user_from_token(db, portal_token(portal_key, subject="nobody")) is None -def test_token_for_another_installation_is_refused( +def test_token_for_another_instance_is_refused( enrolled: User, # noqa: ARG001 (fixture installs the enrolment) portal_key: rsa.RSAPrivateKey, ) -> None: - """The audience is this installation's id, so someone else's is worthless.""" + """The audience is this instance's id, so someone else's is worthless.""" with pytest.raises(jwt.exceptions.InvalidTokenError): decode_token(portal_token(portal_key, audience=str(uuid.uuid4()))) @@ -67,7 +67,7 @@ def test_token_for_another_installation_is_refused( def test_token_from_an_unpinned_key_is_refused( enrolled: User, # noqa: ARG001 (fixture installs the enrolment) ) -> None: - """A different portal, or a hijacked one, cannot sign for this installation.""" + """A different portal, or a hijacked one, cannot sign for this instance.""" impostor = rsa.generate_private_key(public_exponent=65537, key_size=2048) with pytest.raises(jwt.exceptions.InvalidTokenError): decode_token(portal_token(impostor)) @@ -104,7 +104,7 @@ def test_status_reports_not_enrolled( def test_enrolling_needs_a_superuser( client: TestClient, normal_user_token_headers: dict[str, str] ) -> None: - """Remote access is an installation-wide grant, not a personal setting.""" + """Remote access is an instance-wide grant, not a personal setting.""" response = client.post( f"{settings.API_V1_STR}/cloud/enroll", headers=normal_user_token_headers, @@ -122,7 +122,7 @@ def test_a_panel_scoped_portal_token_reaches_only_its_panel( """A screen paired through the portal is bounded here, not there. The portal names the panel; everything about what that means is this - installation's, which is the whole reason it may mint one at all. + instance's, which is the whole reason it may mint one at all. """ write_config( PanelsConfig( @@ -241,7 +241,7 @@ def test_adding_a_remote_user_maps_and_revokes( f"{settings.API_V1_STR}/users/{body['id']}", headers=superuser_token_headers ) assert removed.status_code == 200, removed.text - assert delete.call_args.args[0].endswith("/installation-members/portal-user-9") + assert delete.call_args.args[0].endswith("/instance-members/portal-user-9") db.expire_all() assert user_from_token(db, theirs) is None @@ -251,7 +251,7 @@ def test_adding_a_remote_user_needs_a_superuser( enrolled: User, # noqa: ARG001 (fixture installs the enrolment) normal_user_token_headers: dict[str, str], ) -> None: - """Widening who can reach this installation stays a superuser's decision.""" + """Widening who can reach this instance stays a superuser's decision.""" response = client.post( f"{settings.API_V1_STR}/cloud/users", headers=normal_user_token_headers, @@ -272,8 +272,8 @@ def test_enrolling_against_a_portal_without_an_owner_is_refused( status_code=200, json=Mock( return_value={ - "installation_id": str(uuid.uuid4()), - "installation_token": "t", + "instance_id": str(uuid.uuid4()), + "instance_token": "t", "ws_url": f"{ISSUER}/api/v1/tunnel/attach", "issuer": ISSUER, "jwks": {"keys": []}, diff --git a/backend/tests/utils/portal.py b/backend/tests/utils/portal.py index 0e1d5b5..7f424f8 100644 --- a/backend/tests/utils/portal.py +++ b/backend/tests/utils/portal.py @@ -14,7 +14,7 @@ import jwt from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa -INSTALLATION_ID = "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f" +INSTANCE_ID = "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f" ISSUER = "https://hub.example.test" @@ -28,7 +28,7 @@ def portal_token( key: rsa.RSAPrivateKey, *, subject: str = "portal-user-1", - audience: str = INSTALLATION_ID, + audience: str = INSTANCE_ID, issuer: str = ISSUER, scope: str = "proxy", ) -> str: diff --git a/development.md b/development.md index eace29d..518ad22 100644 --- a/development.md +++ b/development.md @@ -108,7 +108,7 @@ The backend settings load `../.env` relative to `backend/` through pydantic-sett exported environment variable always outranks the file, which is how the suite pins its own configuration. -`DATA_DIR` is where this installation keeps everything: the SQLite database, the flow git +`DATA_DIR` is where this instance keeps everything: the SQLite database, the flow git repository, `secrets.enc`, `oauth-key.pem`, `cloud.json`, artifacts and the user venv. Each path derives from it and can still be set on its own — the compose stack spells all of them out against `/data`. diff --git a/docker/compose.yml b/docker/compose.yml index 328d073..d8d359a 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -108,7 +108,7 @@ services: - SENTRY_DSN=${SENTRY_DSN} # Flow state survives a restart in Redis; without a host it stays in memory. - REDIS_HOST=redis - # Everything this installation keeps — the database, the flows, secrets, + # Everything this instance keeps — the database, the flows, secrets, # artifacts and the user venv — on one volume. The paths below derive # from it; they are spelled out because each is load bearing. - DATA_DIR=/data @@ -120,7 +120,7 @@ services: # and every issued token with it. - MCP_ENABLED=${MCP_ENABLED-false} - OAUTH_PRIVATE_KEY_FILE=/data/oauth-key.pem - # The portal enrolment, for the same reason: it holds this installation's + # The portal enrolment, for the same reason: it holds this instance's # credential and the key it pinned, so losing it on a rebuild would mean # enrolling again by hand. Absent until someone connects a portal. - CLOUD_CONFIG_FILE=/data/cloud.json diff --git a/docs/code/agents.md b/docs/code/agents.md index 29527b3..cb4ab0f 100644 --- a/docs/code/agents.md +++ b/docs/code/agents.md @@ -106,6 +106,6 @@ you what the canvas would have told you. - MCP is not currently reachable through a [portal](../interface/portal.md) tunnel — the proxy forwards `/api/v1/` only. Connect an agent on the same - network as the installation. + network as the instance. - Secrets are never readable, by an agent or by anyone else. `list_secrets` returns names. diff --git a/docs/code/api.md b/docs/code/api.md index 7ada624..0a1b6d6 100644 --- a/docs/code/api.md +++ b/docs/code/api.md @@ -5,7 +5,7 @@ client of this schema, not a privileged path into the engine — so anything you can click, you can script. Base URL: `https://api.${DOMAIN}/api/v1`, or `http://127.0.0.1:8000/api/v1` for -a `fluksio serve` installation. +a `fluksio serve` instance. ```sh export FLUKSIO=http://127.0.0.1:8000/api/v1 @@ -34,7 +34,7 @@ Agents authenticate differently — see [Agents over MCP](agents.md). the tour. It is closed in production on purpose: the schema enumerates every endpoint - the installation serves, including the paths webhook nodes mounted at + the instance serves, including the paths webhook nodes mounted at runtime. ## Flows @@ -230,7 +230,7 @@ draw from. | `404` | no such flow, dashboard, run or message | | `409` | someone else saved first — the body carries `current_version` | | `422` | a parameter, port or binding did not typecheck | -| `503` | that subsystem is not available on this installation | +| `503` | that subsystem is not available on this instance | A 409 on a save or a publish is not an error to retry blindly: it means the stored version moved past the one you were editing. Re-read, merge, save again. @@ -239,4 +239,4 @@ stored version moved past the one you were editing. Re-read, merge, save again. The frontend's TypeScript client is generated from the OpenAPI schema (`make generate-client`). Any OpenAPI generator will do the same for your -language — point it at `/api/v1/openapi.json` on a non-production installation. +language — point it at `/api/v1/openapi.json` on a non-production instance. diff --git a/docs/code/cli.md b/docs/code/cli.md index f514019..7d99e55 100644 --- a/docs/code/cli.md +++ b/docs/code/cli.md @@ -18,10 +18,10 @@ should only *run nodes* for an engine elsewhere. It has none of the engine in it. See [Remote workers](workers.md). The command is two things at once: `serve`, `enroll` and `worker` *are* an -installation, while `login`, `sync`, `run`, `runs`, `artifacts`, `sweep` and +instance, while `login`, `sync`, `run`, `runs`, `artifacts`, `sweep` and `status` talk to one that may be anywhere. -## Where an installation lives +## Where an instance lives `.fluksio` beside your code, found the way `.git` is: from the working directory, or any directory above it. Two repositories on one machine are @@ -55,14 +55,14 @@ The default port moves out of the way when something already has it — 8001, moved off: `--port 9000` on a taken 9000 fails, because something else is there and you named it. -What it will *not* do is start a second engine for the same installation. If +What it will *not* do is start a second engine for the same instance. If the port is held by an engine already serving this directory, it says so and -stops — one SQLite database wants one engine. Another installation's Fluksio +stops — one SQLite database wants one engine. Another instance's Fluksio on that port is named, and the move happens as usual. | Option | Default | What it does | |---|---|---| -| `--data-dir PATH` | `./.fluksio` (or `$FLUKSIO_HOME`) | where this installation keeps everything | +| `--data-dir PATH` | `./.fluksio` (or `$FLUKSIO_HOME`) | where this instance keeps everything | | `--host HOST` | `127.0.0.1` | what to bind | | `--port PORT` | `8000`, or the next free one | what to listen on | | `--plain` | off at a terminal | the log stream rather than the dashboard | @@ -83,7 +83,7 @@ every other. `--gpus 1` is what serialises them. `--enroll` with `--portal` is the one-command setup: it pairs before the engine starts, so the connection is dialled as part of coming up rather than needing a -restart. It is skipped if the installation is already enrolled. +restart. It is skipped if the instance is already enrolled. !!! warning "One process" @@ -106,11 +106,11 @@ Created the admin account admin@example.com Shown once. Change it from the dashboard. Fluksio 0.1.0 — data in /home/you/.fluksio API http://127.0.0.1:8000/api/v1 - No portal. Pair this installation with: + No portal. Pair this instance with: fluksio enroll ``` -An enrolled installation says which portal it is on instead, and notes that the +An enrolled instance says which portal it is on instead, and notes that the dashboard is served from there rather than here. ### The dashboard @@ -135,9 +135,9 @@ possible — and what makes `q` a way out of the screen rather than a way to stop the engine. Running `fluksio serve` again reattaches to it. An engine started elsewhere is adopted rather than duplicated, and can be -stopped from here only when it is this installation's own: both the pidfile +stopped from here only when it is this instance's own: both the pidfile beside the data and a token this directory's key signed have to agree. Another -installation's engine is named and left alone. +instance's engine is named and left alone. The screen subscribes to the engine's event bus over the same websocket a browser uses, so a run appears the moment it starts rather than at the next @@ -172,7 +172,7 @@ comparison refreshes when the engine says a run finished. ## `fluksio enroll` -Pairs an existing installation with a portal. +Pairs an existing instance with a portal. ```sh fluksio enroll ABCD-1234 @@ -182,10 +182,10 @@ fluksio enroll ABCD-1234 |---|---| | `--portal URL` | a portal of your own, instead of `https://hub.fluksio.com` | | `--as EMAIL` | the local account a portal session arrives as | -| `--data-dir PATH` | which installation, if not the one this directory is in | +| `--data-dir PATH` | which instance, if not the one this directory is in | -Get the code from the portal under **Installations → Add installation**. It is -single-use and expires in fifteen minutes. `--as` matters when the installation +Get the code from the portal under **Instances → Add instance**. It is +single-use and expires in fifteen minutes. `--as` matters when the instance has several superusers — without it, enrolment refuses rather than guessing. Afterwards, `fluksio serve` dials the portal as it comes up, and keeps dialling: @@ -353,7 +353,7 @@ answered. See [Stage caching](../concepts/runs.md#stage-caching). `--local` boots the engine inside this process instead of talking to a served one, so there is no `fluksio serve` terminal to keep open. It is the same -installation either way — the same `.fluksio`, the same database, artifacts +instance either way — the same `.fluksio`, the same database, artifacts and run history — so a run made this way and a run made through a served engine cache against each other. It always waits, because the engine it starts lives exactly as long as the command. Starting one costs a few seconds of @@ -376,7 +376,7 @@ A `resources` line names each machine the engine can run a node on and how much of it is in use, plus how many nodes are queued for one. It is absent on an engine that accounts for nothing. -The portal reads one of three ways. `no portal` means this installation was +The portal reads one of three ways. `no portal` means this instance was never enrolled. `portal hub.fluksio.com` means the link is up. `portal unreachable` names the error, and is the one worth acting on — the dashboard is served from the other end, so nobody can reach it while that is showing. @@ -524,7 +524,7 @@ Two things follow from this layout and are worth internalising: anyone made to any flow. A run records the commit it ran at, so `git show` on that hash is literally the code that produced the number. -**Backing up the data directory backs up the installation.** Everything else is +**Backing up the data directory backs up the instance.** Everything else is rebuildable. Copy it while the engine is stopped, or use SQLite's online backup for the database if it is not. diff --git a/docs/code/nodes.md b/docs/code/nodes.md index ab5bf12..e4b654f 100644 --- a/docs/code/nodes.md +++ b/docs/code/nodes.md @@ -213,7 +213,7 @@ with everything else. ## Packages -Node code runs in a virtual environment of its own, on the installation's data +Node code runs in a virtual environment of its own, on the instance's data volume — deliberately separate from the one Fluksio itself runs on. Declare what you import in [Modules](../interface/operations.md), or over the diff --git a/docs/concepts/flows.md b/docs/concepts/flows.md index 9aae75f..cba81fe 100644 --- a/docs/concepts/flows.md +++ b/docs/concepts/flows.md @@ -144,7 +144,7 @@ Every flow has a published version and, while you are working, a draft. The store is a git repository — `flow.json` for the structure, `nodes/*.py` for the code — and each save is a commit. So a flow's history is readable with -ordinary git tooling, and copying a flow between installations is copying a +ordinary git tooling, and copying a flow between instances is copying a directory. Saving carries the version you last saw. If someone else saved in between, you diff --git a/docs/getting-started/data-science.md b/docs/getting-started/data-science.md index c688512..2d08973 100644 --- a/docs/getting-started/data-science.md +++ b/docs/getting-started/data-science.md @@ -14,7 +14,7 @@ pip install fluksio fluksio serve ``` -That is the whole installation. No Docker, no database server, no ports to +That is the whole instance. No Docker, no database server, no ports to open, and no login. The first run prints something like: ```text @@ -25,7 +25,7 @@ Fluksio 0.1.0 — data in /home/you/my-research/.fluksio API http://127.0.0.1:8000/api/v1 Nodes /home/you/my-research/.venv/bin/python your environment, adopted. Add packages with pip. - No portal. Pair this installation with: + No portal. Pair this instance with: fluksio enroll Signed in as admin@example.com token in /home/you/my-research/.fluksio/client.json @@ -39,7 +39,7 @@ below just works. `fluksio login` is for an engine somewhere *else*. **Write that password down** anyway. It is shown once, and it is what the dashboard asks for. -### One installation per project +### One instance per project `.fluksio` sits beside your code, and is found the way `.git` is — from the directory you are standing in, or any directory above it. So two repositories @@ -135,7 +135,7 @@ environment it did not make. ### A venv of Fluksio's own -Sometimes you want the isolation instead: a shared installation, a container, +Sometimes you want the isolation instead: a shared instance, a container, or an environment too precious to let a node's dependency near. Set `NODE_VENV=managed` and Fluksio builds and owns one under the data directory: @@ -761,14 +761,14 @@ A submit is around 15 ms, so calling that in a loop is a reasonable thing to do. The pip install gives you the engine and the API, not a web interface — a machine with no inbound route cannot serve one usefully anyway. To see the -canvas, the run history and live loss curves, pair the installation with a +canvas, the run history and live loss curves, pair the instance with a portal, which serves the dashboard from its side: ```sh fluksio enroll --portal https://hub.fluksio.com ``` -Get the claim code from the portal under **Installations → Add installation**. +Get the claim code from the portal under **Instances → Add instance**. Nothing needs to be exposed: your machine dials out and holds the connection open. See [Accounts and the portal](../interface/portal.md). diff --git a/docs/getting-started/facility-automation.md b/docs/getting-started/facility-automation.md index b3a8faf..4a681c2 100644 --- a/docs/getting-started/facility-automation.md +++ b/docs/getting-started/facility-automation.md @@ -53,9 +53,9 @@ $EDITOR .env # FIRST_SUPERUSER, ENVIRONMENT=production | `FIRST_SUPERUSER_PASSWORD` | leave it as `changethis` and one is generated for you | | `ENVIRONMENT` | `production` closes the interactive API schema; `local` leaves it open | -Everything the installation owns — the database, your flows, secrets, +Everything the instance owns — the database, your flows, secrets, artifacts, the packages your node code imports — is on one Docker volume. -Backing that volume up is backing up the installation. +Backing that volume up is backing up the instance. ??? note "Even smaller: no Docker at all" @@ -313,5 +313,5 @@ bad it gets. See [Secrets, modules and alerts](../interface/operations.md). - [The flow editor](../interface/flow-editor.md) — the canvas, in detail - [Keeping state in a flow](../concepts/state.md) — running totals, debounces, and the one rule that makes them safe -- [Accounts and the portal](../interface/portal.md) — reach the installation +- [Accounts and the portal](../interface/portal.md) — reach the instance from outside the house without opening a port diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 118a1d3..cc4c957 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -53,7 +53,7 @@ Some rough tells: | **The thing you look at** | run history and loss curves | a dashboard, maybe on a wall | If both describe you — a lab with instruments to drive *and* models to -fit — start with the data-science path. It is the smaller installation, and it +fit — start with the data-science path. It is the smaller instance, and it grows into the other one without being reinstalled: the same engine, the same flows, just more of them running all the time. @@ -62,7 +62,7 @@ flows, just more of them running all the time. Whichever door you came in: - **Flows are files in a git repository.** Every save is a commit. You can read - the history with ordinary git, and you can copy a flow between installations + the history with ordinary git, and you can copy a flow between instances by copying a directory. - **Editing is separate from running.** You edit a draft; the engine keeps running what was published until you publish. diff --git a/docs/index.md b/docs/index.md index 063ba9d..8f4406e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,6 +52,6 @@ and a training pipeline on it. | Drive it from Python, a shell or CI | [Code and the CLI](code/cli.md) | | Look up a node type or a payload type | [Reference](reference/node-types.md) | -Fluksio is self-hosted by default. An installation runs offline, keeps its data +Fluksio is self-hosted by default. An instance runs offline, keeps its data on its own disk, and never contacts anything unless you [connect it to a portal](interface/portal.md) yourself. diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index ee74dca..d22855f 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -272,26 +272,26 @@ swap the device out without rebuilding what hangs there. !!! note "If the link is wrong" - The pairing link is built from the installation's `FRONTEND_HOST`. If that + The pairing link is built from the instance's `FRONTEND_HOST`. If that is not the address devices on your network actually reach, fix the setting rather than the link: it is the same one password-reset mails and the OAuth metadata are built from. ### A screen somewhere you cannot reach -Another building, someone else's network, no route in. An installation +Another building, someone else's network, no route in. An instance [enrolled with a portal](portal.md) shows a second link, -`https://hub.${DOMAIN}/i/{installation-id}/panel`, and the same three steps +`https://hub.${DOMAIN}/i/{instance-id}/panel`, and the same three steps work through it: the portal serves that one page without a session, forwards the pairing calls down the tunnel, and mints the credential when you approve the code. The pairing line then reads *via portal*. The portal names the panel and nothing else. What the panel may read is decided -on the installation, on every call, by the same check a locally paired screen -passes. Two differences: it acts as the account the installation was enrolled +on the instance, on every call, by the same check a locally paired screen +passes. Two differences: it acts as the account the instance was enrolled with rather than as whoever approved it, and deleting the panel stops it here immediately while the portal's copy of the token expires on its own — which is -also why unpairing, which works on a credential this installation signed, does +also why unpairing, which works on a credential this instance signed, does not reach a remote screen. Revoke that one at the hub. ## See also diff --git a/docs/interface/index.md b/docs/interface/index.md index d9b932d..33c0e5c 100644 --- a/docs/interface/index.md +++ b/docs/interface/index.md @@ -1,12 +1,12 @@ # The dashboard app The web interface is a single-page app served at `app.${DOMAIN}` — or, for an -installation reached through a portal, at `${DOMAIN}/i/{installation-id}`. +instance reached through a portal, at `${DOMAIN}/i/{instance-id}`. Either way it is the same application, and it is a client of the same REST API you can script against. -Sign in with the account the installation was created with. On a fresh -installation that account was printed once, on the first start. +Sign in with the account the instance was created with. On a fresh +instance that account was printed once, on the first start. ## The shell @@ -22,7 +22,7 @@ phone the sidebar collapses to a sheet. | **Modules** | the Python packages your node code may import | | **Alerts** | where failures get sent | | **Admin** | users (superusers only) | -| **Search** | anything in this installation, by name | +| **Search** | anything in this instance, by name | | **Settings** | your account, appearance, and remote access | ### Search @@ -33,8 +33,8 @@ dashboards and the widgets on them, panels, secrets, modules, workers and alert channels. Picking a node opens its flow with that node in focus; picking a widget opens its dashboard. -It searches this installation. Reached through a portal, other installations -are behind **All installations** at the top of the sidebar. +It searches this instance. Reached through a portal, other instances +are behind **All instances** at the top of the sidebar. ## Home @@ -44,7 +44,7 @@ The one screen you leave open. Four things share it. Every flow drawn as a neuron, wired to the flows it exchanges messages with. This is the brand mark made live, and it is also the fastest read on the -installation: a neuron pulses when its flow is running work, and its ring turns +instance: a neuron pulses when its flow is running work, and its ring turns terracotta when the flow cannot run as written. A neuron with a problem keeps its label showing so you can see which one it is without hovering. @@ -70,7 +70,7 @@ Always answers, degraded or not. The tiles cover: because their graph does not validate - **Nodes** — how many failed to load - **Runs running** — batch runs in flight right now, and how many are - waiting. Only on an installation that has run something + waiting. Only on an instance that has run something - **Queue** — depth, and how old the oldest pending item is - **Loop lag** — whether the engine's event loop is keeping up @@ -110,8 +110,8 @@ so does Escape. The Dashboards screen works the same way. - [Dashboards and panels](dashboards.md) — widgets, bindings, and hanging a screen on a wall - [Secrets, modules and alerts](operations.md) — the three screens that keep an - installation running -- [Accounts and the portal](portal.md) — reaching an installation from outside + instance running +- [Accounts and the portal](portal.md) — reaching an instance from outside its network ## Appearance diff --git a/docs/interface/operations.md b/docs/interface/operations.md index 3dc6a6f..d419cdf 100644 --- a/docs/interface/operations.md +++ b/docs/interface/operations.md @@ -1,7 +1,7 @@ # Secrets, modules and alerts Three screens that have nothing to do with each other except that an -installation you actually depend on needs all of them. +instance you actually depend on needs all of them. ## Secrets @@ -18,7 +18,7 @@ have stored. **Secrets** is where the values live. Add a name and a value; the value is never shown again, and the list only ever shows names. -They are encrypted at rest with a key derived from the installation's +They are encrypted at rest with a key derived from the instance's `SECRET_KEY`, and kept **outside** the flow repository. That matters because flows are a git repository you may well push somewhere: what gets committed and shared never contains a password. @@ -35,7 +35,7 @@ authentication failure. ## Modules -Node code runs in a virtual environment of its own, on the installation's data +Node code runs in a virtual environment of its own, on the instance's data volume — separate from the packages Fluksio itself runs on. A pin of yours can never shadow one of ours, and vice versa. @@ -69,7 +69,7 @@ where you say who hears about it. | Kind | Settings | |---|---| | **ntfy** | server, topic, and a token for a protected topic | -| **SMTP** | an address to send to (the installation's mail settings do the rest) | +| **SMTP** | an address to send to (the instance's mail settings do the rest) | | **Webhook** | a URL to POST to | | **Dashboard** | a message name a notification widget reads | @@ -126,6 +126,6 @@ are registered. Your own account: name, email, password, and appearance (light, dark, or follow the system). -**Remote access** is where an installation is paired with a portal, remote +**Remote access** is where an instance is paired with a portal, remote users are admitted, and the link is cut again. That has [its own page](portal.md). diff --git a/docs/interface/portal.md b/docs/interface/portal.md index 1703869..fd9345e 100644 --- a/docs/interface/portal.md +++ b/docs/interface/portal.md @@ -1,6 +1,6 @@ # Accounts and the portal -Fluksio is self-hosted by default. An installation runs offline, keeps its data +Fluksio is self-hosted by default. An instance runs offline, keeps its data on its own disk, and never contacts anything unless you tell it to. The **portal** is optional, and it exists to solve two specific problems: @@ -8,10 +8,10 @@ The **portal** is optional, and it exists to solve two specific problems: 1. **Your machine has no inbound route.** A homelab behind CGNAT, a cluster node with no open ports, a laptop. Opening one is work, and often not allowed. -2. **You want a browser on it anyway.** A `pip install fluksio` installation +2. **You want a browser on it anyway.** A `pip install fluksio` instance has no web server for the dashboard at all. -An enrolled installation dials *out* to the portal and holds one websocket +An enrolled instance dials *out* to the portal and holds one websocket open. The portal serves the dashboard from its own side, and only the API calls travel down the tunnel — so the interface loads at portal speed and your machine stays unreachable from the internet. @@ -19,18 +19,18 @@ machine stays unreachable from the internet. ## Enrolling Two halves, deliberately: whoever performs the second step decides what the -installation's owner gets. +instance's owner gets. **On the portal** (`hub.${DOMAIN}`, or [fluksio.com](https://fluksio.com) for -the hosted one): **Installations → Add installation**, give it a name, and copy +the hosted one): **Instances → Add instance**, give it a name, and copy the code. -**On the installation**, either from the dashboard: +**On the instance**, either from the dashboard: > **Settings → Remote access**, enter the portal URL and the code, press > **Connect**. -or from the command line, which is the path for an installation with no web +or from the command line, which is the path for an instance with no web interface of its own: ```sh @@ -42,40 +42,40 @@ The code is single-use and expires in fifteen minutes. A portal session then arrives as *that local account* — the settings screen states this plainly, because it is the whole security model in one sentence. Use `--as someone@example.com` to enrol as a specific local account when the -installation has several superusers. +instance has several superusers. -Once enrolled, the installation appears under **Installations** with its +Once enrolled, the instance appears under **Instances** with its status, when it was last seen and its version. **Open** takes you to its -dashboard at `${DOMAIN}/i/{installation-id}`. +dashboard at `${DOMAIN}/i/{instance-id}`. ## What the portal can and cannot do -The portal holds one credential for your installation and proxies requests down -the tunnel. What those requests may do is decided **on the installation**, by +The portal holds one credential for your instance and proxies requests down +the tunnel. What those requests may do is decided **on the instance**, by the same checks a local session passes. -The trust anchor is a signing keypair on the portal. Every installation pins +The trust anchor is a signing keypair on the portal. Every instance pins its public half at enrolment and rejects anything else — which is what stops a hijacked DNS entry or a mis-issued certificate from impersonating the portal. ## Letting someone else in -Anyone else on the portal reaches your installation only if a superuser there +Anyone else on the portal reaches your instance only if a superuser there admits them, and they arrive as a local user of their own rather than as you. -1. **They**: **Installations → Join an installation**, and copy the code. It is +1. **They**: **Instances → Join an instance**, and copy the code. It is bound to their portal account and expires in fifteen minutes. -2. **You**, on the installation: **Settings → Remote access → Add remote +2. **You**, on the instance: **Settings → Remote access → Add remote user**, and enter the code. -3. They now see the installation under **Installations**, marked *Shared by*, +3. They now see the instance under **Instances**, marked *Shared by*, with **Open** and nothing else. Renaming, re-keying and removing stay with you. -The installation redeems that code against the portal using its own credential. +The instance redeems that code against the portal using its own credential. A portal session cannot do this — which is what stops somebody you let in from letting others in. -On the installation they appear under **Admin → Users**, badged *Portal*, never +On the instance they appear under **Admin → Users**, badged *Portal*, never a superuser and with no password. ## Cutting it off @@ -84,29 +84,29 @@ a superuser and with no password. |---|---|---| | The portal | **New code** | rotates the credential and drops the current link | | The portal | **Remove** | deletes the registration and cuts the connection | -| The installation | **Disconnect** | unilateral and immediate — the portal's tokens stop verifying here whatever the portal still has on file | -| The installation | delete a user under **Admin → Users** | that one person, immediately, independent of the portal | +| The instance | **Disconnect** | unilateral and immediate — the portal's tokens stop verifying here whatever the portal still has on file | +| The instance | delete a user under **Admin → Users** | that one person, immediately, independent of the portal | -**New code** also cuts every credential the portal minted for this installation, +**New code** also cuts every credential the portal minted for this instance, including [wall panels paired through it](dashboards.md#a-screen-somewhere-you-cannot-reach). -The installation's own **Disconnect** is the one to reach for if you are ever +The instance's own **Disconnect** is the one to reach for if you are ever unsure: it does not need the portal's cooperation. ## Running your own portal The portal is the `index` stack's `hub` service — accounts, the registry of -connected installations, and the websocket each one dials in on. Two things +connected instances, and the websocket each one dials in on. Two things about it are load-bearing: -- **It runs a single process.** It keeps its attached installations in the +- **It runs a single process.** It keeps its attached instances in the memory of the process holding their sockets, so a second worker would answer for links it does not hold. Scaling out needs a routing layer first. - **Back up the signing keypair with the database.** Replacing it forces every - installation to be enrolled again. + instance to be enrolled again. Websocket upgrades must be enabled on the `hub` hostname in whatever proxy -fronts it. Without them every installation sits in a reconnect loop and the +fronts it. Without them every instance sits in a reconnect loop and the portal shows them all offline. ## See also diff --git a/docs/llms.txt b/docs/llms.txt index e36af6a..c347be1 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -5,7 +5,7 @@ > same engine serves two shapes of work: live flows that never end (buildings, > labs, homelabs) and batch runs that finish and leave a record (experiments, > ML pipelines, CI-style jobs). Self-hosted by default; an optional portal -> exists only to reach an installation that has no inbound route. +> exists only to reach an instance that has no inbound route. ## Getting started - [Pick your starting point](/getting-started/): the two setup paths and how to choose diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 323426e..8baf07f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1,7 +1,7 @@ # Configuration Every setting comes from the environment, or from an env file. Which file -depends on how the installation was started: +depends on how the instance was started: | Started with | Reads | |---|---| @@ -36,7 +36,7 @@ what the container images do to pin everything onto `/data`. | `managed` | a venv the engine builds under `DATA_DIR` and owns, which the Modules screen installs into with `uv pip sync`. The container images set this: the venv in them holds the app and nothing of anybody else's. | | a path | that interpreter, or that venv, whatever it is. | -An installation that already has a managed venv keeps it on upgrade under +An instance that already has a managed venv keeps it on upgrade under `auto`, because it may hold packages somebody installed on purpose. !!! warning "The four files that must be on persistent storage" @@ -112,10 +112,10 @@ remember it is fixed at build time: changing it means rebuilding that image. `delay` with a cron expression fires on local time. Left at `UTC`, "off at 02:00" means two in the morning UTC, which in most of the world is neither two o'clock nor the same hour in summer as in winter. Set it to where the -installation is. +instance is. `production` closes `/docs`, `/redoc` and the OpenAPI document, because the -schema enumerates every endpoint the installation serves — including the paths +schema enumerates every endpoint the instance serves — including the paths webhook nodes mounted at runtime. It also turns a `changethis` secret from a warning into a refusal to start. diff --git a/docs/reference/node-types.md b/docs/reference/node-types.md index bb691a6..9339910 100644 --- a/docs/reference/node-types.md +++ b/docs/reference/node-types.md @@ -5,7 +5,7 @@ editor generates from its parameter schema, so they all behave the same way. Anything here could be written as a **Function** node — that is what the function node is for. These exist because the same handful of shapes account -for most of a real installation, and a rule you fill in is easier to read on a +for most of a real instance, and a rule you fill in is easier to read on a canvas, and to change, than five lines of code repeated eighty times. `GET /flows/node-types` returns this list with each type's full parameter @@ -138,7 +138,7 @@ thing configured elsewhere. | `start_delay` | `1.0` | how long to wait before that first emission | The scheduler: a `cron` expression here is what makes a flow run by the clock. -It is also the most-placed node in a real installation — mostly as a button +It is also the most-placed node in a real instance — mostly as a button someone presses. ### Delay & schedule diff --git a/frontend/index.html b/frontend/index.html index ad124a6..f78a925 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,9 +5,9 @@ - + instance's own path so the install is scoped to one house. --> Fluksio diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 383f466..530a3f1 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -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() diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts index 19cbf96..af1fef2 100644 --- a/frontend/src/client/schemas.gen.ts +++ b/frontend/src/client/schemas.gen.ts @@ -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. diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index 8a9b160..49af8a4 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -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. diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 6e0035e..4a611d9 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -852,7 +852,7 @@ export type PanelDef = { }; /** - * Every panel this installation knows about. + * Every panel this instance knows about. */ export type PanelsConfig = { panels?: Array; @@ -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. diff --git a/frontend/src/components/Common/ConnectionNotice.tsx b/frontend/src/components/Common/ConnectionNotice.tsx index 5d29b41..f4675f7 100644 --- a/frontend/src/components/Common/ConnectionNotice.tsx +++ b/frontend/src/components/Common/ConnectionNotice.tsx @@ -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 }, ) diff --git a/frontend/src/components/Common/DashboardMosaic.tsx b/frontend/src/components/Common/DashboardMosaic.tsx index 0c0e67c..521eee3 100644 --- a/frontend/src/components/Common/DashboardMosaic.tsx +++ b/frontend/src/components/Common/DashboardMosaic.tsx @@ -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`. */ diff --git a/frontend/src/components/Common/GlobalSearch.tsx b/frontend/src/components/Common/GlobalSearch.tsx index efc524e..0014f4b 100644 --- a/frontend/src/components/Common/GlobalSearch.tsx +++ b/frontend/src/components/Common/GlobalSearch.tsx @@ -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({ ) : (

- Start typing to search this installation. + Start typing to search this instance.

)} diff --git a/frontend/src/components/Common/Notifications.tsx b/frontend/src/components/Common/Notifications.tsx index b8590ad..22adb19 100644 --- a/frontend/src/components/Common/Notifications.tsx +++ b/frontend/src/components/Common/Notifications.tsx @@ -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. diff --git a/frontend/src/components/Common/OverviewToolbar.tsx b/frontend/src/components/Common/OverviewToolbar.tsx index 4554bd8..45a9e8a 100644 --- a/frontend/src/components/Common/OverviewToolbar.tsx +++ b/frontend/src/components/Common/OverviewToolbar.tsx @@ -303,10 +303,10 @@ export function ConfirmDelete({ {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. )} diff --git a/frontend/src/components/Dashboard/PanelsDialog.tsx b/frontend/src/components/Dashboard/PanelsDialog.tsx index b561955..9bf3bd8 100644 --- a/frontend/src/components/Dashboard/PanelsDialog.tsx +++ b/frontend/src/components/Dashboard/PanelsDialog.tsx @@ -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() { Panels 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. @@ -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({ event.currentTarget.select()} diff --git a/frontend/src/components/Flow/NodePanel.tsx b/frontend/src/components/Flow/NodePanel.tsx index acf995b..af62453 100644 --- a/frontend/src/components/Flow/NodePanel.tsx +++ b/frontend/src/components/Flow/NodePanel.tsx @@ -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({ diff --git a/frontend/src/components/Flow/queries.ts b/frontend/src/components/Flow/queries.ts index 993eb3e..66a748d 100644 --- a/frontend/src/components/Flow/queries.ts +++ b/frontend/src/components/Flow/queries.ts @@ -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), diff --git a/frontend/src/components/Flow/useFlowSocket.ts b/frontend/src/components/Flow/useFlowSocket.ts index 147eb90..acff808 100644 --- a/frontend/src/components/Flow/useFlowSocket.ts +++ b/frontend/src/components/Flow/useFlowSocket.ts @@ -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 } | { @@ -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) diff --git a/frontend/src/components/Health/HealthOverview.tsx b/frontend/src/components/Health/HealthOverview.tsx index 848b878..6d32d3e 100644 --- a/frontend/src/components/Health/HealthOverview.tsx +++ b/frontend/src/components/Health/HealthOverview.tsx @@ -75,7 +75,7 @@ export function HealthOverview({ const queue = (summary?.queue ?? {}) as Record 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) diff --git a/frontend/src/components/Health/LiveIndicator.tsx b/frontend/src/components/Health/LiveIndicator.tsx index 55d182d..f5a4d1a 100644 --- a/frontend/src/components/Health/LiveIndicator.tsx +++ b/frontend/src/components/Health/LiveIndicator.tsx @@ -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. diff --git a/frontend/src/components/Sidebar/AppSidebar.tsx b/frontend/src/components/Sidebar/AppSidebar.tsx index 6c5ef45..db6534b 100644 --- a/frontend/src/components/Sidebar/AppSidebar.tsx +++ b/frontend/src/components/Sidebar/AppSidebar.tsx @@ -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, diff --git a/frontend/src/components/UserSettings/RemoteAccess.tsx b/frontend/src/components/UserSettings/RemoteAccess.tsx index 2c06e1b..0c9cbaa 100644 --- a/frontend/src/components/UserSettings/RemoteAccess.tsx +++ b/frontend/src/components/UserSettings/RemoteAccess.tsx @@ -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() { Remote access - 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. @@ -133,9 +133,9 @@ export function RemoteAccess() { own user. - + - {status.installation_id ?? "—"} + {status.instance_id ?? "—"} @@ -156,8 +156,8 @@ export function RemoteAccess() {

Remote users

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

@@ -173,9 +173,9 @@ export function RemoteAccess() { } />

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

@@ -210,7 +210,7 @@ export function RemoteAccess() { onChange={(event) => setCode(event.target.value.toUpperCase())} />

- Get a code at fluksio.com → Installations → Add installation. + Get a code at fluksio.com → Instances → Add instance.

@@ -235,8 +235,8 @@ export function RemoteAccess() { Disconnect from the portal? 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. diff --git a/frontend/src/lib/connectionStore.ts b/frontend/src/lib/connectionStore.ts index 532f17a..0b66b07 100644 --- a/frontend/src/lib/connectionStore.ts +++ b/frontend/src/lib/connectionStore.ts @@ -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( diff --git a/frontend/src/lib/portal.ts b/frontend/src/lib/portal.ts index f749717..c2b74c3 100644 --- a/frontend/src/lib/portal.ts +++ b/frontend/src/lib/portal.ts @@ -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 } diff --git a/frontend/src/lib/webpush.ts b/frontend/src/lib/webpush.ts index 47c1bee..87bd54f 100644 --- a/frontend/src/lib/webpush.ts +++ b/frontend/src/lib/webpush.ts @@ -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 { } /** - * 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. diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 80b106b..e4d0115 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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") diff --git a/frontend/src/routes/_layout/alerts.tsx b/frontend/src/routes/_layout/alerts.tsx index 8429d58..625172b 100644 --- a/frontend/src/routes/_layout/alerts.tsx +++ b/frontend/src/routes/_layout/alerts.tsx @@ -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() diff --git a/frontend/src/routes/_layout/index.tsx b/frontend/src/routes/_layout/index.tsx index 589e994..509cf10 100644 --- a/frontend/src/routes/_layout/index.tsx +++ b/frontend/src/routes/_layout/index.tsx @@ -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. */ diff --git a/frontend/src/routes/_layout/settings.tsx b/frontend/src/routes/_layout/settings.tsx index 582244f..788fa41 100644 --- a/frontend/src/routes/_layout/settings.tsx +++ b/frontend/src/routes/_layout/settings.tsx @@ -50,7 +50,7 @@ function UserSettings() { - {/* 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 && ( diff --git a/frontend/src/routes/panel.index.tsx b/frontend/src/routes/panel.index.tsx index 531d2f3..0afd086 100644 --- a/frontend/src/routes/panel.index.tsx +++ b/frontend/src/routes/panel.index.tsx @@ -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. */ diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index af9f963..a7eb1eb 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -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) { diff --git a/frontend/tests/panel.spec.ts b/frontend/tests/panel.spec.ts index d2dcaf9..179e785 100644 --- a/frontend/tests/panel.spec.ts +++ b/frontend/tests/panel.spec.ts @@ -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" }) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 362ba28..43fd394 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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: { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7932128..f61a045 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -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: {