create-pmos 0.132.0 → 0.133.0
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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/pmos.js +18 -2
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/welcome.md +38 -0
- package/workspace/lib/import-overlay.js +32 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-onboarding.js +215 -0
- package/workspace/lib/vendor/scripts/anchor-resolve.js +351 -0
- package/workspace/lib/vendor/scripts/delivery-index.js +356 -0
- package/workspace/lib/vendor/scripts/intake.js +78 -0
- package/workspace/lib/vendor/scripts/lane-defs.js +226 -0
- package/workspace/lib/vendor/scripts/migrate-layout.js +775 -0
- package/workspace/lib/vendor/scripts/nine-stages.js +81 -0
- package/workspace/lib/vendor/scripts/onboarding/apply.js +110 -0
- package/workspace/lib/vendor/scripts/onboarding/brief.js +154 -0
- package/workspace/lib/vendor/scripts/onboarding/first-briefing.js +41 -0
- package/workspace/lib/vendor/scripts/onboarding/first-look.js +84 -0
- package/workspace/lib/vendor/scripts/onboarding/infer-brief.js +70 -0
- package/workspace/lib/vendor/scripts/onboarding/proposals.js +98 -0
- package/workspace/lib/vendor/scripts/onboarding/scan-worker.js +27 -0
- package/workspace/lib/vendor/scripts/onboarding/scan.js +859 -0
- package/workspace/lib/vendor/scripts/onboarding/structure.js +217 -0
- package/workspace/lib/vendor/scripts/plan-items-stage.js +271 -0
- package/workspace/lib/vendor/scripts/read-jsonl.js +53 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +43 -0
- package/workspace/lib/vendor/scripts/status-facts.js +214 -0
- package/workspace/lib/vendor/scripts/trust/import-guard.js +36 -0
- package/workspace/lib/vendor/scripts/trust/ledger.js +53 -0
- package/workspace/lib/vendor/scripts/workflow-state.js +1888 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +91 -0
- package/workspace/lib/vendor/workspace/lib/board-cache.js +156 -0
- package/workspace/lib/vendor/workspace/lib/console-state.js +295 -0
- package/workspace/lib/vendor/workspace/lib/doc-category.js +227 -0
- package/workspace/lib/vendor/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/vendor/workspace/lib/git-info.js +306 -0
- package/workspace/lib/vendor/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +419 -0
- package/workspace/lib/vendor/workspace/lib/review-gate.js +39 -0
- package/workspace/lib/vendor/workspace/lib/sink.js +321 -0
- package/workspace/lib/vendor/workspace/lib/state-root.js +587 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +280 -0
- package/workspace/public/pm/welcome.js +283 -0
- package/workspace/public/welcome.html +20 -0
- package/workspace/server.js +2 -2
- package/workspace/test-lists/onboarding-firstrun.json +15 -0
|
@@ -7965,5 +7965,285 @@
|
|
|
7965
7965
|
containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
|
|
7966
7966
|
(8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
|
|
7967
7967
|
|
|
7968
|
+
/* -- layouts · .wl- Welcome: the first-run page (onboarding-scan-and-map slice 2) -- */
|
|
7969
|
+
.wl-page {
|
|
7970
|
+
background: var(--background);
|
|
7971
|
+
}
|
|
7972
|
+
.wl-body {
|
|
7973
|
+
margin: 0;
|
|
7974
|
+
background: var(--background);
|
|
7975
|
+
color: var(--foreground);
|
|
7976
|
+
font-family: var(--sans);
|
|
7977
|
+
font-size: 16px;
|
|
7978
|
+
line-height: 1.5;
|
|
7979
|
+
}
|
|
7980
|
+
.wl-wrap {
|
|
7981
|
+
max-width: 720px;
|
|
7982
|
+
margin: 0 auto;
|
|
7983
|
+
padding: 24px 16px 64px;
|
|
7984
|
+
box-sizing: border-box;
|
|
7985
|
+
}
|
|
7986
|
+
.wl-wrap * {
|
|
7987
|
+
box-sizing: border-box;
|
|
7988
|
+
}
|
|
7989
|
+
.wl-steps {
|
|
7990
|
+
display: flex;
|
|
7991
|
+
flex-wrap: wrap;
|
|
7992
|
+
gap: 6px;
|
|
7993
|
+
margin: 0 0 20px;
|
|
7994
|
+
padding: 0;
|
|
7995
|
+
list-style: none;
|
|
7996
|
+
font-family: var(--mono);
|
|
7997
|
+
font-size: 12px;
|
|
7998
|
+
color: var(--muted-foreground);
|
|
7999
|
+
}
|
|
8000
|
+
.wl-steps li {
|
|
8001
|
+
padding: 2px 8px;
|
|
8002
|
+
border: 1px solid var(--border);
|
|
8003
|
+
border-radius: 6px;
|
|
8004
|
+
}
|
|
8005
|
+
.wl-steps li[aria-current="step"] {
|
|
8006
|
+
color: var(--foreground);
|
|
8007
|
+
border-color: var(--foreground);
|
|
8008
|
+
}
|
|
8009
|
+
.wl-h1 {
|
|
8010
|
+
font-size: 20px;
|
|
8011
|
+
line-height: 1.2;
|
|
8012
|
+
margin: 0 0 8px;
|
|
8013
|
+
font-weight: 600;
|
|
8014
|
+
}
|
|
8015
|
+
.wl-h2 {
|
|
8016
|
+
font-size: 16px;
|
|
8017
|
+
margin: 0;
|
|
8018
|
+
font-weight: 600;
|
|
8019
|
+
}
|
|
8020
|
+
.wl-p {
|
|
8021
|
+
margin: 0 0 12px;
|
|
8022
|
+
}
|
|
8023
|
+
.wl-muted {
|
|
8024
|
+
color: var(--muted-foreground);
|
|
8025
|
+
}
|
|
8026
|
+
.wl-card {
|
|
8027
|
+
background: var(--card);
|
|
8028
|
+
border: 1px solid var(--border);
|
|
8029
|
+
border-radius: 8px;
|
|
8030
|
+
padding: 14px 16px;
|
|
8031
|
+
margin: 0 0 14px;
|
|
8032
|
+
}
|
|
8033
|
+
.wl-path {
|
|
8034
|
+
font-family: var(--mono);
|
|
8035
|
+
font-size: 12px;
|
|
8036
|
+
color: var(--muted-foreground);
|
|
8037
|
+
word-break: break-all;
|
|
8038
|
+
}
|
|
8039
|
+
.wl-promise {
|
|
8040
|
+
font-family: var(--mono);
|
|
8041
|
+
font-size: 12px;
|
|
8042
|
+
margin: 16px 0;
|
|
8043
|
+
padding: 10px 12px;
|
|
8044
|
+
border-left: 3px solid var(--foreground);
|
|
8045
|
+
background: var(--muted);
|
|
8046
|
+
}
|
|
8047
|
+
.wl-actions {
|
|
8048
|
+
display: flex;
|
|
8049
|
+
flex-wrap: wrap;
|
|
8050
|
+
align-items: center;
|
|
8051
|
+
gap: 12px;
|
|
8052
|
+
margin-top: 18px;
|
|
8053
|
+
}
|
|
8054
|
+
.wl-btn {
|
|
8055
|
+
font: inherit;
|
|
8056
|
+
cursor: pointer;
|
|
8057
|
+
border-radius: 8px;
|
|
8058
|
+
padding: 10px 16px;
|
|
8059
|
+
min-height: 44px;
|
|
8060
|
+
border: 1px solid var(--border);
|
|
8061
|
+
background: var(--secondary);
|
|
8062
|
+
color: var(--foreground);
|
|
8063
|
+
text-decoration: none;
|
|
8064
|
+
display: inline-flex;
|
|
8065
|
+
align-items: center;
|
|
8066
|
+
justify-content: center;
|
|
8067
|
+
}
|
|
8068
|
+
.wl-btn.wl-primary {
|
|
8069
|
+
background: var(--primary);
|
|
8070
|
+
color: var(--primary-foreground);
|
|
8071
|
+
border-color: var(--primary);
|
|
8072
|
+
font-weight: 600;
|
|
8073
|
+
}
|
|
8074
|
+
.wl-btn:disabled {
|
|
8075
|
+
opacity: 0.55;
|
|
8076
|
+
cursor: default;
|
|
8077
|
+
}
|
|
8078
|
+
.wl-btn.wl-quiet {
|
|
8079
|
+
background: none;
|
|
8080
|
+
border: none;
|
|
8081
|
+
color: var(--muted-foreground);
|
|
8082
|
+
text-decoration: underline;
|
|
8083
|
+
padding: 8px 4px;
|
|
8084
|
+
min-height: 36px;
|
|
8085
|
+
}
|
|
8086
|
+
.wl-btn.wl-small {
|
|
8087
|
+
min-height: 32px;
|
|
8088
|
+
padding: 4px 10px;
|
|
8089
|
+
font-size: 14px;
|
|
8090
|
+
}
|
|
8091
|
+
.wl-btn:focus-visible,
|
|
8092
|
+
.wl-input:focus-visible,
|
|
8093
|
+
.wl-check:focus-visible {
|
|
8094
|
+
outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));
|
|
8095
|
+
outline-offset: 2px;
|
|
8096
|
+
}
|
|
8097
|
+
.wl-spin {
|
|
8098
|
+
display: inline-block;
|
|
8099
|
+
width: 14px;
|
|
8100
|
+
height: 14px;
|
|
8101
|
+
border: 2px solid var(--border);
|
|
8102
|
+
border-top-color: var(--foreground);
|
|
8103
|
+
border-radius: 6px;
|
|
8104
|
+
animation: wlspin 0.8s linear infinite;
|
|
8105
|
+
margin-right: 8px;
|
|
8106
|
+
}
|
|
8107
|
+
@keyframes wlspin {
|
|
8108
|
+
to {
|
|
8109
|
+
transform: rotate(360deg);
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
.wl-error {
|
|
8113
|
+
border: 1px solid var(--destructive);
|
|
8114
|
+
color: var(--destructive);
|
|
8115
|
+
border-radius: 8px;
|
|
8116
|
+
padding: 10px 12px;
|
|
8117
|
+
margin: 0 0 14px;
|
|
8118
|
+
}
|
|
8119
|
+
.wl-findings {
|
|
8120
|
+
margin: 0;
|
|
8121
|
+
padding-left: 22px;
|
|
8122
|
+
}
|
|
8123
|
+
.wl-findings li {
|
|
8124
|
+
margin: 0 0 12px;
|
|
8125
|
+
}
|
|
8126
|
+
.wl-ref {
|
|
8127
|
+
font-family: var(--mono);
|
|
8128
|
+
font-size: 12px;
|
|
8129
|
+
color: var(--muted-foreground);
|
|
8130
|
+
word-break: break-all;
|
|
8131
|
+
}
|
|
8132
|
+
.wl-board > header {
|
|
8133
|
+
display: flex;
|
|
8134
|
+
flex-wrap: wrap;
|
|
8135
|
+
justify-content: space-between;
|
|
8136
|
+
gap: 8px;
|
|
8137
|
+
align-items: baseline;
|
|
8138
|
+
margin-bottom: 8px;
|
|
8139
|
+
}
|
|
8140
|
+
.wl-row {
|
|
8141
|
+
display: grid;
|
|
8142
|
+
grid-template-columns: auto 1fr;
|
|
8143
|
+
gap: 6px 10px;
|
|
8144
|
+
padding: 10px 0;
|
|
8145
|
+
border-top: 1px solid var(--border);
|
|
8146
|
+
align-items: start;
|
|
8147
|
+
}
|
|
8148
|
+
.wl-check {
|
|
8149
|
+
width: 20px;
|
|
8150
|
+
height: 20px;
|
|
8151
|
+
margin: 10px 0 0;
|
|
8152
|
+
}
|
|
8153
|
+
.wl-input {
|
|
8154
|
+
font: inherit;
|
|
8155
|
+
color: var(--foreground);
|
|
8156
|
+
background: var(--background);
|
|
8157
|
+
border: 1px solid var(--input);
|
|
8158
|
+
border-radius: 6px;
|
|
8159
|
+
padding: 6px 8px;
|
|
8160
|
+
min-height: 36px;
|
|
8161
|
+
max-width: 100%;
|
|
8162
|
+
}
|
|
8163
|
+
.wl-row .wl-input[type="text"] {
|
|
8164
|
+
width: 100%;
|
|
8165
|
+
}
|
|
8166
|
+
.wl-meta {
|
|
8167
|
+
display: flex;
|
|
8168
|
+
flex-wrap: wrap;
|
|
8169
|
+
gap: 6px 10px;
|
|
8170
|
+
align-items: center;
|
|
8171
|
+
font-family: var(--mono);
|
|
8172
|
+
font-size: 12px;
|
|
8173
|
+
color: var(--muted-foreground);
|
|
8174
|
+
}
|
|
8175
|
+
.wl-chip {
|
|
8176
|
+
border: 1px solid var(--border);
|
|
8177
|
+
border-radius: 6px;
|
|
8178
|
+
padding: 1px 8px;
|
|
8179
|
+
}
|
|
8180
|
+
.wl-docs {
|
|
8181
|
+
margin: 4px 0 0;
|
|
8182
|
+
padding-left: 18px;
|
|
8183
|
+
font-family: var(--mono);
|
|
8184
|
+
font-size: 12px;
|
|
8185
|
+
color: var(--muted-foreground);
|
|
8186
|
+
word-break: break-all;
|
|
8187
|
+
}
|
|
8188
|
+
.wl-q {
|
|
8189
|
+
margin: 0 0 14px;
|
|
8190
|
+
}
|
|
8191
|
+
.wl-label {
|
|
8192
|
+
display: block;
|
|
8193
|
+
margin: 4px 0;
|
|
8194
|
+
}
|
|
8195
|
+
.wl-q .wl-free {
|
|
8196
|
+
margin-top: 10px;
|
|
8197
|
+
color: var(--muted-foreground);
|
|
8198
|
+
}
|
|
8199
|
+
.wl-q .wl-free .wl-input {
|
|
8200
|
+
display: block;
|
|
8201
|
+
margin-top: 4px;
|
|
8202
|
+
}
|
|
8203
|
+
.wl-added {
|
|
8204
|
+
margin: 8px 0 0;
|
|
8205
|
+
padding-left: 18px;
|
|
8206
|
+
font-size: 14px;
|
|
8207
|
+
}
|
|
8208
|
+
.wl-sources {
|
|
8209
|
+
list-style: none;
|
|
8210
|
+
margin: 0 0 12px;
|
|
8211
|
+
padding: 0;
|
|
8212
|
+
display: grid;
|
|
8213
|
+
gap: 12px;
|
|
8214
|
+
}
|
|
8215
|
+
.wl-source {
|
|
8216
|
+
display: flex;
|
|
8217
|
+
flex-wrap: wrap;
|
|
8218
|
+
justify-content: space-between;
|
|
8219
|
+
align-items: center;
|
|
8220
|
+
gap: 12px;
|
|
8221
|
+
padding-bottom: 12px;
|
|
8222
|
+
border-bottom: 1px solid var(--border);
|
|
8223
|
+
}
|
|
8224
|
+
.wl-source-text {
|
|
8225
|
+
flex: 1 1 260px;
|
|
8226
|
+
min-width: 0;
|
|
8227
|
+
}
|
|
8228
|
+
.wl-source-actions {
|
|
8229
|
+
display: flex;
|
|
8230
|
+
flex-wrap: wrap;
|
|
8231
|
+
align-items: center;
|
|
8232
|
+
gap: 8px;
|
|
8233
|
+
}
|
|
8234
|
+
@media (max-width: 520px) {
|
|
8235
|
+
.wl-actions {
|
|
8236
|
+
flex-direction: column;
|
|
8237
|
+
align-items: stretch;
|
|
8238
|
+
}
|
|
8239
|
+
.wl-actions .wl-btn {
|
|
8240
|
+
width: 100%;
|
|
8241
|
+
}
|
|
8242
|
+
.wl-actions .wl-btn.wl-quiet {
|
|
8243
|
+
width: auto;
|
|
8244
|
+
align-self: center;
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
|
|
7968
8248
|
/* -- layouts · .cal3- the console3 Calendar page (calendar-time-page): its rules sit in console3.css beside the .vday rules they extend -- */
|
|
7969
8249
|
/* -- end of the layout sections: add nothing below this line -- */
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/* pm/welcome.js — the first-run page (/welcome). One primary button per screen.
|
|
2
|
+
*
|
|
3
|
+
* Welcome → First look → Your portfolio → Questions (only when the scan left any) → First Briefing → More sources
|
|
4
|
+
*
|
|
5
|
+
* Routes (lib/routes-onboarding.js): GET /api/onboarding/status|structure|briefing, POST /api/onboarding/scan|apply|undo.
|
|
6
|
+
* More sources uses POST /api/sources/refresh (lib/routes-sources.js): it starts the source's read-only skill as a session in
|
|
7
|
+
* the workspace with the PM's own connection. Nothing is read until the PM presses Bring in; the server calls nothing outside.
|
|
8
|
+
* Presses carry the launch nonce through pm/http.js. The page writes nothing itself; text taken from the scanned folder is
|
|
9
|
+
* untrusted, so every value goes through esc() and no quoted text from a file is shown.
|
|
10
|
+
* data-testid attributes are the browser suite's handles.
|
|
11
|
+
*/
|
|
12
|
+
import { pmGet, pmPost } from './http.js';
|
|
13
|
+
|
|
14
|
+
const root = document.getElementById('wl-root');
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
const S = {
|
|
17
|
+
ws: params.get('ws') || '', folder: null, added: [], step: 'welcome', busy: '', error: '', note: '',
|
|
18
|
+
look: null, structure: null, checked: {}, titles: {}, boardOf: {}, open: {}, answers: {}, skipped: {}, importId: '', briefing: [], sources: {},
|
|
19
|
+
};
|
|
20
|
+
const STEPS = [['welcome', 'Welcome'], ['look', 'First look'], ['portfolio', 'Your portfolio'], ['questions', 'Questions'], ['briefing', 'First Briefing'], ['sources', 'More sources']];
|
|
21
|
+
/* The sources a first run can bring in. `source` is a lib/freshness.js SOURCES id with a read-only skill. Inbox and Slack
|
|
22
|
+
* have no import skill yet, so the page says so instead of offering a button that cannot work. */
|
|
23
|
+
const MORE_SOURCES = [
|
|
24
|
+
{ source: 'calendar', name: 'Calendar', adds: 'Your meetings today and for the next 14 days, so the Briefing can get you ready for them.' },
|
|
25
|
+
{ source: 'my-work', name: 'Jira', adds: 'The Jira issues assigned to you, so My work starts with them.' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
29
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
30
|
+
const ref = (e) => (e ? (e.line ? `${e.file}:${e.line}` : e.file) : '');
|
|
31
|
+
|
|
32
|
+
/* ── model ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
33
|
+
const rowsOf = () => (S.structure ? S.structure.boards.flatMap((b) => b.initiatives.map((r) => ({ ...r, board: b.key }))) : []);
|
|
34
|
+
const boardOfRow = (r) => S.boardOf[r.proposal_id] || r.board;
|
|
35
|
+
function counts() {
|
|
36
|
+
const on = rowsOf().filter((r) => S.checked[r.proposal_id]);
|
|
37
|
+
return { n: on.length, m: new Set(on.map(boardOfRow)).size };
|
|
38
|
+
}
|
|
39
|
+
function adopt(structure) {
|
|
40
|
+
S.structure = structure; S.checked = {}; S.titles = {}; S.boardOf = {}; S.open = {}; S.answers = {}; S.skipped = {};
|
|
41
|
+
for (const r of rowsOf()) { S.checked[r.proposal_id] = !!r.checked; S.titles[r.proposal_id] = r.title; }
|
|
42
|
+
}
|
|
43
|
+
function revisions() {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const r of rowsOf()) {
|
|
46
|
+
const rev = {};
|
|
47
|
+
if (S.titles[r.proposal_id] !== r.title && String(S.titles[r.proposal_id]).trim()) rev.title = String(S.titles[r.proposal_id]).trim();
|
|
48
|
+
if (S.boardOf[r.proposal_id] && S.boardOf[r.proposal_id] !== r.board) rev.board = S.boardOf[r.proposal_id];
|
|
49
|
+
if (Object.keys(rev).length) out[r.proposal_id] = rev;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ── actions ─────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
55
|
+
const fail = (r, what) => { S.error = (r && r.error) || `${what} did not work.`; };
|
|
56
|
+
async function run(label, fn) {
|
|
57
|
+
S.busy = label; S.error = ''; render();
|
|
58
|
+
try { await fn(); } catch (e) { S.error = String((e && e.message) || e); }
|
|
59
|
+
S.busy = ''; render();
|
|
60
|
+
}
|
|
61
|
+
async function loadStructure() {
|
|
62
|
+
const r = await pmGet(`/api/onboarding/structure?ws=${encodeURIComponent(S.ws)}`);
|
|
63
|
+
if (!r || !r.ok) { fail(r, 'Reading the portfolio'); return false; }
|
|
64
|
+
adopt(r); return true;
|
|
65
|
+
}
|
|
66
|
+
async function loadBriefing() {
|
|
67
|
+
const r = await pmGet(`/api/onboarding/briefing?ws=${encodeURIComponent(S.ws)}`);
|
|
68
|
+
if (!r || !r.ok) { fail(r, 'Reading the Briefing'); return false; }
|
|
69
|
+
S.briefing = r.items || []; return true;
|
|
70
|
+
}
|
|
71
|
+
const addFolder = () => run('add', async () => {
|
|
72
|
+
const p = await pmPost('/api/pick-folder', {});
|
|
73
|
+
if (!p || !p.ok) { if (p && p.cancelled) return; S.note = (p && p.unsupported) ? 'This system has no folder chooser. Start pmos in the folder instead.' : ''; if (!S.note) fail(p, 'Choosing a folder'); return; }
|
|
74
|
+
const w = await pmPost('/api/workspaces', { root: p.root, name: p.root.split(/[\\/]/).filter(Boolean).pop(), focus: false });
|
|
75
|
+
if (!w || !w.workspace) { fail(w, 'Adding the folder'); return; }
|
|
76
|
+
if (!S.added.some((a) => a.id === w.workspace.id) && w.workspace.id !== S.ws) S.added.push({ id: w.workspace.id, name: w.workspace.name, root: w.workspace.root });
|
|
77
|
+
});
|
|
78
|
+
const scan = () => run('scan', async () => {
|
|
79
|
+
const r = await pmPost('/api/onboarding/scan', { ws: S.ws, extra_roots: S.added.map((a) => a.root) });
|
|
80
|
+
if (!r || !r.ok) { fail(r, 'The scan'); return; }
|
|
81
|
+
S.look = r.first_look; S.step = 'look';
|
|
82
|
+
});
|
|
83
|
+
const toPortfolio = () => run('structure', async () => { if (await loadStructure()) S.step = 'portfolio'; });
|
|
84
|
+
async function applyNow() {
|
|
85
|
+
const include = rowsOf().filter((r) => S.checked[r.proposal_id]).map((r) => r.proposal_id);
|
|
86
|
+
const answers = {};
|
|
87
|
+
// an untouched question sends its default (the PM saw it and pressed Import); a skipped one sends nothing
|
|
88
|
+
for (const q of (S.structure.questions || [])) {
|
|
89
|
+
if (S.skipped[q.id]) continue;
|
|
90
|
+
const v = S.answers[q.id] != null ? S.answers[q.id] : q.default;
|
|
91
|
+
if (q.id === 'template') answers.template = Object.assign(answers.template || {}, { [q.board]: v });
|
|
92
|
+
else answers[q.id] = v;
|
|
93
|
+
}
|
|
94
|
+
const r = await pmPost('/api/onboarding/apply', { ws: S.ws, include, revisions: revisions(), answers });
|
|
95
|
+
if (!r || !r.ok) { fail(r, 'The import'); return; }
|
|
96
|
+
S.importId = r.import_id;
|
|
97
|
+
if (await loadBriefing()) S.step = 'briefing';
|
|
98
|
+
}
|
|
99
|
+
const importNow = () => {
|
|
100
|
+
if (!counts().n) return;
|
|
101
|
+
if ((S.structure.questions || []).length && S.step !== 'questions') { S.step = 'questions'; render(); return; }
|
|
102
|
+
run('apply', applyNow);
|
|
103
|
+
};
|
|
104
|
+
// "Not now": Welcome stops opening on its own for this folder (`pmos welcome` still opens it), then the console opens
|
|
105
|
+
const notNow = () => run('dismiss', async () => {
|
|
106
|
+
const r = await pmPost('/api/onboarding/dismiss', { ws: S.ws });
|
|
107
|
+
if (!r || !r.ok) { fail(r, 'Not now'); return; }
|
|
108
|
+
location.href = '/c3.html?ws=' + encodeURIComponent(S.ws);
|
|
109
|
+
});
|
|
110
|
+
const undo = () => run('undo', async () => {
|
|
111
|
+
const r = await pmPost('/api/onboarding/undo', { ws: S.ws, import_id: S.importId });
|
|
112
|
+
if (!r || !r.ok) { fail(r, 'Undo'); return; }
|
|
113
|
+
S.importId = '';
|
|
114
|
+
if (await loadStructure()) S.step = 'portfolio';
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/* ── views ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
118
|
+
const spin = (txt) => `<span class="wl-spin" data-testid="loading" aria-hidden="true"></span>${esc(txt)}`;
|
|
119
|
+
|
|
120
|
+
function viewWelcome() {
|
|
121
|
+
const f = S.folder || { name: '', root: '' };
|
|
122
|
+
const added = S.added.length ? `<ul class="wl-added" data-testid="added-folders">${S.added.map((a) => `<li data-testid="added-folder">${esc(a.name)} <span class="wl-path">${esc(a.root)}</span></li>`).join('')}</ul>` : '';
|
|
123
|
+
return `<h1 class="wl-h1">Welcome to PMOS</h1>
|
|
124
|
+
<p class="wl-p wl-muted">PMOS reads your folder and shows you what it found: your initiatives, your documents and what needs you first.</p>
|
|
125
|
+
<section class="wl-card"><h2 class="wl-h2" data-testid="folder-name">${esc(f.name || 'This folder')}</h2><div class="wl-path" data-testid="folder-path">${esc(f.root)}</div>${added}
|
|
126
|
+
<div class="wl-actions"><button type="button" class="wl-btn" data-testid="add-folder" ${S.busy ? 'disabled' : ''}>${S.busy === 'add' ? spin('Waiting for the chooser') : 'Add another folder'}</button></div>
|
|
127
|
+
${S.note ? `<p class="wl-p wl-muted" data-testid="note">${esc(S.note)}</p>` : ''}</section>
|
|
128
|
+
<p class="wl-promise" data-testid="promise">Reads only. Writes nothing in your folder. Nothing leaves this computer.</p>
|
|
129
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="scan-button" ${S.busy ? 'disabled' : ''}>${S.busy === 'scan' ? spin('Scanning your folder') : 'Scan'}</button>
|
|
130
|
+
<button type="button" class="wl-btn wl-quiet" data-testid="not-now" ${S.busy ? 'disabled' : ''}>Not now</button></div>
|
|
131
|
+
${S.busy === 'scan' ? '<p class="wl-p wl-muted" data-testid="scan-wait">A large folder can take a minute or two. PMOS keeps working while it reads.</p>' : ''}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function viewLook() {
|
|
135
|
+
const L = S.look || {};
|
|
136
|
+
const items = (L.findings || []).slice(0, 10).map((f) => `<li data-testid="finding"><strong>${esc(f.title)}</strong>${f.why ? `<div class="wl-muted">${esc(f.why)}</div>` : ''}<div class="wl-ref" data-testid="finding-ref">${esc(ref((f.evidence || [])[0]))}</div></li>`).join('');
|
|
137
|
+
return `<h1 class="wl-h1">First look</h1>
|
|
138
|
+
<p class="wl-p" data-testid="first-look-sentence">${esc(L.sentence)}</p>
|
|
139
|
+
<section class="wl-card"><h2 class="wl-h2">What matters most</h2>${items ? `<ol class="wl-findings">${items}</ol>` : '<p class="wl-p wl-muted" data-testid="no-findings">Nothing in this folder needs you urgently.</p>'}</section>
|
|
140
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="continue-button" ${S.busy ? 'disabled' : ''}>${S.busy === 'structure' ? spin('Building your portfolio') : 'Continue'}</button></div>`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rowView(r) {
|
|
144
|
+
const id = r.proposal_id; const docs = r.docs || []; const open = !!S.open[id];
|
|
145
|
+
const opts = S.structure.boards.map((b) => `<option value="${esc(b.key)}" ${b.key === boardOfRow(r) ? 'selected' : ''}>${esc(b.name)}</option>`).join('');
|
|
146
|
+
return `<div class="wl-row" data-testid="row" data-id="${esc(id)}">
|
|
147
|
+
<input type="checkbox" class="wl-check" data-testid="row-check" data-id="${esc(id)}" aria-label="Import ${esc(S.titles[id])}" ${S.checked[id] ? 'checked' : ''} />
|
|
148
|
+
<div><input type="text" class="wl-input" data-testid="row-title" data-id="${esc(id)}" value="${esc(S.titles[id])}" aria-label="Title" />
|
|
149
|
+
<div class="wl-meta"><span class="wl-chip" data-testid="row-lane">${esc(r.lane)}</span><span class="wl-chip" data-testid="row-anchor">${esc(r.anchor || 'no anchor')}</span><span class="wl-chip" data-testid="row-stage">${esc(r.stage)}</span>
|
|
150
|
+
<button type="button" class="wl-btn wl-small" data-testid="row-docs-toggle" data-id="${esc(id)}" aria-expanded="${open}">${plural(docs.length, 'document', 'documents')}</button>
|
|
151
|
+
<select class="wl-input" data-testid="row-board" data-id="${esc(id)}" aria-label="Board">${opts}</select></div>
|
|
152
|
+
${open ? `<ul class="wl-docs" data-testid="row-docs">${docs.map((d) => `<li>${esc(d.title && d.title !== d.file ? d.title + ' · ' : '')}${esc(d.file)}</li>`).join('')}</ul>` : ''}</div></div>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function viewPortfolio() {
|
|
156
|
+
const all = rowsOf(); const c = counts();
|
|
157
|
+
const boards = S.structure.boards.map((b) => {
|
|
158
|
+
const mine = all.filter((r) => boardOfRow(r) === b.key);
|
|
159
|
+
return `<section class="wl-card wl-board" data-testid="board" data-board="${esc(b.key)}"><header><h2 class="wl-h2" data-testid="board-name">${esc(b.name)}</h2>
|
|
160
|
+
<span class="wl-muted" data-testid="unsorted-count">${plural((b.unsorted || []).length, 'document', 'documents')} unsorted</span></header>
|
|
161
|
+
${mine.length ? mine.map(rowView).join('') : '<p class="wl-p wl-muted">No initiatives here.</p>'}</section>`;
|
|
162
|
+
}).join('');
|
|
163
|
+
return `<h1 class="wl-h1">Your portfolio</h1>
|
|
164
|
+
<p class="wl-p wl-muted">This is how PMOS would set up your work. Untick what you do not want, rename it, or move it to another board.</p>
|
|
165
|
+
${boards}
|
|
166
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="import-button" ${S.busy || !c.n ? 'disabled' : ''}>${S.busy === 'apply' ? spin('Importing') : `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`}</button></div>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function viewQuestions() {
|
|
170
|
+
const qs = (S.structure.questions || []).map((q) => {
|
|
171
|
+
const skipped = !!S.skipped[q.id];
|
|
172
|
+
const cur = S.answers[q.id] != null ? S.answers[q.id] : q.default;
|
|
173
|
+
const input = (q.options && q.options.length)
|
|
174
|
+
? (q.options.length > 4 && !q.free
|
|
175
|
+
? `<select class="wl-input" data-testid="question-select" data-id="${esc(q.id)}" ${skipped ? 'disabled' : ''}>${q.options.map((o) => { const v = typeof o === 'object' ? o.value : o; const t = typeof o === 'object' ? (o.label || o.value) : o; return `<option value="${esc(v)}" ${String(cur) === String(v) ? 'selected' : ''}>${esc(t)}</option>`; }).join('')}</select>`
|
|
176
|
+
: q.options.map((o) => { const v = typeof o === 'object' ? o.value : o; const t = typeof o === 'object' ? (o.label || o.value) : o; return `<label class="wl-label"><input type="radio" name="q-${esc(q.id)}" data-testid="question-option" data-id="${esc(q.id)}" value="${esc(v)}" ${String(cur) === String(v) ? 'checked' : ''} ${skipped ? 'disabled' : ''} /> ${esc(t)}</label>`; }).join('')
|
|
177
|
+
+ (q.free ? `<label class="wl-label wl-free">${esc(q.free)} <input type="text" class="wl-input" data-testid="question-input" data-id="${esc(q.id)}" placeholder="KR-A1 or health:upkeep" value="${q.options.some((o) => String(typeof o === 'object' ? o.value : o) === String(cur)) ? '' : esc(cur)}" ${skipped ? 'disabled' : ''} /></label>` : ''))
|
|
178
|
+
: `<input type="text" class="wl-input" data-testid="question-input" data-id="${esc(q.id)}" value="${esc(cur)}" ${skipped ? 'disabled' : ''} />`;
|
|
179
|
+
return `<section class="wl-card wl-q" data-testid="question" data-id="${esc(q.id)}"><p class="wl-p"><strong>${esc(q.text)}</strong></p>${input}
|
|
180
|
+
${q.options && q.options.length ? '' : `<p class="wl-p wl-muted">Default: ${esc(q.default)}</p>`}<button type="button" class="wl-btn wl-small" data-testid="question-skip" data-id="${esc(q.id)}" aria-pressed="${skipped}">${skipped ? 'Skipped. Use the default' : 'Skip'}</button></section>`;
|
|
181
|
+
}).join('');
|
|
182
|
+
const c = counts();
|
|
183
|
+
return `<h1 class="wl-h1">A few questions</h1><p class="wl-p wl-muted">The scan could not decide these. Each has a default, and you can skip any of them.</p>${qs}
|
|
184
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="questions-continue" ${S.busy ? 'disabled' : ''}>${S.busy === 'apply' ? spin('Importing') : `Import ${plural(c.n, 'initiative', 'initiatives')} into ${plural(c.m, 'board', 'boards')}`}</button></div>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function viewBriefing() {
|
|
188
|
+
const items = S.briefing.map((b) => `<li data-testid="briefing-item"><strong>${esc(b.title)}</strong>${b.why ? `<div class="wl-muted">${esc(b.why)}</div>` : ''}${b.initiative ? `<div class="wl-muted" data-testid="briefing-initiative">${esc(b.initiative.title)} · board ${esc(b.initiative.board)}</div>` : ''}<div class="wl-ref">${esc(ref((b.evidence || [])[0]))}</div></li>`).join('');
|
|
189
|
+
return `<h1 class="wl-h1">Your first Briefing</h1>
|
|
190
|
+
<p class="wl-p wl-muted">${S.briefing.length ? 'These need you first.' : 'Nothing needs you right now.'}</p>
|
|
191
|
+
<section class="wl-card">${items ? `<ol class="wl-findings">${items}</ol>` : '<p class="wl-p wl-muted" data-testid="no-briefing">Your portfolio is in. Open PMOS to see it.</p>'}</section>
|
|
192
|
+
<div class="wl-actions"><button type="button" class="wl-btn wl-primary" data-testid="sources-continue">Continue</button>
|
|
193
|
+
<button type="button" class="wl-btn wl-quiet" data-testid="undo-link" ${S.busy ? 'disabled' : ''}>${S.busy === 'undo' ? spin('Undoing') : 'Undo this import'}</button></div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sourceRow(x) {
|
|
197
|
+
const st = S.sources[x.source] || {};
|
|
198
|
+
const busy = S.busy === 'source:' + x.source;
|
|
199
|
+
const state = st.started ? '<span class="wl-muted" data-testid="source-started">Started in an agent session. Follow it in PMOS.</span>'
|
|
200
|
+
: st.skipped ? '<span class="wl-muted" data-testid="source-skipped">Skipped. You can bring it in later from Settings.</span>'
|
|
201
|
+
: `<button type="button" class="wl-btn" data-testid="source-bring" data-id="${esc(x.source)}" ${S.busy ? 'disabled' : ''}>${busy ? spin('Starting') : 'Bring in'}</button>
|
|
202
|
+
<button type="button" class="wl-btn wl-quiet" data-testid="source-skip" data-id="${esc(x.source)}" ${S.busy ? 'disabled' : ''}>Skip</button>`;
|
|
203
|
+
return `<li class="wl-source" data-testid="source-row" data-id="${esc(x.source)}"><div class="wl-source-text"><strong>${esc(x.name)}</strong><div class="wl-muted">${esc(x.adds)}</div>${st.error ? `<div class="wl-error" role="alert">${esc(st.error)}</div>` : ''}</div><div class="wl-source-actions">${state}</div></li>`;
|
|
204
|
+
}
|
|
205
|
+
function viewSources() {
|
|
206
|
+
return `<h1 class="wl-h1">Bring in more sources</h1>
|
|
207
|
+
<p class="wl-p wl-muted">Your folder is in. These sources are optional. Each one runs in an agent session with your own connection, reads only, and writes nothing to the tool. Nothing is read until you press Bring in.</p>
|
|
208
|
+
<section class="wl-card"><ul class="wl-sources">${MORE_SOURCES.map(sourceRow).join('')}</ul>
|
|
209
|
+
<p class="wl-p wl-muted" data-testid="sources-not-yet">Inbox and Slack have no import yet.</p></section>
|
|
210
|
+
<div class="wl-actions"><a class="wl-btn wl-primary" data-testid="open-pmos" href="/c3.html?ws=${encodeURIComponent(S.ws)}">Open PMOS</a></div>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const VIEWS = { welcome: viewWelcome, look: viewLook, portfolio: viewPortfolio, questions: viewQuestions, briefing: viewBriefing, sources: viewSources };
|
|
214
|
+
|
|
215
|
+
function render() {
|
|
216
|
+
const keepScroll = window.scrollY;
|
|
217
|
+
const stepsBar = `<ol class="wl-steps" data-testid="steps">${STEPS.filter(([k]) => k !== 'questions' || S.step === 'questions' || (S.structure && (S.structure.questions || []).length))
|
|
218
|
+
.map(([k, t]) => `<li ${k === S.step ? 'aria-current="step"' : ''} data-step="${k}">${t}</li>`).join('')}</ol>`;
|
|
219
|
+
const a = document.activeElement;
|
|
220
|
+
const was = a && root.contains(a) ? [a.getAttribute('data-testid'), a.getAttribute('data-id')] : null;
|
|
221
|
+
root.setAttribute('data-step', S.step);
|
|
222
|
+
root.innerHTML = stepsBar + (S.error ? `<div class="wl-error" role="alert" data-testid="error">${esc(S.error)}</div>` : '') + VIEWS[S.step]();
|
|
223
|
+
window.scrollTo(0, keepScroll);
|
|
224
|
+
if (was && was[0]) { // keep the keyboard where it was after a re-render
|
|
225
|
+
const again = [...root.querySelectorAll('[data-testid="' + was[0] + '"]')].find((el) => (el.getAttribute('data-id') || null) === (was[1] || null));
|
|
226
|
+
if (again && !again.disabled) again.focus();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ── events (one delegate; the markup is rebuilt on each render) ─────────────────────────────────────────────────── */
|
|
231
|
+
const idOf = (el) => el.getAttribute('data-id');
|
|
232
|
+
const bringIn = (source) => run('source:' + source, async () => {
|
|
233
|
+
const r = await pmPost('/api/sources/refresh', { ws: S.ws, source });
|
|
234
|
+
S.sources[source] = r && r.ok ? { started: true } : { error: (r && r.error) || 'Starting the import did not work.' };
|
|
235
|
+
});
|
|
236
|
+
document.addEventListener('click', (e) => {
|
|
237
|
+
if (!root.contains(e.target)) return;
|
|
238
|
+
const t = e.target.closest('[data-testid]'); if (!t) return;
|
|
239
|
+
const k = t.getAttribute('data-testid');
|
|
240
|
+
if (k === 'add-folder') addFolder();
|
|
241
|
+
else if (k === 'scan-button') scan();
|
|
242
|
+
else if (k === 'continue-button') toPortfolio();
|
|
243
|
+
else if (k === 'import-button' || k === 'questions-continue') { if (k === 'questions-continue') run('apply', applyNow); else importNow(); }
|
|
244
|
+
else if (k === 'undo-link') undo();
|
|
245
|
+
else if (k === 'sources-continue') { S.step = 'sources'; S.error = ''; render(); }
|
|
246
|
+
else if (k === 'source-bring') bringIn(idOf(t));
|
|
247
|
+
else if (k === 'source-skip') { S.sources[idOf(t)] = { skipped: true }; render(); }
|
|
248
|
+
else if (k === 'not-now') notNow();
|
|
249
|
+
else if (k === 'row-docs-toggle') { S.open[idOf(t)] = !S.open[idOf(t)]; render(); }
|
|
250
|
+
else if (k === 'question-skip') { const id = idOf(t); S.skipped[id] = !S.skipped[id]; render(); }
|
|
251
|
+
});
|
|
252
|
+
root.addEventListener('change', (e) => {
|
|
253
|
+
const t = e.target; const k = t.getAttribute && t.getAttribute('data-testid');
|
|
254
|
+
if (k === 'row-check') { S.checked[idOf(t)] = t.checked; render(); }
|
|
255
|
+
else if (k === 'row-board') { S.boardOf[idOf(t)] = t.value; render(); }
|
|
256
|
+
else if (k === 'question-option' || k === 'question-select') S.answers[idOf(t)] = t.value;
|
|
257
|
+
});
|
|
258
|
+
root.addEventListener('input', (e) => {
|
|
259
|
+
const t = e.target; const k = t.getAttribute && t.getAttribute('data-testid');
|
|
260
|
+
if (k === 'row-title') S.titles[idOf(t)] = t.value;
|
|
261
|
+
else if (k === 'question-input') {
|
|
262
|
+
const id = idOf(t); const q = ((S.structure && S.structure.questions) || []).find((x) => x.id === id);
|
|
263
|
+
S.answers[id] = t.value.trim() ? t.value : (q ? q.default : t.value); // an emptied free answer goes back to the default
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/* ── start ───────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
268
|
+
async function start() {
|
|
269
|
+
render();
|
|
270
|
+
const list = await pmGet('/api/workspaces');
|
|
271
|
+
const all = (list && list.workspaces) || [];
|
|
272
|
+
const mine = all.find((w) => w.id === S.ws) || (!S.ws ? all[0] : null);
|
|
273
|
+
if (!mine) { S.error = 'This folder is not open in PMOS. Start pmos in the folder, then reload.'; render(); return; }
|
|
274
|
+
S.ws = mine.id; S.folder = { name: mine.name, root: mine.root };
|
|
275
|
+
const st = await pmGet(`/api/onboarding/status?ws=${encodeURIComponent(S.ws)}`);
|
|
276
|
+
if (st && st.ok && st.state === 'imported') {
|
|
277
|
+
const live = (st.imports || []).filter((i) => !i.retracted).pop();
|
|
278
|
+
S.importId = live ? live.import_id : '';
|
|
279
|
+
if (S.importId && await loadBriefing() && await loadStructure()) S.step = 'briefing';
|
|
280
|
+
}
|
|
281
|
+
render();
|
|
282
|
+
}
|
|
283
|
+
start();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="wl-page" data-pm-page-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="pmos-nonce" content="" />
|
|
7
|
+
<title>Welcome to PMOS</title>
|
|
8
|
+
<!-- The first-run page. The server fills the nonce meta the way it does for /c3.html, so a press (Scan, Import, Undo)
|
|
9
|
+
carries it. Tokens and fonts are the console's, so light and dark both resolve. The page does not load the console. -->
|
|
10
|
+
<link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
|
|
11
|
+
<link rel="stylesheet" href="/pm/tokens.css" />
|
|
12
|
+
<link rel="stylesheet" href="/pm/components.css" />
|
|
13
|
+
|
|
14
|
+
</head>
|
|
15
|
+
<body class="wl-body">
|
|
16
|
+
<main id="wl-root" class="wl-wrap" data-testid="welcome-root"></main>
|
|
17
|
+
<noscript><p class="wl-wrap">This page needs JavaScript.</p></noscript>
|
|
18
|
+
<script type="module" src="/pm/welcome.js"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
package/workspace/server.js
CHANGED
|
@@ -5547,12 +5547,12 @@ async function handleRequest(req, res) {
|
|
|
5547
5547
|
const built = BUILD_RE.exec(url.pathname);
|
|
5548
5548
|
const p = built ? built[1] : url.pathname;
|
|
5549
5549
|
|
|
5550
|
-
if (p === '/' || p === '/index.html' || p === '/c3.html') {
|
|
5550
|
+
if (p === '/' || p === '/index.html' || p === '/c3.html' || p === '/welcome' || p === '/welcome.html') {
|
|
5551
5551
|
// pm-os-absorb Run 2 (R25, X11): the per-launch nonce goes into the base meta line, nowhere else. no-store, so a
|
|
5552
5552
|
// restarted server never answers a press from a page that holds the previous launch's value. The page code is
|
|
5553
5553
|
// referenced under this launch's build prefix.
|
|
5554
5554
|
const n = pmGuard.launchNonce();
|
|
5555
|
-
return fs.readFile(path.join(PUB, p === '/c3.html' ? 'c3.html' : 'index.html'), 'utf8', (err, html) => {
|
|
5555
|
+
return fs.readFile(path.join(PUB, p === '/c3.html' ? 'c3.html' : (p === '/welcome' || p === '/welcome.html') ? 'welcome.html' : 'index.html'), 'utf8', (err, html) => {
|
|
5556
5556
|
if (err) { res.writeHead(404); res.end('not found'); return; }
|
|
5557
5557
|
let out = withBuild(html);
|
|
5558
5558
|
if (n) out = out.replace('<meta name="' + pmGuard.NONCE_META + '" content="" />', '<meta name="' + pmGuard.NONCE_META + '" content="' + n + '" />');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Initiative onboarding-scan-and-map, slice 2: the first-run routes (in-process) and the /welcome page (browser).",
|
|
3
|
+
"modules": [
|
|
4
|
+
"routes-onboarding.test.js",
|
|
5
|
+
"onboarding-scripts.test.js"
|
|
6
|
+
],
|
|
7
|
+
"panels": [
|
|
8
|
+
"pm-welcome-test.js"
|
|
9
|
+
],
|
|
10
|
+
"seconds": {
|
|
11
|
+
"routes-onboarding.test.js": 6,
|
|
12
|
+
"pm-welcome-test.js": 25,
|
|
13
|
+
"onboarding-scripts.test.js": 8
|
|
14
|
+
}
|
|
15
|
+
}
|