@bongos/core 1.21.96 → 1.21.98
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.
- package/.bongos-core.json +62 -47
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0152-metic-task-abandonment.md +2 -0
- package/docs/api/openapi.json +52 -5
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +3 -0
- package/docs/copy-inventory.md +395 -393
- package/docs/copy-registry.json +486 -468
- package/docs/design/project-startup-direction.md +13 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +260 -260
- package/migrations/core_274_parked_backlog_stays_parked.sql +140 -0
- package/modules/hall-ui/public/studio-lib.js +22 -0
- package/modules/hall-ui/public/studio.css +138 -7
- package/modules/hall-ui/public/studio.html +10 -4
- package/modules/hall-ui/public/studio.js +39 -1
- package/modules/hall-ui/records/studio.md +16 -0
- package/modules/lifecycle/db-ship.js +7 -2
- package/modules/lifecycle/db-tasks.js +25 -0
- package/modules/lifecycle/routes/tasks.js +5 -5
- package/modules/provisioning/github-setup.js +73 -4
- package/modules/provisioning/routes/github-setup.js +43 -1
- package/modules/provisioning/routes/provisioning.js +8 -4
- package/modules/public-landing/public/projects.html +185 -30
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/provision-github-setup.js +29 -9
- package/scripts/gds/provision.js +20 -9
- package/src/module-api.js +1 -1
- package/tests/abandon_task.mjs +1 -1
- package/tests/github_one_trip.mjs +4 -4
- package/tests/github_org_account.mjs +3 -2
- package/tests/github_pick_up.mjs +366 -0
- package/tests/hall_studio_home.mjs +140 -5
- package/tests/parked_backlog_stays_parked.mjs +182 -0
- package/tests/projects_hub_pre_uat.mjs +3 -2
- package/tests/promotion_triggers_skip_cascade.mjs +5 -3
- package/tests/provisioning_github_app_routes.mjs +73 -0
- package/tests/wizard_github_feedback.mjs +95 -29
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
-- core_274 a task a PERSON parked in backlog is never auto-promoted out of it
|
|
2
|
+
-- Applied under task 1004592.
|
|
3
|
+
--
|
|
4
|
+
-- WHY. Two routes put a task into backlog on purpose:
|
|
5
|
+
--
|
|
6
|
+
-- POST /tasks/:id/promote on an ABANDONED task restoreTask (ADR 0152) -- the
|
|
7
|
+
-- task "comes back un-workable and needs a second, explicit promote"
|
|
8
|
+
-- POST /tasks/:id/demote a person took a ready task
|
|
9
|
+
-- back off the board
|
|
10
|
+
--
|
|
11
|
+
-- Neither left anything behind in the row. The promotion sweep
|
|
12
|
+
-- (modules/lifecycle/db-tasks.js PROMOTABLE_BACKLOG_WHERE) promotes every
|
|
13
|
+
-- backlog row with no unmet dependency and no open blocker, so both were undone
|
|
14
|
+
-- on the next reconciler tick. OBSERVED on task 1003397 (2026-10-06): restored,
|
|
15
|
+
-- promoted by the sweep at 19:58:34Z with nobody promoting it; demoted at about
|
|
16
|
+
-- 20:00Z, back at ready at 20:03:34Z, one tick later.
|
|
17
|
+
--
|
|
18
|
+
-- SHAPE. A durable marker, tasks.parked_at + tasks.parked_by, written by the
|
|
19
|
+
-- two routes and honoured by every promoter:
|
|
20
|
+
--
|
|
21
|
+
-- (1) the sweep: `AND t.parked_at IS NULL` in PROMOTABLE_BACKLOG_WHERE
|
|
22
|
+
-- (2) the three trigger functions below, for the same reason core_250 gives
|
|
23
|
+
-- for its cascade clause: a parked task that HAS dependencies would
|
|
24
|
+
-- otherwise be promoted the moment one of them ships. Fixing only the
|
|
25
|
+
-- sweep would leave that path open.
|
|
26
|
+
--
|
|
27
|
+
-- The marker is cleared by a BEFORE UPDATE trigger whenever status leaves
|
|
28
|
+
-- 'backlog'. So a person's promote needs no change at all, and a stale marker
|
|
29
|
+
-- can never strand a task that a person later sends back to backlog by some
|
|
30
|
+
-- other route: once it leaves backlog, it is no longer parked.
|
|
31
|
+
--
|
|
32
|
+
-- NOT task_is_fully_unblocked(). Same reason as core_250: that predicate is
|
|
33
|
+
-- shared with the claim gate and answers "are the dependencies done", not "has
|
|
34
|
+
-- a person said wait".
|
|
35
|
+
--
|
|
36
|
+
-- Forward-safe: two nullable columns with no default, CREATE OR REPLACE only.
|
|
37
|
+
-- An older release never writes parked_at, so on a rolled-back pin nothing is
|
|
38
|
+
-- parked and promotion behaves exactly as before. The three function bodies are
|
|
39
|
+
-- core_250's verbatim apart from the one added line each.
|
|
40
|
+
|
|
41
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS parked_at timestamptz;
|
|
42
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS parked_by bigint REFERENCES builders(id) ON DELETE SET NULL;
|
|
43
|
+
|
|
44
|
+
-- Leaving backlog un-parks. Runs only when status actually changes, so a
|
|
45
|
+
-- write that parks a task (status -> 'backlog' plus parked_at) keeps it.
|
|
46
|
+
CREATE OR REPLACE FUNCTION clear_parked_on_leave_backlog()
|
|
47
|
+
RETURNS TRIGGER AS $$
|
|
48
|
+
BEGIN
|
|
49
|
+
IF NEW.status IS DISTINCT FROM 'backlog' THEN
|
|
50
|
+
NEW.parked_at := NULL;
|
|
51
|
+
NEW.parked_by := NULL;
|
|
52
|
+
END IF;
|
|
53
|
+
RETURN NEW;
|
|
54
|
+
END;
|
|
55
|
+
$$ LANGUAGE plpgsql;
|
|
56
|
+
|
|
57
|
+
DROP TRIGGER IF EXISTS trg_clear_parked_on_leave_backlog ON tasks;
|
|
58
|
+
CREATE TRIGGER trg_clear_parked_on_leave_backlog
|
|
59
|
+
BEFORE UPDATE OF status ON tasks
|
|
60
|
+
FOR EACH ROW
|
|
61
|
+
WHEN (OLD.status IS DISTINCT FROM NEW.status)
|
|
62
|
+
EXECUTE FUNCTION clear_parked_on_leave_backlog();
|
|
63
|
+
|
|
64
|
+
-- (a) a task shipped
|
|
65
|
+
CREATE OR REPLACE FUNCTION promote_dependents_on_ship()
|
|
66
|
+
RETURNS TRIGGER AS $$
|
|
67
|
+
BEGIN
|
|
68
|
+
IF NEW.status = 'shipped' AND (OLD.status IS DISTINCT FROM 'shipped') THEN
|
|
69
|
+
UPDATE tasks t
|
|
70
|
+
SET status = 'ready', promoted_at = COALESCE(t.promoted_at, now()), updated_at = now()
|
|
71
|
+
WHERE t.status = 'backlog'
|
|
72
|
+
AND t.kind <> 'spike'
|
|
73
|
+
AND t.source IS DISTINCT FROM 'cascade'
|
|
74
|
+
AND t.parked_at IS NULL
|
|
75
|
+
AND EXISTS (
|
|
76
|
+
SELECT 1 FROM dependencies d
|
|
77
|
+
WHERE d.to_kind = 'task' AND d.to_id = NEW.id
|
|
78
|
+
AND ( (d.from_kind = 'task' AND d.from_id = t.id)
|
|
79
|
+
OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
|
|
80
|
+
OR (d.from_kind = 'criterion' AND d.from_id IN (
|
|
81
|
+
SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)) )
|
|
82
|
+
)
|
|
83
|
+
AND task_is_fully_unblocked(t.id);
|
|
84
|
+
END IF;
|
|
85
|
+
RETURN NEW;
|
|
86
|
+
END;
|
|
87
|
+
$$ LANGUAGE plpgsql;
|
|
88
|
+
|
|
89
|
+
-- (b) a criterion satisfied
|
|
90
|
+
CREATE OR REPLACE FUNCTION promote_dependents_on_criterion_satisfy()
|
|
91
|
+
RETURNS TRIGGER AS $$
|
|
92
|
+
BEGIN
|
|
93
|
+
IF NEW.satisfied = true AND (OLD.satisfied IS DISTINCT FROM true) THEN
|
|
94
|
+
UPDATE tasks t
|
|
95
|
+
SET status = 'ready', promoted_at = COALESCE(t.promoted_at, now()), updated_at = now()
|
|
96
|
+
WHERE t.status = 'backlog'
|
|
97
|
+
AND t.kind <> 'spike'
|
|
98
|
+
AND t.source IS DISTINCT FROM 'cascade'
|
|
99
|
+
AND t.parked_at IS NULL
|
|
100
|
+
AND EXISTS (
|
|
101
|
+
SELECT 1 FROM dependencies d
|
|
102
|
+
WHERE d.to_kind = 'criterion' AND d.to_id = NEW.id
|
|
103
|
+
AND ( (d.from_kind = 'task' AND d.from_id = t.id)
|
|
104
|
+
OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
|
|
105
|
+
OR (d.from_kind = 'criterion' AND d.from_id IN (
|
|
106
|
+
SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)) )
|
|
107
|
+
)
|
|
108
|
+
AND task_is_fully_unblocked(t.id);
|
|
109
|
+
END IF;
|
|
110
|
+
RETURN NEW;
|
|
111
|
+
END;
|
|
112
|
+
$$ LANGUAGE plpgsql;
|
|
113
|
+
|
|
114
|
+
-- (c) a goal achieved
|
|
115
|
+
CREATE OR REPLACE FUNCTION promote_dependents_on_goal_achieve()
|
|
116
|
+
RETURNS TRIGGER AS $$
|
|
117
|
+
BEGIN
|
|
118
|
+
IF NEW.status = 'achieved' AND (OLD.status IS DISTINCT FROM 'achieved') THEN
|
|
119
|
+
UPDATE tasks t
|
|
120
|
+
SET status = 'ready', promoted_at = COALESCE(t.promoted_at, now()), updated_at = now()
|
|
121
|
+
WHERE t.status = 'backlog'
|
|
122
|
+
AND t.kind <> 'spike'
|
|
123
|
+
AND t.source IS DISTINCT FROM 'cascade'
|
|
124
|
+
AND t.parked_at IS NULL
|
|
125
|
+
AND EXISTS (
|
|
126
|
+
SELECT 1 FROM dependencies d
|
|
127
|
+
WHERE d.to_kind = 'goal' AND d.to_id = NEW.id
|
|
128
|
+
AND ( (d.from_kind = 'task' AND d.from_id = t.id)
|
|
129
|
+
OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
|
|
130
|
+
OR (d.from_kind = 'criterion' AND d.from_id IN (
|
|
131
|
+
SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)) )
|
|
132
|
+
)
|
|
133
|
+
AND task_is_fully_unblocked(t.id);
|
|
134
|
+
END IF;
|
|
135
|
+
RETURN NEW;
|
|
136
|
+
END;
|
|
137
|
+
$$ LANGUAGE plpgsql;
|
|
138
|
+
|
|
139
|
+
INSERT INTO schema_migrations (version) VALUES ('core_274_parked_backlog_stays_parked')
|
|
140
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
// matchLine(text, lines) which line of the studio's own reading a word on
|
|
20
20
|
// the page is, so an in-place edit names its key
|
|
21
21
|
// claimWords(code, detail) a refused claim, in words
|
|
22
|
+
// skyAt(date) 'day' 07:00-18:59 on the viewer's clock, else 'night'
|
|
23
|
+
// dayPart(date) 'morning' / 'afternoon' in the day room, 'evening' at night
|
|
24
|
+
// clockGreets(served) whether the served greeting is still the stock one,
|
|
25
|
+
// so the clock may choose (an artist's tweak wins)
|
|
22
26
|
//
|
|
23
27
|
// Node-require-safe (the tweak-editor-lib.js pattern): assignment is the only
|
|
24
28
|
// eval-time side effect, and only when a window exists.
|
|
@@ -198,9 +202,27 @@
|
|
|
198
202
|
return 'That page could not be taken just now. Nothing changed.';
|
|
199
203
|
}
|
|
200
204
|
|
|
205
|
+
// The room's sky and the greeting's time of day (task 1004654; the mockup
|
|
206
|
+
// docs/design/mocks/day-night-rooms). Day from 07:00 to 18:59 on the viewer's
|
|
207
|
+
// own clock, night otherwise: the creation page's rule (projects.html skyAt),
|
|
208
|
+
// written again here because hall-ui does not import the landing's code. The
|
|
209
|
+
// greeting is "morning" before noon in the day room, "afternoon" after it,
|
|
210
|
+
// and "evening" whenever the room is at night.
|
|
211
|
+
function skyAt(d) { const h = d.getHours(); return h >= 7 && h < 19 ? 'day' : 'night'; }
|
|
212
|
+
function dayPart(d) {
|
|
213
|
+
if (skyAt(d) === 'night') return 'evening';
|
|
214
|
+
return d.getHours() < 12 ? 'morning' : 'afternoon';
|
|
215
|
+
}
|
|
216
|
+
// The greeting the page is served with. Only while the page still says
|
|
217
|
+
// exactly this does the clock choose the greeting: once an artist's tweak of
|
|
218
|
+
// the studio page has rewritten it, their words show at every hour.
|
|
219
|
+
const STOCK_GREETING = 'Good evening.';
|
|
220
|
+
function clockGreets(served) { return tidy(served) === STOCK_GREETING; }
|
|
221
|
+
|
|
201
222
|
const api = {
|
|
202
223
|
STUDIO_PAGE, PAGE_ID_RE, SURFACE_WORDS, surfaceWord, titleOf, numberWords, tallyParts, resumeDetail,
|
|
203
224
|
subParts, flowDetail, statusDetail, apexHost, landingHost, surfaceRows, askedLine, pickLabel, matchLine, claimWords,
|
|
225
|
+
skyAt, dayPart, STOCK_GREETING, clockGreets,
|
|
204
226
|
};
|
|
205
227
|
if (typeof window !== 'undefined') window.OTBStudioLib = api;
|
|
206
228
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
@@ -17,6 +17,15 @@
|
|
|
17
17
|
shell's full-screen button while it floats over the room. The hall chrome
|
|
18
18
|
around the stage keeps its own light and dark.
|
|
19
19
|
|
|
20
|
+
ONE ROOM, TWO SKIES (task 1004654). By the viewer's own clock the room is
|
|
21
|
+
the night room or the day room of docs/design/mocks/day-night-rooms (day
|
|
22
|
+
from 07:00 to 18:59, re-checked each minute; studio.js sets data-sky on the
|
|
23
|
+
body). The day palette is the mockup's [data-sky="day"] block, declared just
|
|
24
|
+
below the night tokens it replaces; the same drawing, with clouds and a sun
|
|
25
|
+
in the window, lighter towers with unlit panes, a warm wall and floor, the
|
|
26
|
+
lamp off and a patch of sun on the floor. The glass panels over the room
|
|
27
|
+
follow its sky; the hall chrome still keeps its own light and dark.
|
|
28
|
+
|
|
20
29
|
The second half is the older reviews' garden (R06 of goal 1000074, task
|
|
21
30
|
1003117), which still answers the reviews filed before Tweak Mode (TW14) in
|
|
22
31
|
its own quiet dialog, in the hall's own tokens as it always was. */
|
|
@@ -52,6 +61,9 @@ body[data-page="studio"] {
|
|
|
52
61
|
--st-accent: #fb9569;
|
|
53
62
|
--st-on-accent: #000000;
|
|
54
63
|
--st-link: #fcbfa4;
|
|
64
|
+
--st-pane: #fb9569;
|
|
65
|
+
--st-credits: #fb9569;
|
|
66
|
+
--st-focus: #fb9569;
|
|
55
67
|
--st-resume: rgba(251, 149, 105, .16);
|
|
56
68
|
--st-resume-edge: rgba(251, 149, 105, .55);
|
|
57
69
|
--st-word-hover: rgba(255, 255, 255, .28);
|
|
@@ -69,6 +81,86 @@ body[data-page="studio"] {
|
|
|
69
81
|
--st-mono: var(--font-mono, "JetBrains Mono", ui-monospace, Consolas, monospace);
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
/* The day room: the mockup's [data-sky="day"] tokens, on the same names. Three
|
|
85
|
+
inks are set darker than the mockup's so they hold AA on the glass over a
|
|
86
|
+
light wall (records/studio.md): the link, and the credits figure and focus
|
|
87
|
+
ring, which by night are the accent. */
|
|
88
|
+
body[data-page="studio"][data-sky="day"] {
|
|
89
|
+
--st-room: #efe7dc;
|
|
90
|
+
--st-frame: #d6c9b7;
|
|
91
|
+
--st-night: #bfdaf0;
|
|
92
|
+
--st-moon: #ffd46e;
|
|
93
|
+
--st-moon-halo: rgba(255, 196, 90, .5);
|
|
94
|
+
--st-star: #ffffff;
|
|
95
|
+
--st-tower-1: #9db2c7;
|
|
96
|
+
--st-tower-2: #8ea4bb;
|
|
97
|
+
--st-tower-3: #a8bccf;
|
|
98
|
+
--st-pane: #e9f1f8;
|
|
99
|
+
--st-floor: #e2d5c3;
|
|
100
|
+
--st-floor-edge: rgba(60, 40, 20, .08);
|
|
101
|
+
--st-glow: rgba(255, 222, 160, .55);
|
|
102
|
+
--st-glow-out: rgba(255, 222, 160, 0);
|
|
103
|
+
--st-stem: #c4b4a0;
|
|
104
|
+
--st-shade: #d5c5af;
|
|
105
|
+
--st-mug: #cbbba8;
|
|
106
|
+
--st-leaf-1: #6f9a6a;
|
|
107
|
+
--st-leaf-2: #5d8a59;
|
|
108
|
+
--st-ink: #1b1a1d;
|
|
109
|
+
--st-ink-sub: #3f3e44;
|
|
110
|
+
--st-ink-soft: #4d4c52;
|
|
111
|
+
--st-ink-faint: #5a595f;
|
|
112
|
+
--st-ink-dim: #66656b;
|
|
113
|
+
--st-ink-hint: #66656b;
|
|
114
|
+
--st-glass: rgba(255, 255, 255, .52);
|
|
115
|
+
--st-glass-edge: rgba(60, 40, 20, .16);
|
|
116
|
+
--st-rule: rgba(60, 40, 20, .14);
|
|
117
|
+
--st-link: #96410f;
|
|
118
|
+
--st-credits: #96410f;
|
|
119
|
+
--st-focus: #96410f;
|
|
120
|
+
--st-resume: rgba(251, 149, 105, .22);
|
|
121
|
+
--st-resume-edge: rgba(190, 85, 35, .6);
|
|
122
|
+
--st-word-hover: rgba(60, 40, 20, .28);
|
|
123
|
+
--st-word-ring: rgba(190, 85, 35, .6);
|
|
124
|
+
--st-panel: rgba(255, 255, 255, .75);
|
|
125
|
+
--st-panel-edge: rgba(60, 40, 20, .16);
|
|
126
|
+
--st-panel-rim: rgba(255, 255, 255, .9);
|
|
127
|
+
--st-panel-drop: rgba(60, 40, 20, .18);
|
|
128
|
+
--st-close: rgba(60, 40, 20, .08);
|
|
129
|
+
--st-chip-edge: rgba(60, 40, 20, .2);
|
|
130
|
+
--st-track: rgba(60, 40, 20, .12);
|
|
131
|
+
--st-danger: #bb1f1f;
|
|
132
|
+
}
|
|
133
|
+
/* The panels' scrim is the day wall's own tint, whatever the hall's theme, set
|
|
134
|
+
on the overlay so it outranks the theme's .42 and the phone's .88. */
|
|
135
|
+
body[data-page="studio"][data-sky="day"] .st-overlay {
|
|
136
|
+
--st-scrim: rgba(239, 231, 220, .9);
|
|
137
|
+
color-scheme: light;
|
|
138
|
+
}
|
|
139
|
+
body[data-page="studio"][data-sky="day"] .studio { color-scheme: light; }
|
|
140
|
+
/* The Approval queue (approval-queue.css) is night glass on its own root; over
|
|
141
|
+
the day room it wears the same day glass as the studio's own panels. Its
|
|
142
|
+
rendered page stays on its paper frame, and its accent button stays the
|
|
143
|
+
accent, which carries black words at AA in either sky. */
|
|
144
|
+
body[data-page="studio"][data-sky="day"] .aq-overlay {
|
|
145
|
+
color-scheme: light;
|
|
146
|
+
--aq-scrim: rgba(239, 231, 220, .9);
|
|
147
|
+
--aq-glass: rgba(255, 255, 255, .75);
|
|
148
|
+
--aq-glass-edge: rgba(60, 40, 20, .16);
|
|
149
|
+
--aq-glass-rim: rgba(255, 255, 255, .9);
|
|
150
|
+
--aq-drop: rgba(60, 40, 20, .18);
|
|
151
|
+
--aq-ink: #1b1a1d;
|
|
152
|
+
--aq-ink-soft: #4d4c52;
|
|
153
|
+
--aq-ink-faint: #5a595f;
|
|
154
|
+
--aq-ink-dim: #66656b;
|
|
155
|
+
--aq-well: rgba(60, 40, 20, .07);
|
|
156
|
+
--aq-pill-on: rgba(255, 255, 255, .9);
|
|
157
|
+
--aq-close: rgba(60, 40, 20, .08);
|
|
158
|
+
--aq-ghost-edge: rgba(60, 40, 20, .3);
|
|
159
|
+
--aq-link: #96410f;
|
|
160
|
+
--aq-rule: rgba(60, 40, 20, .12);
|
|
161
|
+
}
|
|
162
|
+
body[data-page="studio"][data-sky="day"] .aq-overlay :focus-visible { outline-color: var(--st-focus); }
|
|
163
|
+
|
|
72
164
|
/* The stage fills the content box: the hall's content column is a reading
|
|
73
165
|
width, and the artboard is the whole window. */
|
|
74
166
|
body[data-page="studio"] .app-content { max-width: none; padding: 0; }
|
|
@@ -87,7 +179,7 @@ body[data-page="studio"] .app-foot { display: none; }
|
|
|
87
179
|
}
|
|
88
180
|
html[data-fullscreen="on"] .studio { min-height: 100vh; }
|
|
89
181
|
.studio a { color: var(--st-link); }
|
|
90
|
-
.studio a:hover { color: var(--st-
|
|
182
|
+
.studio a:hover { color: var(--st-focus); }
|
|
91
183
|
.studio button { font-family: inherit; }
|
|
92
184
|
|
|
93
185
|
/* In full screen the shell's one button floats over the night room in either
|
|
@@ -140,13 +232,23 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
|
|
|
140
232
|
.st-star--3 { left: 260px; top: 120px; width: 3px; height: 3px; opacity: .7; }
|
|
141
233
|
.st-star--4 { left: 470px; top: 170px; width: 2px; height: 2px; }
|
|
142
234
|
.st-star--5 { left: 130px; top: 190px; width: 2px; height: 2px; opacity: .6; }
|
|
235
|
+
/* The clouds belong to the day room; at night they are not there. */
|
|
236
|
+
.st-cloud { border-radius: 40px; background: var(--st-star); opacity: 0; }
|
|
237
|
+
.st-cloud::before,
|
|
238
|
+
.st-cloud::after { content: ""; position: absolute; background: inherit; border-radius: 50%; }
|
|
239
|
+
.st-cloud--1 { left: 44px; top: 76px; width: 118px; height: 28px; }
|
|
240
|
+
.st-cloud--1::before { left: 24px; top: -17px; width: 47px; height: 41px; }
|
|
241
|
+
.st-cloud--1::after { left: 58px; top: -11px; width: 37px; height: 32px; }
|
|
242
|
+
.st-cloud--2 { left: 248px; top: 150px; width: 86px; height: 22px; }
|
|
243
|
+
.st-cloud--2::before { left: 17px; top: -13px; width: 34px; height: 30px; }
|
|
244
|
+
.st-cloud--2::after { left: 43px; top: -9px; width: 28px; height: 24px; }
|
|
143
245
|
.st-tower { bottom: 0; }
|
|
144
246
|
.st-tower--1 { left: 0; width: 120px; height: 230px; background: var(--st-tower-1); }
|
|
145
247
|
.st-tower--2 { left: 110px; width: 90px; height: 310px; background: var(--st-tower-2); }
|
|
146
248
|
.st-tower--3 { left: 200px; width: 140px; height: 200px; background: var(--st-tower-3); }
|
|
147
249
|
.st-tower--4 { left: 330px; width: 80px; height: 270px; background: var(--st-tower-2); }
|
|
148
250
|
.st-tower--5 { left: 400px; width: 140px; height: 180px; background: var(--st-tower-1); }
|
|
149
|
-
.st-light { width: 6px; height: 8px; background: var(--st-
|
|
251
|
+
.st-light { width: 6px; height: 8px; background: var(--st-pane); }
|
|
150
252
|
.st-light--1 { left: 136px; bottom: 250px; opacity: .75; }
|
|
151
253
|
.st-light--2 { left: 160px; bottom: 210px; opacity: .55; }
|
|
152
254
|
.st-light--3 { left: 30px; bottom: 150px; opacity: .6; }
|
|
@@ -161,6 +263,14 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
|
|
|
161
263
|
background: var(--st-floor);
|
|
162
264
|
border-top: 1px solid var(--st-floor-edge);
|
|
163
265
|
}
|
|
266
|
+
/* By day the lamp is off and a patch of sun lies on the floor instead. */
|
|
267
|
+
.st-sunpatch {
|
|
268
|
+
left: calc(50% + 140px); top: calc(712px + var(--room-drop)); width: 520px; height: 120px;
|
|
269
|
+
opacity: 0;
|
|
270
|
+
background: var(--st-glow);
|
|
271
|
+
transform: skewX(-28deg);
|
|
272
|
+
filter: blur(10px);
|
|
273
|
+
}
|
|
164
274
|
.st-glow {
|
|
165
275
|
left: calc(50% + 440px); top: calc(470px + var(--room-drop)); width: 300px; height: 300px;
|
|
166
276
|
border-radius: 50%;
|
|
@@ -173,6 +283,26 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
|
|
|
173
283
|
.st-leaf--1 { left: calc(50% + 180px); top: calc(610px + var(--room-drop)); width: 64px; height: 90px; background: var(--st-leaf-1); }
|
|
174
284
|
.st-leaf--2 { left: calc(50% + 220px); top: calc(590px + var(--room-drop)); width: 50px; height: 110px; background: var(--st-leaf-2); }
|
|
175
285
|
|
|
286
|
+
/* The day room: the sun rides higher and larger than the moon, the stars and
|
|
287
|
+
the lamp's glow are gone, the clouds and the patch of sun are there. */
|
|
288
|
+
body[data-sky="day"] .st-moon { left: 394px; top: 42px; width: 76px; height: 76px; box-shadow: 0 0 80px 26px var(--st-moon-halo); }
|
|
289
|
+
body[data-sky="day"] .st-star,
|
|
290
|
+
body[data-sky="day"] .st-glow { opacity: 0; }
|
|
291
|
+
body[data-sky="day"] .st-cloud { opacity: .9; }
|
|
292
|
+
body[data-sky="day"] .st-sunpatch { opacity: .7; }
|
|
293
|
+
|
|
294
|
+
/* The room's only motion: the clouds drift, and a change of sky fades over
|
|
295
|
+
.8s. Both live only without a reduced-motion preference, and style.css's
|
|
296
|
+
kill switch stops them besides. */
|
|
297
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
298
|
+
.st-cloud { animation: st-drift 60s linear infinite; }
|
|
299
|
+
.st-cloud--2 { animation-delay: -25s; }
|
|
300
|
+
.studio,
|
|
301
|
+
.st-room > *,
|
|
302
|
+
.st-window > * { transition: background-color .8s ease, opacity .8s ease, box-shadow .8s ease; }
|
|
303
|
+
@keyframes st-drift { from { transform: translateX(-30px); } to { transform: translateX(60px); } }
|
|
304
|
+
}
|
|
305
|
+
|
|
176
306
|
/* Everything the artist reads or presses sits above the room. */
|
|
177
307
|
.st-top,
|
|
178
308
|
.st-words,
|
|
@@ -206,7 +336,7 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
|
|
|
206
336
|
.st-tally[hidden] { display: none; }
|
|
207
337
|
.st-tally__item { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
|
|
208
338
|
.st-tally__n { font-size: 20px; font-weight: 300; color: var(--st-ink); font-variant-numeric: tabular-nums; }
|
|
209
|
-
.st-tally__n--credits { color: var(--st-
|
|
339
|
+
.st-tally__n--credits { color: var(--st-credits); }
|
|
210
340
|
.st-tally__label { font-size: 13px; color: var(--st-ink-soft); }
|
|
211
341
|
.st-tally__rule { width: 1px; height: 20px; background: var(--st-rule); }
|
|
212
342
|
|
|
@@ -239,7 +369,7 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
|
|
|
239
369
|
}
|
|
240
370
|
.st-words [data-words] { cursor: text; border-radius: 2px; }
|
|
241
371
|
.st-words [data-words]:hover { box-shadow: 0 1px 0 var(--st-word-hover); }
|
|
242
|
-
.st-words [data-words][contenteditable="true"]:focus { box-shadow: 0 1px 0 var(--st-
|
|
372
|
+
.st-words [data-words][contenteditable="true"]:focus { box-shadow: 0 1px 0 var(--st-focus); }
|
|
243
373
|
.st-words [data-words][contenteditable="true"]:focus-visible {
|
|
244
374
|
outline: 1px solid var(--st-word-ring);
|
|
245
375
|
outline-offset: 6px;
|
|
@@ -356,7 +486,7 @@ html[data-fullscreen="on"] body[data-page="studio"] .fullscreen-toggle {
|
|
|
356
486
|
.st-resume:focus-visible,
|
|
357
487
|
.st-action:focus-visible,
|
|
358
488
|
.st-hint:focus-visible,
|
|
359
|
-
.st-older:focus-visible { outline: 2px solid var(--st-
|
|
489
|
+
.st-older:focus-visible { outline: 2px solid var(--st-focus); outline-offset: 3px; }
|
|
360
490
|
|
|
361
491
|
/* The older reviews' door: a quiet line under the actions, not a fourth row. */
|
|
362
492
|
.st-older {
|
|
@@ -421,7 +551,7 @@ html.st-open { overflow: hidden; }
|
|
|
421
551
|
gap: 22px;
|
|
422
552
|
}
|
|
423
553
|
.st-overlay button { font-family: inherit; }
|
|
424
|
-
.st-overlay :focus-visible { outline: 2px solid var(--st-
|
|
554
|
+
.st-overlay :focus-visible { outline: 2px solid var(--st-focus); outline-offset: 2px; }
|
|
425
555
|
.st-dialog__head { display: flex; align-items: center; gap: 12px; }
|
|
426
556
|
.st-dialog__title {
|
|
427
557
|
margin: 0;
|
|
@@ -510,7 +640,7 @@ html.st-open { overflow: hidden; }
|
|
|
510
640
|
color: var(--st-ink); text-decoration: none;
|
|
511
641
|
}
|
|
512
642
|
.st-site:hover { background: var(--st-glass); }
|
|
513
|
-
.st-site:focus-visible { outline: 2px solid var(--st-
|
|
643
|
+
.st-site:focus-visible { outline: 2px solid var(--st-focus); outline-offset: 1px; }
|
|
514
644
|
.st-site__head { display: flex; align-items: baseline; gap: 4px 12px; flex-wrap: wrap; }
|
|
515
645
|
.st-site__page { font-size: 17px; font-weight: 300; }
|
|
516
646
|
.st-site__meta { font-size: 13px; color: var(--st-ink-dim); }
|
|
@@ -551,6 +681,7 @@ html.st-open { overflow: hidden; }
|
|
|
551
681
|
.st-actions { margin-top: 36px; width: 100%; }
|
|
552
682
|
.st-floor { top: auto; bottom: 0; min-height: 0; height: 96px; }
|
|
553
683
|
.st-glow { left: auto; right: -110px; top: auto; bottom: -20px; }
|
|
684
|
+
.st-sunpatch { left: auto; right: 40px; top: auto; bottom: 10px; width: 300px; height: 70px; }
|
|
554
685
|
.st-lamp-stem { left: auto; right: 44px; top: auto; bottom: 96px; height: 100px; }
|
|
555
686
|
.st-lamp-shade { left: auto; right: 0; top: auto; bottom: 170px; }
|
|
556
687
|
.st-mug { left: auto; right: 116px; top: auto; bottom: 96px; }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
15
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
16
|
-
<link rel="stylesheet" href="/builders/studio.css?v=2026-09-
|
|
16
|
+
<link rel="stylesheet" href="/builders/studio.css?v=2026-10-09-day">
|
|
17
17
|
<link rel="stylesheet" href="/builders/approval-queue.css?v=2026-09-29-aqfix">
|
|
18
18
|
</head>
|
|
19
19
|
<!-- THE STUDIO HOME (task 1003971 / BV2.TW16, goal 1000095; ADR 0341 D2, D5,
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
<!-- data-apex: the instance's own address, stamped by the server from the
|
|
41
41
|
branding pack (the landing surface's line in Artist Review Status). -->
|
|
42
42
|
<main id="hall-main" class="studio" data-apex="{{publicOrigin}}">
|
|
43
|
-
<!-- The
|
|
43
|
+
<!-- The room. Drawn, not an image: it is the artboard's own shapes. By the
|
|
44
|
+
viewer's clock it is the night room or the day room (task 1004654,
|
|
45
|
+
docs/design/mocks/day-night-rooms): studio.js sets data-sky on the
|
|
46
|
+
body, and the clouds and the patch of sun are only seen by day. -->
|
|
44
47
|
<div class="st-room" aria-hidden="true">
|
|
45
48
|
<div class="st-window">
|
|
46
49
|
<span class="st-moon"></span>
|
|
@@ -49,6 +52,8 @@
|
|
|
49
52
|
<span class="st-star st-star--3"></span>
|
|
50
53
|
<span class="st-star st-star--4"></span>
|
|
51
54
|
<span class="st-star st-star--5"></span>
|
|
55
|
+
<span class="st-cloud st-cloud--1"></span>
|
|
56
|
+
<span class="st-cloud st-cloud--2"></span>
|
|
52
57
|
<span class="st-tower st-tower--1"></span>
|
|
53
58
|
<span class="st-tower st-tower--2"></span>
|
|
54
59
|
<span class="st-tower st-tower--3"></span>
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
<span class="st-mullion"></span>
|
|
65
70
|
</div>
|
|
66
71
|
<div class="st-floor"></div>
|
|
72
|
+
<div class="st-sunpatch"></div>
|
|
67
73
|
<div class="st-glow"></div>
|
|
68
74
|
<div class="st-lamp-stem"></div>
|
|
69
75
|
<div class="st-lamp-shade"></div>
|
|
@@ -199,7 +205,7 @@
|
|
|
199
205
|
<!-- The page names and the draft body (the tweak editor's pure rules), then
|
|
200
206
|
the studio's own. -->
|
|
201
207
|
<script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
|
|
202
|
-
<script src="/builders/studio-lib.js?v=2026-09-
|
|
203
|
-
<script src="/builders/studio.js?v=2026-09-
|
|
208
|
+
<script src="/builders/studio-lib.js?v=2026-10-09-day"></script>
|
|
209
|
+
<script src="/builders/studio.js?v=2026-10-09-day"></script>
|
|
204
210
|
</body>
|
|
205
211
|
</html>
|
|
@@ -838,7 +838,10 @@
|
|
|
838
838
|
wedit.lastSent = JSON.stringify(editorLib.draftBody(wedit.readingHash, wedit.lines, wedit.values));
|
|
839
839
|
wedit.keys = new Map();
|
|
840
840
|
for (const el of wordEls()) {
|
|
841
|
-
|
|
841
|
+
// The greeting is matched by the words the page was served with first:
|
|
842
|
+
// the clock may be showing "Good morning." over the page's one greeting
|
|
843
|
+
// line. A reading taken in daylight may list only the shown words.
|
|
844
|
+
const line = (el === greetEl && lib.matchLine(servedGreeting, wedit.lines)) || lib.matchLine(el.textContent, wedit.lines);
|
|
842
845
|
if (!line) { el.setAttribute('data-unread', ''); continue; }
|
|
843
846
|
wedit.keys.set(el, line.key);
|
|
844
847
|
el.textContent = wedit.values[line.key];
|
|
@@ -929,6 +932,40 @@
|
|
|
929
932
|
if (wedit.again) { wedit.again = false; saveWords(); }
|
|
930
933
|
}
|
|
931
934
|
|
|
935
|
+
// ---- the room's sky and the greeting (task 1004654) ------------------------
|
|
936
|
+
// The day room from 07:00 to 18:59 on the viewer's own clock and the night
|
|
937
|
+
// room otherwise (docs/design/mocks/day-night-rooms), re-checked each minute.
|
|
938
|
+
// Only an attribute changes here: studio.css draws both rooms, and the kill
|
|
939
|
+
// switch stops their motion.
|
|
940
|
+
const SKY_RECHECK_MS = 60000;
|
|
941
|
+
const greetEl = byId('studio-greet');
|
|
942
|
+
const servedGreeting = greetEl ? greetEl.textContent : '';
|
|
943
|
+
|
|
944
|
+
// The greeting follows the time of day while the page still carries the
|
|
945
|
+
// stock one, and never while the artist is typing over it: an artist's tweak
|
|
946
|
+
// of the studio page always wins. Written here, not in studio-lib.js, so the
|
|
947
|
+
// copy registry places each greeting at its own assignment.
|
|
948
|
+
function paintGreeting(part) {
|
|
949
|
+
if (!greetEl || wedit.editing || !lib.clockGreets(servedGreeting)) return;
|
|
950
|
+
if (part === 'morning') greetEl.textContent = 'Good morning.';
|
|
951
|
+
else if (part === 'afternoon') greetEl.textContent = 'Good afternoon.';
|
|
952
|
+
else greetEl.textContent = servedGreeting;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function paintSky() {
|
|
956
|
+
const now = new Date();
|
|
957
|
+
if (document.body) document.body.setAttribute('data-sky', lib.skyAt(now));
|
|
958
|
+
paintGreeting(lib.dayPart(now));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Synchronous, before the first read, so the room is drawn in the right sky
|
|
962
|
+
// from the start rather than turning to day after the network answers.
|
|
963
|
+
function startSky() {
|
|
964
|
+
if (!hasDom || !lib) return;
|
|
965
|
+
paintSky();
|
|
966
|
+
if (typeof setInterval === 'function') setInterval(paintSky, SKY_RECHECK_MS);
|
|
967
|
+
}
|
|
968
|
+
|
|
932
969
|
function wireHome() {
|
|
933
970
|
const flow = byId('st-flow');
|
|
934
971
|
if (flow) flow.addEventListener('click', () => openPanel('flow', flow));
|
|
@@ -1001,6 +1038,7 @@
|
|
|
1001
1038
|
}
|
|
1002
1039
|
|
|
1003
1040
|
(async () => {
|
|
1041
|
+
startSky();
|
|
1004
1042
|
mountApprovals();
|
|
1005
1043
|
const me = await api.request('GET', `${API}/me`);
|
|
1006
1044
|
if (me.ok) viewer = me.data.builder || me.data;
|
|
@@ -23,6 +23,22 @@ The page keeps its words until the round is submitted, applied, approved and shi
|
|
|
23
23
|
|
|
24
24
|
**Why the sub-line's sentence is in studio.js, not studio-lib.js.** The copy registry places a line at its `textContent` assignment. Built inside the lib it read as unplaced, and the reader's pattern tier then matched it to the generic `{…} of {…}` row (sky-panel.js). Written where studio.js assigns it, it is placed on studio.js, which is the page's own file and wins.
|
|
25
25
|
|
|
26
|
+
## The day room (task 1004654)
|
|
27
|
+
|
|
28
|
+
From 07:00 to 18:59 on the viewer's own clock the room is the day room of the owner-approved mockup [`docs/design/mocks/day-night-rooms`](../../../docs/design/mocks/day-night-rooms/README.md) ("Studio", sky "Day"), and the night room otherwise. studio.js sets `data-sky` on the body when the page loads, before the first read, and again every minute. The rule is `skyAt` in studio-lib.js. It is the creation page's rule (projects.html), written again here because hall-ui does not import the landing's code. The day palette is the mockup's `[data-sky="day"]` block, declared on the same `--st-*` names just below the night tokens. The drawing is the same, with clouds and a sun in the window, lighter towers with unlit panes, a warm wall and floor, the lamp off and a patch of sun on the floor. The clouds drift and a change of sky fades, both only without a reduced-motion preference (and style.css's kill switch stops them besides). The hall chrome keeps its own light and dark.
|
|
29
|
+
|
|
30
|
+
The greeting follows the time of day: "Good morning." before noon by day, "Good afternoon." after it, and the page's own "Good evening." at night. It does so only while the served greeting is still the stock "Good evening." (`clockGreets`). Once an artist's tweak of the studio page has rewritten it, their words show at every hour. The clock never types over the words while the artist is editing them.
|
|
31
|
+
|
|
32
|
+
Where it differs from the mockup, and why:
|
|
33
|
+
|
|
34
|
+
- **The link, the credits figure and the focus ring are #96410f by day**, not the mockup's #a3481a link and #fb9569 accent: #a3481a on Resume's tint over the wall is 4.3:1, under AA, and the accent is 1.8:1 on the wall.
|
|
35
|
+
- **The quiet inks are #5a595f (faint) and #66656b (dim, hint)** where the mockup has the one #66656b: the studio has three quiet tiers, and each holds AA on the glass and the bare wall (the hint is the lowest, 4.7:1).
|
|
36
|
+
- **The sun, clouds and sun patch are placed for the studio's 560px window** (the mockup's is 520px): the sun at 394/42 and 76px, the clouds about 8% larger, the patch 20px right to sit under the studio's lamp.
|
|
37
|
+
- **The panels follow the sky**: by day the two glass panels and the Approval queue wear white glass (.75) over a scrim of the wall's own tint (.9) in either hall theme and on a phone. The queue's rendered page stays on its paper frame. The mockup draws no panel.
|
|
38
|
+
- **"Good morning." while being typed over shows the page's greeting line**, "Good evening.", the one line a tweak can rewrite. Rewritten, it is the greeting at every hour.
|
|
39
|
+
- **On a phone the sun patch lies on the phone's floor** (bottom right, 300 by 70); the mockup hides it under 900px, but the studio's phone keeps its desk.
|
|
40
|
+
- **The stars do not twinkle and the lamp does not breathe at night**: the night room stays as it shipped. This task adds only the day room's drift.
|
|
41
|
+
|
|
26
42
|
## The older reviews stay one quiet door away
|
|
27
43
|
|
|
28
44
|
The garden this page used to be (R06 of goal 1000074; retired in TW14) still answers the reviews filed before Tweak Mode, because they may hold a deploy under the strict artist gate. It sits behind a quiet line under the actions ("1 review from before Tweak Mode still waits"), shown only while a review waits, and opens in its own dialog in the hall's tokens with the focus card, the stones and the two links, unchanged. Its verdict writes are unchanged.
|
|
@@ -1253,6 +1253,9 @@ async function abandonTask({ taskId, reason, authorize = null }, deps = {}) {
|
|
|
1253
1253
|
//
|
|
1254
1254
|
// Restores to **backlog**, not ready: the task was retired deliberately, so it
|
|
1255
1255
|
// comes back un-workable and needs a second, explicit promote to be claimable.
|
|
1256
|
+
// It comes back PARKED (parked_at/parked_by, core_274, task 1004592): without
|
|
1257
|
+
// the stamp the promotion sweep promoted it on the next tick, so the "second,
|
|
1258
|
+
// explicit promote" never had to happen. `parkedBy` is the restoring builder.
|
|
1256
1259
|
// abandonTask stamps `value_summary = 'Abandoned: <reason>'`; a restored task
|
|
1257
1260
|
// clears exactly that stamp (and nothing else) so a live task never carries a
|
|
1258
1261
|
// retire note into its ship card / broadcast.
|
|
@@ -1264,7 +1267,7 @@ async function abandonTask({ taskId, reason, authorize = null }, deps = {}) {
|
|
|
1264
1267
|
//
|
|
1265
1268
|
// #538 test seam: a caller-supplied pool is honored ONLY under NODE_ENV==='test'
|
|
1266
1269
|
// (matches abandonTask/confirmTask). Production callers omit deps → module pool.
|
|
1267
|
-
async function restoreTask({ taskId }, deps = {}) {
|
|
1270
|
+
async function restoreTask({ taskId, parkedBy = null }, deps = {}) {
|
|
1268
1271
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
1269
1272
|
const client = await activePool.connect();
|
|
1270
1273
|
try {
|
|
@@ -1296,13 +1299,15 @@ async function restoreTask({ taskId }, deps = {}) {
|
|
|
1296
1299
|
const { rows: updated } = await client.query(
|
|
1297
1300
|
`UPDATE tasks
|
|
1298
1301
|
SET status = 'backlog',
|
|
1302
|
+
parked_at = now(),
|
|
1303
|
+
parked_by = $2,
|
|
1299
1304
|
merged_into_task_id = NULL,
|
|
1300
1305
|
triage_note = NULL,
|
|
1301
1306
|
value_summary = CASE WHEN value_summary LIKE 'Abandoned: %' THEN NULL ELSE value_summary END,
|
|
1302
1307
|
updated_at = now()
|
|
1303
1308
|
WHERE id = $1
|
|
1304
1309
|
RETURNING id, title, status`,
|
|
1305
|
-
[taskId]
|
|
1310
|
+
[taskId, parkedBy]
|
|
1306
1311
|
);
|
|
1307
1312
|
|
|
1308
1313
|
await client.query('COMMIT');
|
|
@@ -1257,6 +1257,21 @@ async function updateTaskStatus(id, status) {
|
|
|
1257
1257
|
await pool.query(`UPDATE tasks SET status = $2 WHERE id = $1`, [id, status]);
|
|
1258
1258
|
}
|
|
1259
1259
|
|
|
1260
|
+
// POST /tasks/:id/demote (task 1004592): ready -> backlog, PARKED. The stamp is
|
|
1261
|
+
// what tells the promotion sweep and the core_274 triggers that a person put
|
|
1262
|
+
// this task here, so neither undoes the demote on the next tick. The status
|
|
1263
|
+
// guard sits in the WHERE so a task that moved since the route read it is left
|
|
1264
|
+
// alone; returns whether the row was demoted.
|
|
1265
|
+
async function parkTask(id, builderId, exec) {
|
|
1266
|
+
const { rowCount } = await (exec || pool).query(
|
|
1267
|
+
`UPDATE tasks
|
|
1268
|
+
SET status = 'backlog', parked_at = now(), parked_by = $2, updated_at = now()
|
|
1269
|
+
WHERE id = $1 AND status = 'ready'`,
|
|
1270
|
+
[id, builderId == null ? null : builderId]
|
|
1271
|
+
);
|
|
1272
|
+
return rowCount > 0;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1260
1275
|
// The UNSCOPED half of auto-promotion (task 1003932) — the backlog drain that
|
|
1261
1276
|
// `runCriterionSweep` already is for criteria.
|
|
1262
1277
|
//
|
|
@@ -1305,9 +1320,18 @@ async function updateTaskStatus(id, status) {
|
|
|
1305
1320
|
//
|
|
1306
1321
|
// The exclusion is by SOURCE, not by discipline: engineer-scoping-on-full-idea
|
|
1307
1322
|
// is equally a request, and the preamble above is written on both.
|
|
1323
|
+
//
|
|
1324
|
+
// The second kind has a second source: a PERSON parking a task (task 1004592).
|
|
1325
|
+
// restoreTask and POST /tasks/:id/demote both stamp tasks.parked_at, and
|
|
1326
|
+
// `parked_at IS NULL` keeps the sweep off those rows. core_274 puts the same
|
|
1327
|
+
// clause in the three promotion triggers and clears the stamp whenever a task
|
|
1328
|
+
// leaves backlog, so a person's promote is the only way out. Hand-filed backlog
|
|
1329
|
+
// with no parked_at is still drained: create-time intent is not recorded, and
|
|
1330
|
+
// dependencies are often added after create.
|
|
1308
1331
|
const PROMOTABLE_BACKLOG_WHERE = `
|
|
1309
1332
|
WHERE t.status = 'backlog'
|
|
1310
1333
|
AND t.source IS DISTINCT FROM 'cascade'
|
|
1334
|
+
AND t.parked_at IS NULL
|
|
1311
1335
|
AND task_is_fully_unblocked(t.id)
|
|
1312
1336
|
AND NOT EXISTS (
|
|
1313
1337
|
SELECT 1
|
|
@@ -1444,6 +1468,7 @@ module.exports = {
|
|
|
1444
1468
|
updateTaskRequiresRank,
|
|
1445
1469
|
updateTaskSecuritySensitive,
|
|
1446
1470
|
updateTaskStatus,
|
|
1471
|
+
parkTask,
|
|
1447
1472
|
updateTaskValueSummary,
|
|
1448
1473
|
versionAcceptsNewWork,
|
|
1449
1474
|
};
|