create-pmos 0.131.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/data-freshness.md +40 -0
- package/workspace/guide/index.md +2 -0
- package/workspace/guide/welcome.md +38 -0
- package/workspace/lib/calendar-cli.js +84 -0
- package/workspace/lib/facts-async.js +144 -35
- package/workspace/lib/freshness.js +167 -0
- package/workspace/lib/import-overlay.js +32 -0
- package/workspace/lib/offloop.js +94 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/route-modules.js +2 -0
- package/workspace/lib/routes-board.js +3 -1
- package/workspace/lib/routes-my-work.js +27 -9
- package/workspace/lib/routes-onboarding.js +215 -0
- package/workspace/lib/routes-sources.js +64 -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/c3-shell.js +4 -1
- package/workspace/public/pm/components.css +313 -0
- package/workspace/public/pm/dest-settings.js +10 -1
- package/workspace/public/pm/freshness-line.js +123 -0
- package/workspace/public/pm/welcome.js +283 -0
- package/workspace/public/welcome.html +20 -0
- package/workspace/server.js +41 -4
- package/workspace/skills/calendar-sync.skill +30 -0
- package/workspace/test-lists/onboarding-firstrun.json +15 -0
- package/workspace/test-lists/workspace-data-freshness.json +6 -0
|
@@ -3499,6 +3499,39 @@
|
|
|
3499
3499
|
.fr-set__envrows { display: flex; flex-direction: column; gap: 6px; }
|
|
3500
3500
|
.fr-set__flags { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; min-width: 0; width: 100%; }
|
|
3501
3501
|
|
|
3502
|
+
/* workspace-data-freshness: the console header's freshness line and Settings › Data freshness (pm/freshness-line.js) */
|
|
3503
|
+
.fr-fresh {
|
|
3504
|
+
display: flex;
|
|
3505
|
+
flex-wrap: wrap;
|
|
3506
|
+
align-items: center;
|
|
3507
|
+
gap: 8px;
|
|
3508
|
+
font-size: 12px;
|
|
3509
|
+
color: var(--muted-foreground);
|
|
3510
|
+
}
|
|
3511
|
+
.fr-fresh__state--fresh { color: var(--foreground); }
|
|
3512
|
+
.fr-fresh__state--stale { color: var(--warning); }
|
|
3513
|
+
.fr-fresh__state--failed { color: var(--destructive); }
|
|
3514
|
+
.fr-fresh__state--never,
|
|
3515
|
+
.fr-fresh__state--building,
|
|
3516
|
+
.fr-fresh__state--off { color: var(--muted-foreground); }
|
|
3517
|
+
.fr-fresh-table__grid {
|
|
3518
|
+
width: 100%;
|
|
3519
|
+
border-collapse: collapse;
|
|
3520
|
+
font-size: 12px;
|
|
3521
|
+
}
|
|
3522
|
+
.fr-fresh-table__grid th {
|
|
3523
|
+
padding: 4px 8px;
|
|
3524
|
+
text-align: left;
|
|
3525
|
+
font-weight: 600;
|
|
3526
|
+
}
|
|
3527
|
+
.fr-fresh-table__grid td {
|
|
3528
|
+
padding: 4px 8px;
|
|
3529
|
+
vertical-align: top;
|
|
3530
|
+
}
|
|
3531
|
+
.fr-fresh-table__watch {
|
|
3532
|
+
margin-top: 8px;
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3502
3535
|
/* -- layouts · .td- Today (Run 3, slice 3B) -- */
|
|
3503
3536
|
.td-board {
|
|
3504
3537
|
display: flex;
|
|
@@ -7932,5 +7965,285 @@
|
|
|
7932
7965
|
containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
|
|
7933
7966
|
(8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
|
|
7934
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
|
+
|
|
7935
8248
|
/* -- layouts · .cal3- the console3 Calendar page (calendar-time-page): its rules sit in console3.css beside the .vday rules they extend -- */
|
|
7936
8249
|
/* -- end of the layout sections: add nothing below this line -- */
|
|
@@ -33,6 +33,7 @@ import { COPY } from './frame-copy.js';
|
|
|
33
33
|
import { askConfirm } from './dialogs.js';
|
|
34
34
|
import { icon } from './icons.js';
|
|
35
35
|
import { flagPicker, flagsLine, flagsMissingValue } from './flag-picker.js';
|
|
36
|
+
import { freshnessTable } from './freshness-line.js';
|
|
36
37
|
|
|
37
38
|
const P = COPY.page;
|
|
38
39
|
const roots = new WeakMap(); // host -> { root, sig }
|
|
@@ -542,6 +543,13 @@ function checkButton(scope) {
|
|
|
542
543
|
});
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
/* ── Data freshness (workspace-data-freshness R9): how old each source is, Refresh, and the change detector switch ── */
|
|
547
|
+
function freshnessSection(scope) {
|
|
548
|
+
const ws = wsOfRepo(scope === 'all' ? null : activeProduct());
|
|
549
|
+
if (!ws) return section('freshness', 'Data freshness', [empty({ text: 'Open a repository to see how old its data is.' })], null, 'refresh-cw');
|
|
550
|
+
return section('freshness', 'Data freshness', [freshnessTable(ws)], null, 'refresh-cw');
|
|
551
|
+
}
|
|
552
|
+
|
|
545
553
|
/* ── mount ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
546
554
|
|
|
547
555
|
function signature(scope) {
|
|
@@ -583,7 +591,8 @@ export function mountDestination(host, ctx) {
|
|
|
583
591
|
peopleSection(scope),
|
|
584
592
|
onlineSection(),
|
|
585
593
|
themeSection(),
|
|
586
|
-
updatesSection(scope)
|
|
594
|
+
updatesSection(scope),
|
|
595
|
+
freshnessSection(scope)),
|
|
587
596
|
]);
|
|
588
597
|
}
|
|
589
598
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/* pm/freshness-line.js — how old each source of the workspace is (workspace-data-freshness R9).
|
|
2
|
+
*
|
|
3
|
+
* freshnessLine(ws) → element the console header's short line: the sources that are old or never read, else
|
|
4
|
+
* "Up to date"; a Refresh button for the oldest one. Updates itself every 15 s.
|
|
5
|
+
* freshnessTable(ws) → element Settings › Data freshness: every source, when it was read, its state, its schedule,
|
|
6
|
+
* Refresh, and the repository's Watch for changes switch.
|
|
7
|
+
*
|
|
8
|
+
* Both read GET /api/sources (lib/routes-sources.js). The presses go through pmPost (the page nonce), so an agent cannot
|
|
9
|
+
* make them. When a workspace's data version moves, the line sends `pmos:data-changed` with { ws } on window, so a view
|
|
10
|
+
* that wants to read again only on a change can listen for it. One element per ws and kind, kept across redraws, so a
|
|
11
|
+
* page that rebuilds its DOM does not start a second poll.
|
|
12
|
+
*/
|
|
13
|
+
import { el, setChildren } from './dom.js';
|
|
14
|
+
import { pmGet, pmPost } from './http.js';
|
|
15
|
+
import { button, SEL } from './primitives.js';
|
|
16
|
+
|
|
17
|
+
const POLL_MS = 15000;
|
|
18
|
+
const live = new Map(); // kind|ws -> { node, timer, version }
|
|
19
|
+
|
|
20
|
+
const hhmm = (iso) => { const d = new Date(iso); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); };
|
|
21
|
+
function ago(iso) {
|
|
22
|
+
if (!iso) return 'never read';
|
|
23
|
+
const ms = Date.now() - Date.parse(iso);
|
|
24
|
+
if (ms < 60000) return 'just now';
|
|
25
|
+
if (ms < 3600000) return Math.round(ms / 60000) + ' min ago';
|
|
26
|
+
if (ms < 24 * 3600000) return 'today ' + hhmm(iso);
|
|
27
|
+
return Math.round(ms / (24 * 3600000)) + ' d ago';
|
|
28
|
+
}
|
|
29
|
+
const STATE_WORD = { fresh: 'Up to date', stale: 'Old', never: 'Never read', building: 'Reading…', failed: 'Failed', off: 'Off' };
|
|
30
|
+
// the colour of a state: a class in components.css (.fr-fresh__state--<state>), coloured from the tokens
|
|
31
|
+
const tone = (state) => 'fr-fresh__state fr-fresh__state--' + (STATE_WORD[state] ? state : 'never');
|
|
32
|
+
const label = (b, t) => { const l = b.querySelector(SEL.btnLabel); (l || b).textContent = t; };
|
|
33
|
+
|
|
34
|
+
async function read(ws) {
|
|
35
|
+
const r = await pmGet('/api/sources' + (ws ? '?ws=' + encodeURIComponent(ws) : ''));
|
|
36
|
+
return r && r.ok ? r : null;
|
|
37
|
+
}
|
|
38
|
+
async function refresh(ws, source, btn) {
|
|
39
|
+
if (btn) { btn.disabled = true; label(btn, 'Starting…'); }
|
|
40
|
+
const r = await pmPost('/api/sources/refresh', { ws, source });
|
|
41
|
+
if (btn) label(btn, r && r.ok ? (r.started === 'session' ? 'Agent started' : 'Reading…') : 'Could not start');
|
|
42
|
+
return r;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function poll(key, ws, draw) {
|
|
46
|
+
let cur = live.get(key);
|
|
47
|
+
if (cur) return cur.node;
|
|
48
|
+
cur = { node: null, timer: null, version: null };
|
|
49
|
+
live.set(key, cur);
|
|
50
|
+
const tick = async () => {
|
|
51
|
+
const d = await read(ws).catch(() => null);
|
|
52
|
+
if (d) {
|
|
53
|
+
if (cur.version != null && d.version !== cur.version) {
|
|
54
|
+
try { window.dispatchEvent(new CustomEvent('pmos:data-changed', { detail: { ws: d.ws } })); } catch { /* no window */ }
|
|
55
|
+
}
|
|
56
|
+
cur.version = d.version;
|
|
57
|
+
}
|
|
58
|
+
draw(cur.node, d);
|
|
59
|
+
};
|
|
60
|
+
cur.tick = tick;
|
|
61
|
+
cur.node = draw(null, null);
|
|
62
|
+
tick();
|
|
63
|
+
cur.timer = setInterval(() => { if (!cur.node.isConnected && cur.node.dataset.mounted === '1') { clearInterval(cur.timer); live.delete(key); return; } if (cur.node.isConnected) cur.node.dataset.mounted = '1'; tick(); }, POLL_MS);
|
|
64
|
+
return cur.node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The header line: quiet when everything is fresh, amber names when a source is old or never read. */
|
|
68
|
+
export function freshnessLine(ws) {
|
|
69
|
+
return poll('line|' + ws, ws, (node, d) => {
|
|
70
|
+
const n = node || el('div', { class: 'fr-fresh', 'data-c3pg': 'freshness' });
|
|
71
|
+
if (!d) { if (!node) n.textContent = ''; return n; }
|
|
72
|
+
const watched = d.sources.filter((s) => s.state !== 'off');
|
|
73
|
+
const behind = watched.filter((s) => s.state === 'stale' || s.state === 'failed' || (s.state === 'never' && s.schedule));
|
|
74
|
+
const building = watched.find((s) => s.state === 'building');
|
|
75
|
+
const kids = [];
|
|
76
|
+
if (!behind.length) kids.push(el('span', { class: tone('fresh') }, building ? 'Reading the repository…' : 'Data up to date'));
|
|
77
|
+
for (const s of behind.slice(0, 3)) kids.push(el('span', { class: tone(s.state), title: STATE_WORD[s.state] }, s.label + ' ' + ago(s.as_of)));
|
|
78
|
+
if (behind.length) {
|
|
79
|
+
const first = behind[0];
|
|
80
|
+
const btn = button({ label: 'Refresh ' + first.label, size: 'sm', attrs: { 'data-c3pg': 'freshness-refresh' },
|
|
81
|
+
onClick: () => refresh(d.ws, first.id, btn) });
|
|
82
|
+
kids.push(btn);
|
|
83
|
+
}
|
|
84
|
+
setChildren(n, kids);
|
|
85
|
+
return n;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The Settings table: every source of the workspace, with Refresh, and the change detector switch. */
|
|
90
|
+
export function freshnessTable(ws) {
|
|
91
|
+
return poll('table|' + ws, ws, (node, d) => {
|
|
92
|
+
const n = node || el('div', { class: 'fr-fresh-table', 'data-fr': 'freshness' });
|
|
93
|
+
if (!d) { if (!node) n.textContent = 'Reading…'; return n; }
|
|
94
|
+
const th = (t) => el('th', null, t);
|
|
95
|
+
const td = (...k) => el('td', null, ...k);
|
|
96
|
+
const rows = d.sources.map((s) => {
|
|
97
|
+
const btn = button({ label: 'Refresh', size: 'sm', disabled: s.state === 'off' || s.state === 'building',
|
|
98
|
+
attrs: { 'data-fr': 'freshness-refresh-' + s.id }, onClick: () => refresh(d.ws, s.id, btn) });
|
|
99
|
+
const sched = s.schedule ? [s.schedule.cadence, s.schedule.day, s.schedule.time].filter(Boolean).join(' ') : (s.skill ? 'no schedule' : 'on every change');
|
|
100
|
+
return el('tr', { 'data-fr-source': s.id },
|
|
101
|
+
td(s.label), td(s.id === 'repository' && s.state === 'fresh' ? 'watching' : s.as_of ? ago(s.as_of) : 'never'), td(el('span', { class: tone(s.state) }, STATE_WORD[s.state] || s.state)),
|
|
102
|
+
td(sched), td(btn));
|
|
103
|
+
});
|
|
104
|
+
const repo = d.sources.find((s) => s.id === 'repository');
|
|
105
|
+
const on = !repo || repo.state !== 'off';
|
|
106
|
+
// a button, not a checkbox: on a phone every control is 44px high (R27), and a checkbox box is not
|
|
107
|
+
const sw = button({ label: on ? 'Stop watching this repository' : 'Watch this repository for changes', size: 'sm',
|
|
108
|
+
attrs: { 'data-fr': 'freshness-watch', 'aria-pressed': on ? 'true' : 'false', class: 'fr-fresh-table__watch' },
|
|
109
|
+
onClick: async () => {
|
|
110
|
+
sw.disabled = true;
|
|
111
|
+
const r = await pmPost('/api/sources/watch', { ws: d.ws, on: !on });
|
|
112
|
+
if (r && r.ok) { const c = live.get('table|' + ws); if (c) c.tick(); } else { sw.disabled = false; label(sw, 'Could not save'); }
|
|
113
|
+
} });
|
|
114
|
+
setChildren(n, [
|
|
115
|
+
el('table', { class: 'fr-fresh-table__grid' },
|
|
116
|
+
el('thead', null, el('tr', null, th('Source'), th('Read'), th('State'), th('Schedule'), th(''))),
|
|
117
|
+
el('tbody', null, ...rows)),
|
|
118
|
+
sw,
|
|
119
|
+
el('p', { class: 'fr-set__note' }, 'The workspace holds no password or token. Calendar, Jira and My work are read by an agent session with your own connection.'),
|
|
120
|
+
]);
|
|
121
|
+
return n;
|
|
122
|
+
});
|
|
123
|
+
}
|