workspaces 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/DESIGN.md +11 -11
- data/lib/workspaces/dashboard/app.js +0 -17
- data/lib/workspaces/dashboard/card.html.erb +1 -1
- data/lib/workspaces/dashboard/styles.css +14 -44
- data/lib/workspaces/dashboard/template.html.erb +10 -10
- data/lib/workspaces/dashboard_view.rb +19 -1
- data/lib/workspaces/presentation.rb +17 -27
- data/lib/workspaces/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b62d0530751f975a4e8604a691dce7bc133c3d7bd57a966e08e9b9e24c7088d9
|
|
4
|
+
data.tar.gz: 3fb3b5acff924444694d1390b9edd0f4dc2eaf5885da24b938552180b52a0b0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e860375ef8492aba63ef9340ff2ad29e22fc504353546d343b3e2c3dc31833b9df5bc77de4a8b367eeeb8e3423aef5918e0f61a51d4de26dd8ea9d916af7bec3
|
|
7
|
+
data.tar.gz: d8521f83fd632951c5ac551f52f1c4a72db8d22afd33dca483bad10e1f82456f01d8fcf4fa0d50fc00e93cb98da32eab34ae6c16454a8cb931e5654c9349ee19
|
data/DESIGN.md
CHANGED
|
@@ -38,7 +38,7 @@ Restrained tooling UI: a near-black page, one slightly lighter card surface with
|
|
|
38
38
|
## Component primitives
|
|
39
39
|
|
|
40
40
|
- **Creation form**: four labelled fields in one wrapping row (`flex: 1 1 220px`, 16px gap), each with a real `<label for>`, then a `type="submit"` button on its own line.
|
|
41
|
-
- **Workspace inventory**: a centered column up to 1180px. The page header carries "N managed worktrees · none running" (or "· K running"), counted from the data. The table sits in an `overflow-x: auto` wrapper with `min-width: 820px`, so a phone scrolls it sideways instead of cramming or hiding columns. Rows have 16px padding; workspace names are monospaced links with no underline until hover. Times read "Oct 27, 10:48", are UTC (the headers say so) and keep the full ISO value in a `title`. A missing start reads "Never" and a missing creation time "Unavailable", in the muted colour.
|
|
41
|
+
- **Workspace inventory**: a centered column up to 1180px. Rows are ordered by last startup, newest first; workspaces that never started (or whose start time cannot be read) come last, and ties fall back to the id so the list never shuffles between loads. Moments are compared as moments, not text. The page header carries "N managed worktrees · none running" (or "· K running"), counted from the data. The table sits in an `overflow-x: auto` wrapper with `min-width: 820px`, so a phone scrolls it sideways instead of cramming or hiding columns. Rows have 16px padding; workspace names are monospaced links with no underline until hover. Times read "Oct 27, 10:48", are UTC (the headers say so) and keep the full ISO value in a `title`. A missing start reads "Never" and a missing creation time "Unavailable", in the muted colour.
|
|
42
42
|
- **Status pill**: a dot and one word (13px, 600, fully rounded), never colour alone. A busy workspace adds "(busy)" and pulses its dot, which stops under reduced motion.
|
|
43
43
|
- **Setup progress**: names and ordered states come from repository run steps; failures are highlighted. Restart shows the background launch and later readiness commands.
|
|
44
44
|
- **Actions by state**: idle/stopped offers Start; active operations show progress without lifecycle buttons; failure offers Retry; ready offers Open workspace plus rebuild/restart/stop.
|
|
@@ -49,18 +49,18 @@ Restrained tooling UI: a near-black page, one slightly lighter card surface with
|
|
|
49
49
|
- **Shared with the settings page**: the card reuses the page's element ids and `app.js`, so polling needs no second script. `app.js` tolerates the elements the card lacks (actions, steps, logs, environment).
|
|
50
50
|
- **On a preview host** (`data-base="/__workspaces"`, `data-reload-when-ready="true"`): the card is served from the workspace's own origin, so it is the smallest surface that can work. It polls `/__workspaces/status`, posts only to `/__workspaces/start|prepare|restart|resume`, shows a fixed failure line instead of error text, and reloads the current page once when the workspace is ready. Everything else (settings, environment, secrets, logs, removal) stays on the shared dashboard origin behind its same-origin check.
|
|
51
51
|
- **Settings page**: the full page described below, with a *Back to workspace* link. It is where Update from PR, keep-awake, rebuild, restart, stop, environment editing and deletion live.
|
|
52
|
-
- **Three layers** (the settings page; the card shows only the lead): the workspace page leads with the friendly, the deliberate, then the technical.
|
|
52
|
+
- **Three layers** (the settings page; the card shows only the lead): the workspace page leads with the friendly, the deliberate, then the technical. Which layers are shown depends on lifecycle state, driven by `Presentation::STATES`. They are plain sections, never folded: the card at `/workspaces/ID` is what keeps the details out of the way, so the settings page has nothing to hide.
|
|
53
53
|
1. **Lead** (always visible): a plain-language headline, one sentence, and at most one primary action (a large button). The setup steps never appear here.
|
|
54
|
-
2. **Settings** (a
|
|
55
|
-
3. **Technical details** (
|
|
54
|
+
2. **Settings** (a section headed *Change settings*, with a *Preset: name* badge beside the heading whenever the saved settings exactly match a preset, or *Choose what to preview* on a brand new workspace): presets first, as buttons, then the environment variables. The one submit button names what it does (Start workspace, Save & restart, Save & start, Save & retry).
|
|
55
|
+
3. **Technical details** (a section): setup step logs with timings, the web server log, workspace controls (rebuild, restart, stop, update from PR), preview URL, branch and path, and the Danger Zone. It is shown whenever the state has something to show (while setup runs, after a failure, and once ready), so a cause is never hidden.
|
|
56
56
|
- **Lead by state**:
|
|
57
|
-
- `idle`: "Let's get your workspace running", with the settings
|
|
58
|
-
- `busy`: "Setting things up…", with the current step as a quiet line and the elapsed time. Technical details
|
|
59
|
-
- `ready`: "Your workspace is ready" with a large **Open workspace** button.
|
|
57
|
+
- `idle`: "Let's get your workspace running", with the settings shown and *Start workspace* in the form.
|
|
58
|
+
- `busy`: "Setting things up…", with the current step as a quiet line and the elapsed time. Technical details are shown so progress and the log can be watched.
|
|
59
|
+
- `ready`: "Your workspace is ready" with a large **Open workspace** button. Settings and technical details sit below it.
|
|
60
60
|
- `stopped`: "Your workspace is paused" with a large **Start workspace** button.
|
|
61
|
-
- `error`/`interrupted`: a plain "Something didn't go to plan" with a large **Retry failed step** button (**Continue setup** after an interruption) that names the step in the summary, a subtle **Re-run all steps** beside it, the error shown, and technical details
|
|
62
|
-
- `unknown`: a minimal view with technical details
|
|
63
|
-
- **
|
|
61
|
+
- `error`/`interrupted`: a plain "Something didn't go to plan" with a large **Retry failed step** button (**Continue setup** after an interruption) that names the step in the summary, a subtle **Re-run all steps** beside it, the error shown, and technical details shown. When the recorded steps cannot be resumed, the single **Retry** of the earlier design is shown instead.
|
|
62
|
+
- `unknown`: a minimal view with technical details shown.
|
|
63
|
+
- **Section visibility**: the server renders which sections the state shows, and the polling script reveals or hides them as the state changes. Sections that are not meaningful in a state are hidden, never shown disabled. Nothing on the page folds except the per-step log rows below, so polling never opens or closes anything the reader is looking at.
|
|
64
64
|
- **Per-step re-run controls**: inside each step's accordion, small subtle **Re-run this step** and **Re-run from here** forms aimed at `/workspaces/ID/resume?only=KEY` and `?from=KEY`. The server decides which appear (`presentation.resume.step_modes`), so the page never offers something that would be refused; they are hidden while a run is in progress and are disabled with the other controls while busy.
|
|
65
65
|
- **Sleeping**: a workspace asleep is its own state (`sleeping`), distinct from stopped and failed. The lead reads "Your workspace is asleep", says files and data are kept and that opening the preview wakes it, and offers one **Wake workspace** button. A ready workspace shows one muted line, "Sleeps after N min without preview traffic", or "Kept awake" when exempt. **Keep awake** / **Allow sleep** are subtle workspace controls. A waking workspace is simply busy.
|
|
66
66
|
- **Environment rows**: the settings form is a list of rows, each a name input, a value input and a **Secret** checkbox, with **Add variable** and a per-row **Remove** (both revealed by script; without it the server-rendered rows still save). Rows post as `env[N][name|value|secret]`. A secret row's value is a password field that is never pre-filled; a stored secret shows its name, the placeholder "Stored. Enter a new value to replace it", and a locked checked checkbox (a hidden field carries it), so it can only be replaced or removed, not exposed. Ticking Secret on a new row switches its value to a password field. Presets replace only the visible rows and are matched against visible rows alone. A note under the rows shows why secrets are unavailable when the vault is not usable, and a line explains that secrets are write-only.
|
|
@@ -70,7 +70,7 @@ Restrained tooling UI: a near-black page, one slightly lighter card surface with
|
|
|
70
70
|
- **Step logs (GitHub Actions style)**: setup output is split by recipe step. Each step is a `<details>` row with a status icon (ring, pulsing ring, check or cross), its name, how long it took and a chevron; opening it shows only that step's output. A step that is running or has failed starts open and a completed or waiting one starts closed. The script updates rows in place by step id, and applies the open preference only when a step's state changes, so a row the reader toggled is never reopened or reclosed and what they are reading is never rebuilt.
|
|
71
71
|
- **Capture**: `StepLog` wraps the combined log. Every line still goes to the combined `<id>.setup.log`; while a step is current the same scrubbed text also goes to `logs/<id>.steps/<step-id>.log`. A step's file is truncated when it begins, the files are cleared when a run starts, and a failing step records its error message in its own file.
|
|
72
72
|
- **Read**: the snapshot carries `step_logs` (the tail of each step that has output, with a `truncated` flag) for the setup view only, and `backend_log_available`.
|
|
73
|
-
- **Tabs**: *Setup steps* (the
|
|
73
|
+
- **Tabs**: *Setup steps* (the step rows) and *Web server log*. The server tab appears only once the backend log has content. Switching tabs reloads the page, and the links end in `#log-title` so the reader lands back on the log rather than the top of the page. The combined log stays reachable as a final, collapsed *Full setup log* row, which also covers workspaces set up before step files existed.
|
|
74
74
|
- **Build progress**: while a build is being prepared the lead is the headline, a progress bar (`role="progressbar"` with a text value), and one line saying *Step n of m · name*, the percentage, the elapsed time and an estimate. The server computes the numbers from the step states and per-step averages frozen when the run began (`Progress`), and the script only draws them; the elapsed clock ticks locally between polls. A step well past its usual time turns the bar amber and the estimate reads "Taking longer than usual". Without history there is no time estimate. The bar is hidden when no build is running.
|
|
75
75
|
- **Build history**: every finished run is appended to `build-history.jsonl` (`BuildHistory`). Estimates are the mean of a step's last 10 successful completions; failed runs are recorded but never counted. Recording is best-effort and cannot fail a build.
|
|
76
76
|
- **Bounded log panel**: monospace text, fixed max height, manual-scroll-safe updates
|
|
@@ -244,7 +244,6 @@
|
|
|
244
244
|
element.hidden = !sections[element.dataset.section];
|
|
245
245
|
});
|
|
246
246
|
renderLead(view);
|
|
247
|
-
applyDisclosureState(view);
|
|
248
247
|
if (window.WorkspaceEnvironment)
|
|
249
248
|
window.WorkspaceEnvironment.setSubmitLabel(view.environment_submit);
|
|
250
249
|
}
|
|
@@ -325,22 +324,6 @@
|
|
|
325
324
|
toggleBusyState(busy);
|
|
326
325
|
}
|
|
327
326
|
|
|
328
|
-
// A disclosure opens by itself when its state says so (a failure must never be hidden,
|
|
329
|
-
// a fresh workspace needs its settings), but we never close one the user opened, and
|
|
330
|
-
// we only apply the state's preference when the state changes, not on every poll.
|
|
331
|
-
let lastDisclosureState = root.dataset.disclosureState || null;
|
|
332
|
-
function applyDisclosureState(view) {
|
|
333
|
-
if (view.state === lastDisclosureState) return;
|
|
334
|
-
lastDisclosureState = view.state;
|
|
335
|
-
const open = view.open || {};
|
|
336
|
-
[['environment-disclosure', open.environment], ['technical-disclosure', open.technical]].forEach(
|
|
337
|
-
([id, shouldOpen]) => {
|
|
338
|
-
const element = node(id);
|
|
339
|
-
if (element) element.open = !!shouldOpen;
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
327
|
// A card on a preview host stands in for the page the visitor asked for; reloading brings them to it.
|
|
345
328
|
let reloading = false;
|
|
346
329
|
function reloadWhenReady(view) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<main class="card-page" id="workspace" data-id="<%= @id %>" data-base="<%= action_base %>" data-reload-when-ready="<%= @preview ? 'true' : 'false' %>" data-status="<%= field('status') %>" data-started="<%= field('prepare_started_at') %>" data-completed="<%= field('completed_at') %>" data-busy="<%= @presentation['busy'] %>"
|
|
1
|
+
<main class="card-page" id="workspace" data-id="<%= @id %>" data-base="<%= action_base %>" data-reload-when-ready="<%= @preview ? 'true' : 'false' %>" data-status="<%= field('status') %>" data-started="<%= field('prepare_started_at') %>" data-completed="<%= field('completed_at') %>" data-busy="<%= @presentation['busy'] %>">
|
|
2
2
|
<section class="card" aria-labelledby="status">
|
|
3
3
|
<header class="card__header">
|
|
4
4
|
<p class="card__name"><span class="eyebrow">Workspace</span><span class="card__id"><%= @id %></span></p>
|
|
@@ -205,48 +205,20 @@ h1 {
|
|
|
205
205
|
.lead__action:empty { display: none; }
|
|
206
206
|
.button--large { min-block-size: 52px; padding-inline: var(--space-8); font-size: 16px; border-radius: var(--radius-control); }
|
|
207
207
|
|
|
208
|
-
/*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
/* Two classes so the narrow-screen panel padding below cannot re-add padding around the summary. */
|
|
213
|
-
.status-panel.disclosure { padding: 0; }
|
|
214
|
-
.disclosure > summary {
|
|
208
|
+
/* Settings page sections: a heading, a one-line hint of what is inside, then the content. Nothing
|
|
209
|
+
folds; the card at /workspaces/ID is what keeps these details out of the way. The section is a grid
|
|
210
|
+
(.status-panel:not(details)), so the gap below the heading comes from there. */
|
|
211
|
+
.section-heading {
|
|
215
212
|
display: flex;
|
|
216
213
|
flex-wrap: wrap;
|
|
217
214
|
align-items: baseline;
|
|
218
215
|
gap: var(--space-1) var(--space-3);
|
|
219
|
-
padding
|
|
220
|
-
|
|
221
|
-
list-style: none;
|
|
222
|
-
color: var(--text);
|
|
223
|
-
font-weight: 600;
|
|
224
|
-
border-radius: var(--radius);
|
|
225
|
-
cursor: pointer;
|
|
226
|
-
user-select: none;
|
|
227
|
-
transition: background-color 120ms ease;
|
|
216
|
+
padding-block-end: var(--space-4);
|
|
217
|
+
border-block-end: 1px solid var(--border);
|
|
228
218
|
}
|
|
229
|
-
.
|
|
230
|
-
.
|
|
231
|
-
|
|
232
|
-
position: absolute;
|
|
233
|
-
inset-inline-end: var(--panel-padding);
|
|
234
|
-
inset-block-start: 50%;
|
|
235
|
-
inline-size: 8px;
|
|
236
|
-
block-size: 8px;
|
|
237
|
-
margin-block-start: -6px;
|
|
238
|
-
border-inline-end: 2px solid var(--muted);
|
|
239
|
-
border-block-end: 2px solid var(--muted);
|
|
240
|
-
transform: rotate(45deg);
|
|
241
|
-
transition: transform 160ms ease, border-color 120ms ease;
|
|
242
|
-
}
|
|
243
|
-
.disclosure[open] > summary::after { margin-block-start: -2px; transform: rotate(225deg); }
|
|
244
|
-
.disclosure > summary:hover { background: color-mix(in oklab, var(--accent) 8%, transparent); }
|
|
245
|
-
.disclosure > summary:hover::after { border-color: var(--text); }
|
|
246
|
-
.disclosure > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
247
|
-
.disclosure[open] > summary { border-end-start-radius: 0; border-end-end-radius: 0; border-block-end: 1px solid var(--border); margin-block-end: 0; }
|
|
248
|
-
.disclosure > :not(summary) { margin: var(--panel-padding); }
|
|
249
|
-
/* Which preset is active, readable without opening the section. Same tint as a pressed preset button. */
|
|
219
|
+
.section-heading h2 { margin: 0; font-size: 1.125rem; }
|
|
220
|
+
.section-heading p { margin: 0; font-size: 13px; }
|
|
221
|
+
/* Which preset is active, readable at a glance. Same tint as a pressed preset button. */
|
|
250
222
|
.badge {
|
|
251
223
|
display: inline-flex;
|
|
252
224
|
align-items: center;
|
|
@@ -262,14 +234,12 @@ h1 {
|
|
|
262
234
|
white-space: nowrap;
|
|
263
235
|
}
|
|
264
236
|
.badge::before { content: ''; inline-size: 6px; block-size: 6px; border-radius: 999px; background: var(--success); }
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.disclosure .danger-zone { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--danger); border-radius: var(--radius); }
|
|
269
|
-
.disclosure .log-panel { padding: 0; }
|
|
237
|
+
#technical-section .technical > section:not(.danger-zone) { padding: 0; border: 0; box-shadow: none; background: none; }
|
|
238
|
+
#technical-section .danger-zone { display: grid; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--danger); border-radius: var(--radius); }
|
|
239
|
+
#technical-section .log-panel { padding: 0; }
|
|
270
240
|
.log-panel > #log { margin-block-start: 0; }
|
|
271
|
-
|
|
272
|
-
|
|
241
|
+
#technical-section .danger-zone h3 { color: var(--danger); font-size: 1rem; }
|
|
242
|
+
#technical-section h3 { font-size: 1rem; }
|
|
273
243
|
|
|
274
244
|
.status-row {
|
|
275
245
|
display: grid;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<main class="dashboard" id="workspace" data-id="<%= @id %>" data-status="<%= field('status') %>" data-started="<%= field('prepare_started_at') %>" data-completed="<%= field('completed_at') %>" data-busy="<%= @presentation['busy'] %>"
|
|
1
|
+
<main class="dashboard" id="workspace" data-id="<%= @id %>" data-status="<%= field('status') %>" data-started="<%= field('prepare_started_at') %>" data-completed="<%= field('completed_at') %>" data-busy="<%= @presentation['busy'] %>">
|
|
2
2
|
<header class="dashboard__header">
|
|
3
3
|
<div class="dashboard__title"><p class="eyebrow">Managed workspace</p><h1 class="mono-title"><%= @id %></h1></div>
|
|
4
4
|
<div class="header-actions"><a class="button button--subtle" href="/workspaces/<%= @id %>">Back to workspace</a><a class="button button--subtle" href="/workspaces">All workspaces</a></div>
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
</section>
|
|
27
27
|
<p id="connection" role="status"></p>
|
|
28
28
|
</section>
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
<div class="workspace-settings"
|
|
29
|
+
<section class="status-panel" id="environment-section" data-section="environment" aria-labelledby="environment-title" <%= 'hidden' unless section?('environment') %>>
|
|
30
|
+
<header class="section-heading"><h2 id="environment-title"><%= escape(@presentation['environment_title']) %></h2><span class="badge" id="environment-preset-badge" <%= 'hidden' unless @presentation['current_preset'] %>><%= escape(preset_badge_text) %></span><p class="muted">Presets and environment variables</p></header>
|
|
31
|
+
<div class="workspace-settings">
|
|
32
32
|
<form method="post" action="/workspaces/<%= @id %>/environment" id="environment-form" class="stack" data-environment-form data-environment-presets="<%= escape(environment_presets_json) %>">
|
|
33
33
|
<fieldset class="env-fieldset" data-environment-fieldset <%= 'disabled' if @presentation['busy'] %>>
|
|
34
34
|
<legend class="env-legend">Workspace settings</legend>
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
<p class="muted" id="environment-status" role="status" aria-live="polite"></p>
|
|
67
67
|
</form>
|
|
68
68
|
</div>
|
|
69
|
-
</
|
|
70
|
-
<
|
|
71
|
-
<
|
|
69
|
+
</section>
|
|
70
|
+
<section class="status-panel" id="technical-section" data-section="technical" aria-labelledby="technical-title" <%= 'hidden' unless section?('technical') %>>
|
|
71
|
+
<header class="section-heading"><h2 id="technical-title">Technical details</h2><p class="muted">Setup steps, logs and workspace controls</p></header>
|
|
72
72
|
<div class="technical stack stack--loose">
|
|
73
73
|
<div class="workspace-actions-group stack stack--snug" data-section="actions" aria-labelledby="actions-heading" <%= 'hidden' unless section?('actions') %>>
|
|
74
74
|
<h3 id="actions-heading">Workspace controls</h3>
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
<section class="log-panel" data-section="log" aria-labelledby="log-title" <%= 'hidden' unless section?('log') %>>
|
|
81
81
|
<div class="log-header"><h3 id="log-title">Live <%= field('log_source') == 'backend' ? 'backend' : 'setup' %> log</h3><time id="updated"><%= field('updated_at') %></time></div>
|
|
82
82
|
<nav class="log-switch" aria-label="Log source">
|
|
83
|
-
<a href="?log=setup" data-log-source="setup" <%= 'aria-current="page"' if @snapshot['log_source'] != 'backend' %>>Setup steps</a>
|
|
84
|
-
<a href="?log=backend" id="backend-tab" data-log-source="backend" <%= 'aria-current="page"' if @snapshot['log_source'] == 'backend' %> <%= 'hidden' unless backend_tab? %>>Web server log</a>
|
|
83
|
+
<a href="?log=setup#log-title" data-log-source="setup" <%= 'aria-current="page"' if @snapshot['log_source'] != 'backend' %>>Setup steps</a>
|
|
84
|
+
<a href="?log=backend#log-title" id="backend-tab" data-log-source="backend" <%= 'aria-current="page"' if @snapshot['log_source'] == 'backend' %> <%= 'hidden' unless backend_tab? %>>Web server log</a>
|
|
85
85
|
</nav>
|
|
86
86
|
<div id="steps" class="step-logs" data-step-logs <%= 'hidden' if @snapshot['log_source'] == 'backend' %>>
|
|
87
87
|
<% @presentation['steps'].each do |step| %><%= step_accordion(step) %><% end %>
|
|
@@ -107,6 +107,6 @@
|
|
|
107
107
|
</form>
|
|
108
108
|
</section>
|
|
109
109
|
</div>
|
|
110
|
-
</
|
|
110
|
+
</section>
|
|
111
111
|
<noscript><a href="/workspaces/<%= @id %>/settings">Refresh status</a></noscript>
|
|
112
112
|
</main>
|
|
@@ -135,13 +135,31 @@ module Workspaces
|
|
|
135
135
|
<table class="workspace-table">
|
|
136
136
|
<thead><tr><th scope="col">Workspace</th><th scope="col">Status</th><th scope="col" class="workspace-table__date">Created (UTC)</th><th scope="col" class="workspace-table__date">Last startup (UTC)</th></tr></thead>
|
|
137
137
|
<tbody>
|
|
138
|
-
#{workspaces.map { |workspace| inventory_row(workspace) }.join}
|
|
138
|
+
#{by_last_startup(workspaces).map { |workspace| inventory_row(workspace) }.join}
|
|
139
139
|
</tbody>
|
|
140
140
|
</table>
|
|
141
141
|
</div>
|
|
142
142
|
HTML
|
|
143
143
|
end
|
|
144
144
|
|
|
145
|
+
# Most recently started first; workspaces that have never started (or whose start time cannot be read)
|
|
146
|
+
# come last. Moments are compared as moments, not as text, so a different UTC offset or fraction cannot
|
|
147
|
+
# misorder rows, and ties fall back to the id so the list never shuffles between loads.
|
|
148
|
+
def by_last_startup(workspaces)
|
|
149
|
+
workspaces.sort_by do |workspace|
|
|
150
|
+
started = startup_moment(workspace['started_at'])
|
|
151
|
+
[started ? 0 : 1, started ? -started.to_r : 0, workspace['id'].to_s]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def startup_moment(value)
|
|
156
|
+
return nil if value.nil? || value.to_s.empty?
|
|
157
|
+
|
|
158
|
+
Time.iso8601(value.to_s)
|
|
159
|
+
rescue ArgumentError
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
145
163
|
# "8 managed worktrees · none running", counted from the data on the page.
|
|
146
164
|
def inventory_summary(workspaces)
|
|
147
165
|
total = workspaces.length
|
|
@@ -11,12 +11,12 @@ module Workspaces
|
|
|
11
11
|
'resume' => 'Retry failed step', 'keep-awake' => 'Keep awake',
|
|
12
12
|
'allow-sleep' => 'Allow sleep' }.freeze
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
# most one primary action. `environment` (presets and variables) is the area for
|
|
16
|
-
# deliberately changing something. `technical`
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# not a section of their own: each
|
|
14
|
+
# The sections of the settings page, from friendly to technical. `lead` is the headline, one
|
|
15
|
+
# sentence and at most one primary action. `environment` (presets and variables) is the area for
|
|
16
|
+
# deliberately changing something. `technical` holds setup steps, logs, workspace details,
|
|
17
|
+
# lifecycle controls and deletion. All of them are plain sections, always open when shown: the
|
|
18
|
+
# card at /workspaces/ID is what keeps the details out of the way, so this page has nothing to
|
|
19
|
+
# fold. Setup steps are not a section of their own: each has its own log row inside `log`.
|
|
20
20
|
SECTIONS = %w(lead actions environment technical log details danger).freeze
|
|
21
21
|
TECHNICAL = %w(technical actions log details danger).freeze
|
|
22
22
|
|
|
@@ -32,66 +32,57 @@ module Workspaces
|
|
|
32
32
|
# `environment_submit` labels the single environment-form button, which saves the
|
|
33
33
|
# settings and (re)starts the workspace, so a brand new workspace needs only that.
|
|
34
34
|
#
|
|
35
|
-
# Per state: `headline` and `summary` are plain language
|
|
36
|
-
# `
|
|
37
|
-
# technical section so the cause is visible without hunting for it.
|
|
35
|
+
# Per state: `headline` and `summary` are plain language, `sections` says which sections are
|
|
36
|
+
# shown, and `environment_submit` / `environment_title` word the settings form.
|
|
38
37
|
STATES = {
|
|
39
38
|
'idle' => {
|
|
40
39
|
sections: %w(lead environment),
|
|
41
40
|
headline: "Let's get your workspace running",
|
|
42
41
|
summary: 'Pick the version of the site you want to preview, then start. Setup usually takes a few minutes.',
|
|
43
|
-
environment_submit: 'Start workspace', environment_title: 'Choose what to preview'
|
|
44
|
-
open_environment: true, open_technical: false
|
|
42
|
+
environment_submit: 'Start workspace', environment_title: 'Choose what to preview'
|
|
45
43
|
},
|
|
46
44
|
'busy' => {
|
|
47
45
|
sections: %w(lead technical log),
|
|
48
46
|
headline: 'Setting things up…',
|
|
49
47
|
summary: 'This page keeps itself up to date. Open the preview once it is ready.',
|
|
50
|
-
environment_submit: 'Save & restart', environment_title: 'Change settings'
|
|
51
|
-
open_environment: false, open_technical: true
|
|
48
|
+
environment_submit: 'Save & restart', environment_title: 'Change settings'
|
|
52
49
|
},
|
|
53
50
|
'ready' => {
|
|
54
51
|
sections: %w(lead environment) + TECHNICAL,
|
|
55
52
|
headline: 'Your workspace is ready',
|
|
56
53
|
summary: 'Everything is set up and running. Open it to see your changes.',
|
|
57
|
-
environment_submit: 'Save & restart', environment_title: 'Change settings'
|
|
58
|
-
open_environment: false, open_technical: false
|
|
54
|
+
environment_submit: 'Save & restart', environment_title: 'Change settings'
|
|
59
55
|
},
|
|
60
56
|
'sleeping' => {
|
|
61
57
|
sections: %w(lead environment) + TECHNICAL,
|
|
62
58
|
headline: 'Your workspace is asleep',
|
|
63
59
|
summary: 'It went quiet, so its server was stopped to free up resources. Your files and data are kept. ' \
|
|
64
60
|
'Opening the preview wakes it in a few seconds.',
|
|
65
|
-
environment_submit: 'Save & start', environment_title: 'Change settings'
|
|
66
|
-
open_environment: false, open_technical: false
|
|
61
|
+
environment_submit: 'Save & start', environment_title: 'Change settings'
|
|
67
62
|
},
|
|
68
63
|
'stopped' => {
|
|
69
64
|
sections: %w(lead environment) + TECHNICAL,
|
|
70
65
|
headline: 'Your workspace is paused',
|
|
71
66
|
summary: 'Start it again to pick up where you left off.',
|
|
72
|
-
environment_submit: 'Save & start', environment_title: 'Change settings'
|
|
73
|
-
open_environment: false, open_technical: false
|
|
67
|
+
environment_submit: 'Save & start', environment_title: 'Change settings'
|
|
74
68
|
},
|
|
75
69
|
'error' => {
|
|
76
70
|
sections: %w(lead environment) + TECHNICAL,
|
|
77
71
|
headline: "Something didn't go to plan",
|
|
78
72
|
summary: 'Setup stopped before it finished. You can try again, or change settings first.',
|
|
79
|
-
environment_submit: 'Save & retry', environment_title: 'Change settings'
|
|
80
|
-
open_environment: false, open_technical: true
|
|
73
|
+
environment_submit: 'Save & retry', environment_title: 'Change settings'
|
|
81
74
|
},
|
|
82
75
|
'interrupted' => {
|
|
83
76
|
sections: %w(lead environment) + TECHNICAL,
|
|
84
77
|
headline: 'Setup was interrupted',
|
|
85
78
|
summary: 'It stopped before it finished. Try again to pick up from the start.',
|
|
86
|
-
environment_submit: 'Save & retry', environment_title: 'Change settings'
|
|
87
|
-
open_environment: false, open_technical: true
|
|
79
|
+
environment_submit: 'Save & retry', environment_title: 'Change settings'
|
|
88
80
|
},
|
|
89
81
|
'unknown' => {
|
|
90
82
|
sections: %w(lead technical log details),
|
|
91
83
|
headline: 'Workspace status unknown',
|
|
92
84
|
summary: 'Open the technical details to see what the workspace is doing.',
|
|
93
|
-
environment_submit: 'Save & restart', environment_title: 'Change settings'
|
|
94
|
-
open_environment: false, open_technical: true
|
|
85
|
+
environment_submit: 'Save & restart', environment_title: 'Change settings'
|
|
95
86
|
}
|
|
96
87
|
}.freeze
|
|
97
88
|
|
|
@@ -107,8 +98,7 @@ module Workspaces
|
|
|
107
98
|
'lead_action' => lead_action, 'total' => total, 'progress' => progress,
|
|
108
99
|
'current_preset' => current_preset, 'sleep' => sleep_info,
|
|
109
100
|
'environment_title' => definition[:environment_title],
|
|
110
|
-
'environment_submit' => definition[:environment_submit]
|
|
111
|
-
'open' => { 'environment' => definition[:open_environment], 'technical' => definition[:open_technical] } }
|
|
101
|
+
'environment_submit' => definition[:environment_submit] }
|
|
112
102
|
end
|
|
113
103
|
|
|
114
104
|
private
|
data/lib/workspaces/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: workspaces
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.20
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Workspaces contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|