From dfd1eb0ebe5bf54e8c5e4c84eeef060af5038bb8 Mon Sep 17 00:00:00 2001 From: samishal1998 Date: Tue, 1 Sep 2026 05:13:18 +0300 Subject: [PATCH] refactor(ui): state, not explanation, across every view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default-visible copy drops from 7,788 to 2,310 words over 23 views. The pages were explaining themselves — how issuance works, why a mode inverts, what a label means — which is documentation in the wrong place, and docs/ already holds it. What is left is state: labels, values, and the one line an operator acts on. Control stack leads with a vitals strip (restarts, OOM, mode, cert days, domains) instead of a paragraph describing them. Safety interlocks are NOT copy and were kept, shortened to a line each: partial state after a cancel, the orphaned-axis warning when a replaced spec omits something, TLS lost by removing the wildcard, an SSO provider whose accounts outlive it, a swarm token's rotation command, a personal token's blast radius. Removing the wildcard also gains the confirm it never had — the deleted prose had been the only guard on a one-click host-wide TLS outage. Empty and error states keep their qualifiers: an empty control-container list means the project label changed, not that nothing runs, and a non-zero log exit stays a hedge rather than an assertion. --- apps/ui/src/styles/app.css | 26 ++++ apps/ui/src/views/ConfigView.vue | 14 +-- apps/ui/src/views/ControlView.vue | 128 +++++++------------ apps/ui/src/views/DashboardView.vue | 45 ++----- apps/ui/src/views/JobDetailView.vue | 66 ++++------ apps/ui/src/views/LoginView.vue | 27 +--- apps/ui/src/views/NotifiersView.vue | 62 ++-------- apps/ui/src/views/RegistriesView.vue | 77 +++--------- apps/ui/src/views/RoutingView.vue | 91 +++----------- apps/ui/src/views/SettingsView.vue | 103 ++++----------- apps/ui/src/views/SpecDetailView.vue | 25 ++-- apps/ui/src/views/SpecsView.vue | 39 ++---- apps/ui/src/views/SsoView.vue | 165 ++++--------------------- apps/ui/src/views/SubmitView.vue | 94 +++++--------- apps/ui/src/views/SwarmView.vue | 72 +++-------- apps/ui/src/views/UsersView.vue | 50 +++----- apps/ui/src/views/VariablesView.vue | 57 +++------ apps/ui/src/views/tabs/AxesTab.vue | 31 ++--- apps/ui/src/views/tabs/ConfigTab.vue | 44 +------ apps/ui/src/views/tabs/DangerTab.vue | 113 ++++++----------- apps/ui/src/views/tabs/LogsTab.vue | 50 ++------ apps/ui/src/views/tabs/OverviewTab.vue | 70 ++--------- apps/ui/src/views/tabs/RuntimeTab.vue | 109 ++-------------- apps/ui/src/views/tabs/TerminalTab.vue | 31 ++--- 24 files changed, 400 insertions(+), 1189 deletions(-) diff --git a/apps/ui/src/styles/app.css b/apps/ui/src/styles/app.css index c33a801..3f7ad40 100644 --- a/apps/ui/src/styles/app.css +++ b/apps/ui/src/styles/app.css @@ -2318,3 +2318,29 @@ input[type='radio']:disabled { :root[data-motion='none'] .refresh-icon.spin { animation: none; } + +/* + * The vitals strip — a control plane's instrument row, under the page title. + * + * Numbers, not sentences: it replaced three paragraphs explaining why restarts and OOM kills + * matter. Monospaced and tabular so the digits hold their column as they change on the poll, and + * COLOURLESS until something is wrong — a healthy control plane should read as one quiet grey line, + * which is what makes the red mean anything. + */ +.vitals { + display: flex; + flex-wrap: wrap; + gap: var(--s1) var(--s3); + margin-top: var(--s2); + font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-size: var(--t-xs); + font-variant-numeric: tabular-nums; + color: var(--fg-mute); +} +.vitals > * + *::before { + content: '·'; + margin-right: var(--s3); + color: var(--line); +} +.vitals .v-warn { color: var(--leak); } +.vitals .v-fail { color: var(--fail); } diff --git a/apps/ui/src/views/ConfigView.vue b/apps/ui/src/views/ConfigView.vue index f9e23cd..f4357cf 100644 --- a/apps/ui/src/views/ConfigView.vue +++ b/apps/ui/src/views/ConfigView.vue @@ -105,12 +105,7 @@ async function run(mode: 'preview' | 'apply'): Promise {

Apply a configuration

-

- A sealed pstack pull config export from another host: accounts, API tokens, - variables and secrets, notifiers, sign-on, registry logins, routing files and named specs. - It creates what is missing and never overwrites, so applying one twice changes - nothing the second time. -

+

A sealed export from another host. Creates what is missing, never overwrites.

@@ -129,10 +124,7 @@ async function run(mode: 'preview' | 'apply'): Promise { placeholder="the passphrase this file was sealed with" @input="clearPreview" /> - - Sent to this host so it can open the file. That is safe in a way exporting would not be: - everything inside is about to be stored here in plain text regardless. - + Sent to this host to open the file.
@@ -149,7 +141,7 @@ async function run(mode: 'preview' | 'apply'): Promise { :disabled="!preview || !!busy" @click="run('apply')" /> - Preview first — Apply stays disabled until you have seen what this file does. + Preview before applying.
diff --git a/apps/ui/src/views/ControlView.vue b/apps/ui/src/views/ControlView.vue index 6baa810..1c3f396 100644 --- a/apps/ui/src/views/ControlView.vue +++ b/apps/ui/src/views/ControlView.vue @@ -19,8 +19,6 @@ import { ago, sentence, stamp } from '../composables/useFormat'; import { toast } from '../composables/useToasts'; import ActionButton from '../components/ActionButton.vue'; import ErrorNote from '../components/ErrorNote.vue'; -import HelpModal from '../components/HelpModal.vue'; -import InfoHint from '../components/InfoHint.vue'; import RefreshButton from '../components/RefreshButton.vue'; import SkeletonList from '../components/SkeletonList.vue'; @@ -49,7 +47,7 @@ async function restart(service: string): Promise { toast('error', problem(r, `restart ${service}`)); return; } - toast('ok', `Restarting ${r.body.container} — back in a few seconds.`); + toast('ok', `Restarting ${r.body.container}.`); await load(); } @@ -81,7 +79,7 @@ async function saveDomains(next: string[]): Promise { return; } newDomain.value = ''; - toast('ok', 'Saved — Traefik picks these up within a couple of seconds.'); + toast('ok', 'Domains saved.'); await Promise.all([loadDomains(), loadTls()]); } @@ -96,6 +94,15 @@ async function loadTls(): Promise { } usePolling(loadTls, 30_000); +// The two numbers that explain a control plane misbehaving, summed for the strip. +const restarts = computed(() => (view.value?.containers ?? []).reduce((n, c) => n + c.restartCount, 0)); +const oomed = computed(() => (view.value?.containers ?? []).some((c) => c.oomKilled)); +// The primary is a hostname, not an entry in `domains` — but it is only there once init has set +// one, so it is counted rather than assumed. +const domainCount = computed(() => + domains.value ? domains.value.domains.length + (domains.value.primary ? 1 : 0) : 0, +); + const daysLeft = computed(() => { if (!tls.value?.wildcard) return null; return Math.floor((tls.value.wildcard.notAfter - Date.now()) / 86_400_000); @@ -117,7 +124,7 @@ async function storeWildcard(): Promise { } certDraft.value = ''; keyDraft.value = ''; - toast('ok', 'Wildcard stored — new deploys inherit it now. Redeploy the rest below.'); + toast('ok', 'Stored — redeploy all stacks.'); redeployed.value = null; // that summary described a run under the previous mode await loadTls(); } @@ -132,7 +139,7 @@ async function removeWildcard(): Promise { toast('error', problem(r, 'remove the wildcard')); return; } - toast('ok', 'Removed — back to Traefik-native resolution. Redeploy the stacks below.'); + toast('ok', 'Removed — redeploy all stacks.'); redeployed.value = null; await loadTls(); } @@ -148,7 +155,7 @@ async function redeployAll(): Promise { return; } redeployed.value = r.body; - toast('ok', `Redeploying ${r.body.started.length} stack${r.body.started.length === 1 ? '' : 's'}; ${r.body.skipped.length} skipped.`); + toast('ok', `Redeploying ${r.body.started.length}, skipped ${r.body.skipped.length}.`); } @@ -156,34 +163,19 @@ async function redeployAll(): Promise {
-

- Control stack - -

- A restarting Traefik quietly breaks TLS issuance. Its certificate challenges - live in process memory, so every restart abandons whatever was mid-flight — the failed - attempts still count against Let's Encrypt's weekly limits, and every container on - this page keeps reporting running. A climbing restart count with an - OOM badge is that story told in two cells. -

-

- Restart is the only action, and never for pstack. The server refuses to restart - its own container — it is the process answering the request, and if its image were - broken, the thing that could repair this host would die with it. From the host: - docker compose -p pstack-control restart pstack. -

-

- Restarting traefik drops every connection for a few seconds — every preview, - this page included. The containers themselves keep running; only routing blinks. -

-
-

-
- The machinery previews run behind, with the counters that catch it misbehaving - - docker inspect over the control project's containers, every ten seconds. - Restart counts and OOM flags are docker's own, since this process last recreated nothing. - +

Control stack

+ +
+ {{ restarts }} restarts + OOM + {{ tls.mode }} + {{ daysLeft }}d cert + {{ domainCount }} domain{{ domainCount > 1 ? 's' : '' }}
@@ -200,10 +192,9 @@ async function redeployAll(): Promise {
- + + @@ -232,7 +223,7 @@ async function redeployAll(): Promise { OOM @@ -252,7 +243,7 @@ async function redeployAll(): Promise { v-else-if="c.service === 'pstack'" class="mute" style="font-size: var(--t-sm)" - title="The server refuses to restart the container answering this request. Restart it from the host." + title="Restart this one from the host" > host-only @@ -268,10 +259,7 @@ async function redeployAll(): Promise {
-

- Docker lists no control containers — on a host serving this page, that means the project - label changed, not that nothing runs. -

+

None listed — the project label changed, not the stack stopped.

@@ -282,15 +270,10 @@ async function redeployAll(): Promise { primary {{ domains.primary || '—' }}
-

{{ domains.note }}

-
  • {{ domains.primary || 'not set' }} - - primary — its routers are labels on the pstack container, so it keeps working whatever - else changes here. It cannot be removed from this page. - + primary
  • {{ d }} @@ -317,11 +300,7 @@ async function redeployAll(): Promise { Add
-

- Adding a domain only makes this host serve and wake names under it. To move a - deployment, set PREVIEW_DOMAIN in its variables and redeploy it — one stack at a - time, and rolling back is that same stack. -

+ @@ -332,8 +311,6 @@ async function redeployAll(): Promise { {{ tls.mode }}
-

{{ tls.note }}

- -

- Storing a wildcard is an admin's: it changes what every hostname on this host presents. -

+

Admin only.

Redeploy all stacks - - Router labels are stamped at deploy time — run this once after changing the mode. Asleep - stacks are skipped; they pick the new labels up when they wake. - + Router labels are stamped at deploy time. Asleep stacks pick them up on wake.

Started {{ redeployed.started.length }} · skipped {{ redeployed.skipped.length diff --git a/apps/ui/src/views/DashboardView.vue b/apps/ui/src/views/DashboardView.vue index 5772081..f9b3c2c 100644 --- a/apps/ui/src/views/DashboardView.vue +++ b/apps/ui/src/views/DashboardView.vue @@ -62,7 +62,6 @@ const recentJobs = computed(() => state.jobs.slice(0, 8));

Dashboard

-
Everything running on this host.
@@ -75,11 +74,7 @@ const recentJobs = computed(() => state.jobs.slice(0, 8)); diff --git a/apps/ui/src/views/RegistriesView.vue b/apps/ui/src/views/RegistriesView.vue index 407f178..8826ff3 100644 --- a/apps/ui/src/views/RegistriesView.vue +++ b/apps/ui/src/views/RegistriesView.vue @@ -24,9 +24,7 @@ import { settings } from '../composables/useSettings'; import { toast } from '../composables/useToasts'; import ActionButton from '../components/ActionButton.vue'; import ErrorNote from '../components/ErrorNote.vue'; -import InfoHint from '../components/InfoHint.vue'; import SkeletonList from '../components/SkeletonList.vue'; -import HelpModal from '../components/HelpModal.vue'; import RefreshButton from '../components/RefreshButton.vue'; const entries = ref([]); @@ -77,14 +75,9 @@ async function save(): Promise { // Cleared immediately: there is no reason for a password to stay in a form field after it is stored, // and the page cannot read it back to re-populate one. password.value = ''; - const stored = r.body.registry; - if (stored && stored !== host.value) { - // Docker Hub's canonical key differs from what anyone types; saying so avoids "I stored it and it - // still is not used". - toast('ok', `Stored for ${stored} — Docker Hub's canonical key.`); - } else { - toast('ok', `Stored for ${stored || host.value}. It applies to the next pull.`); - } + // The server's key, not the typed one: Docker Hub canonicalises, and the list below reloads with + // whatever it stored — so the toast does not have to explain the difference. + toast('ok', `Stored for ${r.body.registry || host.value}.`); host.value = ''; username.value = ''; void load(); @@ -106,15 +99,7 @@ async function forget(registry: string): Promise {

Registries

-
- Credentials for pulling private images - - A pull is authenticated by the docker client, not the daemon: it reads its own - config.json and hands the credential over. pstack's client runs inside the - control container, so a docker login on the host writes a file it cannot see — - and the pull fails on a host that is logged in. - -
+
Credentials for pulling private images
@@ -122,22 +107,9 @@ async function forget(registry: string): Promise { +
@@ -176,21 +145,13 @@ async function forget(registry: string): Promise { -

- Nothing stored. Public images need no credential — this is only for a private registry. -

+

No credentials yet. Add one below.

Add a credential

-

- Applies to the next pull — nothing needs restarting. - - The docker client re-reads config.json on every invocation, so there is no cache - to bust. Storing one now is enough for a deploy started a second later. - -

+

Applies to the next pull.

@@ -202,9 +163,6 @@ async function forget(registry: string): Promise { spellcheck="false" autocomplete="off" /> -
- Docker Hub is stored under its canonical key. -
@@ -215,10 +173,8 @@ async function forget(registry: string): Promise {
-
- Prefer a token scoped to reading packages. Stored on the host as reversible base64, exactly as - Stored the same way a terminal login stores it — never sent back to this page. -
+ +
Stored write-only, as reversible base64 — never shown again.
@@ -229,11 +185,6 @@ async function forget(registry: string): Promise { Storing needs an access token.
- -

- Or from the host, which writes the same file: - docker login --config {{ dir || '/control/docker' }} <registry> -

diff --git a/apps/ui/src/views/RoutingView.vue b/apps/ui/src/views/RoutingView.vue index e4011b9..8f00d7d 100644 --- a/apps/ui/src/views/RoutingView.vue +++ b/apps/ui/src/views/RoutingView.vue @@ -15,7 +15,7 @@ * * What it cannot break: `control.` and `api.` are docker labels on the pstack * container, not file config. This page cannot lock you out of the page you would use to undo a bad - * save. That is stated in the UI, because an operator hesitating over a save deserves to know it. + * save. That reassurance lives in docs/control-plane.md, not in this view. * * `previous` from a save is an in-session undo. There is deliberately no history on disk: the obvious * place to keep it is the one directory that must contain nothing but dynamic config. @@ -35,9 +35,7 @@ import { stamp } from '../composables/useFormat'; import { toast } from '../composables/useToasts'; import ActionButton from '../components/ActionButton.vue'; import ErrorNote from '../components/ErrorNote.vue'; -import InfoHint from '../components/InfoHint.vue'; import SkeletonList from '../components/SkeletonList.vue'; -import HelpModal from '../components/HelpModal.vue'; import RefreshButton from '../components/RefreshButton.vue'; import RouteTarget from '../components/RouteTarget.vue'; @@ -181,7 +179,7 @@ async function save(): Promise { if (r.body.previous) undo.value = { name, content: r.body.previous }; original.value = draft.value; openName.value = name; - toast('ok', `Saved ${name}. Traefik reloads within a second or two.`); + toast('ok', `Saved ${name}.`); void loadList(); } @@ -213,39 +211,8 @@ async function remove(): Promise {
-

- Routing - -

- Traefik reads this directory as a whole. One file it cannot parse and it reports a - problem for the directory — so the symptom is other, unrelated routes - disappearing rather than an error about the file you just saved. -

-

- Saves here are checked before they are written and replace the old file in one step, so a - file that would break the directory never reaches Traefik in the first place. -

-

- This page cannot lock you out. It and the API are routed by their containers' - own settings, not by these files — no save here can take away the page you would use to - undo it. -

-

- What belongs here: shared middleware (sign-in prompts, rate limits, address - allow-lists), TLS options, the catch-all fallback, and routes to things running outside - this host's previews. A single preview's own routes are not here — those travel with the - deployment. -

-
-

-
- Every route on this host, and the config files behind them - - Middleware (basic auth, rate limits, IP allow-lists), TLS options, the catch-all fallback - router, and routes to anything running outside compose. Per-PR routers are not here — - those are labels in each deployment's own compose file. - -
+

Routing

+
Every route on this host, and the files behind them
@@ -258,7 +225,7 @@ async function remove(): Promise { @@ -268,19 +235,12 @@ async function remove(): Promise {

Live routes

- - from container labels - - Traefik reads two providers. Per-PR routers are labels on containers, written by each - deployment's own compose file — so deploying a stack changes this list and never the files. - The files are the other provider: middleware, TLS options, the fallback router. - - + from container labels
@@ -337,11 +297,7 @@ async function remove(): Promise {
-

- No container on this host declares a Traefik router. A deployment's routes come from labels in - its own compose file — open a deployment's Containers & routes tab to see what is - missing. -

+

No routes yet — deploy a stack.

@@ -355,24 +311,20 @@ async function remove(): Promise {
@@ -387,9 +339,7 @@ async function remove(): Promise { -

- No files yet. Everything is routed by container labels, which is a perfectly good place to be. -

+

No files yet — everything is routed by labels.

@@ -416,10 +366,8 @@ async function remove(): Promise { - no deployments reference it + none
  • Created{{ stamp(spec.createdAt) }}
  • @@ -135,14 +127,11 @@ const users = computed(() => state.deployments.filter((d) => d.specName === prop -->
    {{ spec.source }}
    -

    The server returned no source for this spec.

    + +

    No source.

    diff --git a/apps/ui/src/views/SpecsView.vue b/apps/ui/src/views/SpecsView.vue index 8fd91f8..488f5d0 100644 --- a/apps/ui/src/views/SpecsView.vue +++ b/apps/ui/src/views/SpecsView.vue @@ -8,7 +8,7 @@ * torn down — so knowing the count before you try is the difference between a plan and a 409. * * A server built before named specs answers 404 here. That is a capability difference, not an - * error, so it gets its own explanation instead of a red banner. + * error, so it gets its own plain banner instead of a red one. */ import { computed, ref } from 'vue'; import { sentence } from '../composables/useFormat'; @@ -75,13 +75,7 @@ const shown = computed(() => {

    Specs

    -
    - Stored once, used by any number of deployments - - Without this, every deployment carries its own copy — 50 open pull requests meant 50 - identical files, and fixing a teardown step meant re-submitting it 50 times. - -
    +
    Stored once, used by any number of deployments
    @@ -89,13 +83,11 @@ const shown = computed(() => { +
    @@ -116,8 +108,7 @@ const shown = computed(() => { needs - Variables the spec uses but does not set. Every deployment referencing it must - supply these, which is how one spec serves many stacks. + Variables the spec uses but does not set. Used by @@ -137,13 +128,11 @@ const shown = computed(() => { {{ s.requiredVars.join(', ') }} - nothing + none - - {{ users.get(s.name)!.length }} deployment(s) - - nothing yet + {{ users.get(s.name)!.length }} + none @@ -156,16 +145,10 @@ const shown = computed(() => { @@ -544,36 +489,18 @@ function rulesLine(c: SsoConfig): string {
    -

    Leave empty to take the identity from the token response alone — a subject and nothing else.

    -

    - Only consulted when the profile carries no address — which is GitHub's default. Filled - in automatically for a preset, and needed only if you pointed the user info URL at your - own host. -

    -

    - Where this host asks which groups someone is in. Only consulted when there is a group - rule below. Filled in automatically for a preset — but only while the user info URL is - still the preset's, because a self-hosted host's token must not be sent to the public - one, so a self-hosted provider needs this typed. -

    -

    - Claim mapping - - Which key of the provider's user response holds each field. Flat lookups, not - expressions. A preset fills these in; you only touch them for a provider nobody has - written a preset for. - -

    + +

    Claim mapping

    @@ -585,23 +512,15 @@ function rulesLine(c: SsoConfig): string {
    -

    Who gets an account, and as what

    +

    Who gets in

    - By default, anyone this provider lets authenticate. It already owns that decision; this is - only here for the case where the application you registered is broader than the people who - should reach this control plane. Any one entry in a list is enough to satisfy that - list, and every list you fill in has to be satisfied — fill all three in and a login - needs a listed domain, a matching username, and one of the groups. A list left empty is not - a rule at all: it lets everyone past that check, it does not lock everyone out. + Every filled list must match; empty lists restrict nobody.

    - Comma-separated; subdomains count. Empty means no restriction. Non-empty means a login - with no email address at all is refused too — a provider that hides the address (a - private GitHub profile with no user:email scope) cannot be checked, so it is - not waved through. + Comma-separated; subdomains count. When set, a login with no address is refused.

    @@ -609,12 +528,8 @@ function rulesLine(c: SsoConfig): string {

    - Comma-separated patterns, case-insensitive: *, ? and - [0-9] classes. Empty means no restriction. Non-empty means a login whose - provider sends no username is refused too — so this is a rule for a provider that - actually has usernames (GitHub's login, GitLab's username). On - an OpenID Connect issuer that sends no preferred_username it refuses - everyone rather than nobody, so set it only where you know the provider sends one. + Comma-separated patterns: *, ?, [0-9]. + When set, a login with no username is refused.

    @@ -622,26 +537,16 @@ function rulesLine(c: SsoConfig): string {

    - Comma-separated, and exact names, not patterns — an organisation login, or a group - path like acme/backend. Case-insensitive. Empty means no restriction. - Non-empty makes every sign-in ask the provider for that person's groups, so it needs the - OAuth scope that endpoint requires: add it to Scopes above, or the save is refused - — and the refusal names the scope. It also needs a provider whose group list this host - knows how to read, which the save checks by name. If that call does not answer, the - sign-in is refused rather than allowed. + Exact names, not patterns, comma-separated. Needs the matching scope above, or + the save is refused.

    -

    - A group rule needs a provider whose groups endpoint this host knows how to read — an OAuth - 2.0 preset (GitHub, GitLab), not a discovered OpenID Connect issuer — so there is none - here. -

    - +

    - Applied when an account is created here — on someone's first sign-in — and never after: - promoting or demoting a person later is done in Accounts, and this does not reach back - and change them. - The host default follows Settings rather than being copied in now, so raising or - lowering it there moves this provider with it. - {{ hostDefaultRole ? `It is ${hostDefaultRole} today.` : '' }} - Picking a role here pins this provider to it instead. + Set at first sign-in. Host default follows + Settings.

    @@ -678,11 +578,7 @@ function rulesLine(c: SsoConfig): string {
    -

    - Pick where your team already signs in. A preset knows the provider's endpoints and its - quirks — the only things it cannot know are the client id and secret you get when you - register the app, and the form that opens says exactly where to do that. -

    +

    Pick where your team already signs in.

    -

    - Removing a provider does not delete anyone. Those accounts keep their personal tokens, and - pointing the same provider back at this host re-links them by subject. -

    Callback URL

    -

    - The redirect URI to register with every provider — one URL for all of them, built by the - server so it cannot drift from the one the sign-in flow actually sends. -

    +

    Register this with every provider.

    {{ callbackUrl }} diff --git a/apps/ui/src/views/SubmitView.vue b/apps/ui/src/views/SubmitView.vue index c20de72..acb36d9 100644 --- a/apps/ui/src/views/SubmitView.vue +++ b/apps/ui/src/views/SubmitView.vue @@ -312,10 +312,10 @@ function loadExample(): void {
    {{ replacing - ? 'Loaded from what is stored; saving replaces the record' + ? 'Saving replaces the record' : copyFrom - ? `Copied from ${copyFrom} — give it a new id, then edit` - : 'Stores a spec so it can be deployed' + ? `Copied from ${copyFrom} — give it a new id` + : 'Store a spec, then deploy it' }}
    @@ -328,12 +328,6 @@ function loadExample(): void { moment of commitment is where a guarantee about committing belongs, not floating above an unrelated id field. --> - @@ -378,11 +370,11 @@ function loadExample(): void { following it. Silent forks are the kind of thing discovered months later. --> @@ -401,8 +393,7 @@ function loadExample(): void { Lower case - Starts with a letter or digit, then letters, digits, dot, dash or underscore. Up to 64 - characters — [a-z0-9][a-z0-9._-]{0,63}. + [a-z0-9][a-z0-9._-]{0,63} @@ -427,10 +418,6 @@ function loadExample(): void { needs {{ chosenSpec.requiredVars.join(', ') }} no variables. - - A missing variable is refused by name. It is never filled in with a blank, which would - silently give every deployment the same stack name. -

    @@ -453,28 +440,15 @@ function loadExample(): void { v-model="form.compose" rows="6" spellcheck="false" - placeholder="Written next to spec.yml, so a compose file named compose.yml resolves" + placeholder="Written next to spec.yml" /> -

    - Hooks must be inline shell or an absolute path. - - A deployment directory only ever holds spec.yml and - compose.yml, and hooks run from there — so - up: ./hooks/db.sh has nothing to find. - -

    +

    Hooks must be inline shell or an absolute path.

    Variables

    -

    - Saved with the deployment, so tearing it down later targets the same stack. - - They are stored on the server where it supports that, and in this browser either way — so - the actions on the deployment page send exactly these values. - -

    +

    Saved with the deployment, so teardown targets the same stack.

    @@ -482,11 +456,6 @@ function loadExample(): void { Nothing is saved until the spec is valid - - The spec is checked before anything is written, so a rejected submission leaves nothing - behind — and a replace never destroys a good record over a typo. Whether the deployment - is shared or isolated comes from the spec, not from this form. - {{ result.created ? 'Created' : 'Replaced' }} — stack @@ -508,14 +477,13 @@ function loadExample(): void { it can be deliberate, and refusing over a guess would be worse than saying so. --> @@ -525,11 +493,7 @@ function loadExample(): void { point is to reach the author while they are still looking at the file they just wrote. -->
    @@ -204,10 +175,7 @@ onBeforeUnmount(() => {

    Add a worker

    -

    - Open these between the new machine and every other node first — a worker that cannot reach - them joins and then never receives a task. -

    +

    Open these between the new machine and every other node.

    • {{ p.port }} @@ -217,11 +185,11 @@ onBeforeUnmount(() => {
      - + @@ -231,7 +199,7 @@ onBeforeUnmount(() => {
      @@ -244,10 +212,10 @@ onBeforeUnmount(() => {