fdeops 3.23.0 → 3.24.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/README.md +133 -174
- package/bin/fde.js +24 -11
- package/bin/lib/fieldbook-client.js +166 -0
- package/bin/lib/render.js +135 -174
- package/bin/lib/trust.js +27 -4
- package/mcp/fdeops-ingest/package.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/fde/SKILL.md +9 -5
- package/skills/fde/references/dashboard.md +5 -5
- package/skills/fde/references/debrief.md +10 -4
- package/skills/fde/references/discover.md +5 -1
- package/skills/fde/references/earn-trust.md +1 -1
- package/skills/fde/references/ingest.md +1 -1
- package/skills/fde/references/land.md +6 -6
- package/skills/fde/references/pick-three.md +1 -1
- package/skills/fde/references/plan.md +8 -0
- package/skills/fde/references/poc.md +1 -1
- package/skills/fde/references/readout.md +4 -2
- package/skills/fde/references/review.md +1 -1
- package/skills/fde/references/score-use-cases.md +1 -1
- package/skills/fde/references/ship.md +4 -2
- package/skills/fde/references/switch-clients.md +2 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Embedded verbatim in the offline fieldbook. No imports, network, or record writes.
|
|
4
|
+
module.exports = function fieldbookClient() {
|
|
5
|
+
const all = selector => Array.from(document.querySelectorAll(selector))
|
|
6
|
+
const views = all('.fb-view')
|
|
7
|
+
const nav = all('.fb-nav')
|
|
8
|
+
const search = document.getElementById('fb-search')
|
|
9
|
+
const main = document.querySelector('.fb-main')
|
|
10
|
+
const status = document.getElementById('fb-status')
|
|
11
|
+
const palette = document.getElementById('fb-palette-dialog')
|
|
12
|
+
const paletteInput = document.getElementById('fb-palette-input')
|
|
13
|
+
const items = all('.fb-palette-item')
|
|
14
|
+
const promptDialog = document.getElementById('fb-prompt-dialog')
|
|
15
|
+
let returnFocus = null
|
|
16
|
+
let active = 0
|
|
17
|
+
const currentId = () => document.querySelector('.fb-view:not([hidden])').id.replace('view-', '')
|
|
18
|
+
function selectView(id, updateHash = true, focus = false) {
|
|
19
|
+
if (id === 'fb-main') { id = 'today'; updateHash = false }
|
|
20
|
+
if (!document.getElementById('view-' + id)) id = 'today'
|
|
21
|
+
views.forEach(v => { v.hidden = v.id !== 'view-' + id })
|
|
22
|
+
nav.forEach(n => {
|
|
23
|
+
const selected = n.dataset.target === id
|
|
24
|
+
n.classList.toggle('active', selected)
|
|
25
|
+
if (selected) n.setAttribute('aria-current', 'page')
|
|
26
|
+
else n.removeAttribute('aria-current')
|
|
27
|
+
})
|
|
28
|
+
if (updateHash && location.hash !== '#' + id) location.hash = id
|
|
29
|
+
main.scrollTop = 0
|
|
30
|
+
if (focus) document.querySelector('.fb-view:not([hidden]) h1').focus()
|
|
31
|
+
}
|
|
32
|
+
const hashId = () => location.hash.slice(1)
|
|
33
|
+
selectView(hashId() || 'today', false)
|
|
34
|
+
window.addEventListener('hashchange', () => selectView(hashId(), false))
|
|
35
|
+
const skip = document.querySelector('.fb-skip')
|
|
36
|
+
if (skip) skip.addEventListener('click', event => {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
main.focus()
|
|
39
|
+
})
|
|
40
|
+
nav.forEach(n => n.addEventListener('click', () => selectView(n.dataset.target, true, true)))
|
|
41
|
+
all('[data-nav]').forEach(n => n.addEventListener('click', () => selectView(n.dataset.nav, true, true)))
|
|
42
|
+
|
|
43
|
+
// Only escaped fragments become markup; engagement strings never become code.
|
|
44
|
+
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
45
|
+
function snippet(raw, term) {
|
|
46
|
+
const i = raw.indexOf(term)
|
|
47
|
+
const start = Math.max(0, i - 24), end = Math.min(raw.length, i + term.length + 40)
|
|
48
|
+
return (start ? '…' : '') + esc(raw.slice(start, i)) + '<mark>' + esc(raw.slice(i, i + term.length)) + '</mark>' + esc(raw.slice(i + term.length, end)) + (end < raw.length ? '…' : '')
|
|
49
|
+
}
|
|
50
|
+
search.addEventListener('input', () => {
|
|
51
|
+
const term = search.value.trim().toLowerCase()
|
|
52
|
+
let count = 0
|
|
53
|
+
nav.filter(n => n.dataset.target !== 'today').forEach(n => {
|
|
54
|
+
const raw = n.dataset.search || ''
|
|
55
|
+
const hit = !term || raw.includes(term)
|
|
56
|
+
n.classList.toggle('hide', !hit)
|
|
57
|
+
if (hit) count++
|
|
58
|
+
const reason = n.querySelector('.fb-nav-reason')
|
|
59
|
+
if (term && hit) reason.innerHTML = snippet(raw, term)
|
|
60
|
+
else reason.textContent = n.dataset.defaultReason || ''
|
|
61
|
+
reason.classList.toggle('hide', !reason.textContent)
|
|
62
|
+
})
|
|
63
|
+
document.getElementById('fb-search-empty').hidden = !term || count > 0
|
|
64
|
+
document.getElementById('fb-search-count').textContent = term ? count + ' matching engagement' + (count === 1 ? '' : 's') : ''
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function toggleTheme() {
|
|
68
|
+
const next = document.documentElement.dataset.fdeTheme === 'dark' ? 'light' : 'dark'
|
|
69
|
+
document.documentElement.dataset.fdeTheme = next
|
|
70
|
+
try { localStorage.setItem('fde-fieldbook-theme', next) } catch (_) {}
|
|
71
|
+
themeLabel()
|
|
72
|
+
}
|
|
73
|
+
function themeLabel() {
|
|
74
|
+
const dark = document.documentElement.dataset.fdeTheme === 'dark'
|
|
75
|
+
document.getElementById('fde-theme-btn').textContent = dark ? 'Light theme' : 'Dark theme'
|
|
76
|
+
}
|
|
77
|
+
themeLabel()
|
|
78
|
+
document.getElementById('fde-theme-btn').addEventListener('click', toggleTheme)
|
|
79
|
+
document.getElementById('fb-print-btn').addEventListener('click', () => window.print())
|
|
80
|
+
|
|
81
|
+
function openDialog(dialog) {
|
|
82
|
+
returnFocus = document.activeElement
|
|
83
|
+
dialog.showModal()
|
|
84
|
+
}
|
|
85
|
+
;[palette, promptDialog].forEach(dialog => {
|
|
86
|
+
dialog.addEventListener('close', () => { if (returnFocus) returnFocus.focus() })
|
|
87
|
+
dialog.addEventListener('click', event => { if (event.target === dialog) dialog.close() })
|
|
88
|
+
// Native modal dialogs isolate background controls; contain Tab explicitly
|
|
89
|
+
// so keyboard focus never escapes into browser chrome between controls.
|
|
90
|
+
dialog.addEventListener('keydown', event => {
|
|
91
|
+
if (event.key !== 'Tab') return
|
|
92
|
+
const controls = Array.from(dialog.querySelectorAll('input, textarea, button')).filter(el => !el.hidden && !el.classList.contains('hide'))
|
|
93
|
+
const first = controls[0], last = controls[controls.length - 1]
|
|
94
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
|
95
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
all('[data-close-dialog]').forEach(button => button.addEventListener('click', () => button.closest('dialog').close()))
|
|
99
|
+
const visibleItems = () => items.filter(it => !it.classList.contains('hide'))
|
|
100
|
+
function highlight() {
|
|
101
|
+
const visible = visibleItems()
|
|
102
|
+
items.forEach(it => it.classList.remove('active'))
|
|
103
|
+
if (visible[active]) visible[active].classList.add('active')
|
|
104
|
+
}
|
|
105
|
+
function filterPalette() {
|
|
106
|
+
const term = paletteInput.value.trim().toLowerCase()
|
|
107
|
+
items.forEach(it => it.classList.toggle('hide', !it.textContent.toLowerCase().includes(term)))
|
|
108
|
+
active = 0
|
|
109
|
+
highlight()
|
|
110
|
+
document.getElementById('fb-palette-empty').hidden = visibleItems().length > 0
|
|
111
|
+
}
|
|
112
|
+
function openPalette() { openDialog(palette); paletteInput.value = ''; filterPalette(); paletteInput.focus() }
|
|
113
|
+
function runItem(it) {
|
|
114
|
+
if (!it) return
|
|
115
|
+
palette.close()
|
|
116
|
+
if (it.dataset.target === '__theme__') toggleTheme()
|
|
117
|
+
else selectView(it.dataset.target, true, true)
|
|
118
|
+
}
|
|
119
|
+
document.getElementById('fb-palette-btn').addEventListener('click', openPalette)
|
|
120
|
+
paletteInput.addEventListener('input', filterPalette)
|
|
121
|
+
items.forEach(it => it.addEventListener('click', () => runItem(it)))
|
|
122
|
+
paletteInput.addEventListener('keydown', event => {
|
|
123
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
124
|
+
event.preventDefault()
|
|
125
|
+
active = Math.max(0, Math.min(visibleItems().length - 1, active + (event.key === 'ArrowDown' ? 1 : -1)))
|
|
126
|
+
highlight()
|
|
127
|
+
if (visibleItems()[active]) visibleItems()[active].scrollIntoView({ block: 'nearest' })
|
|
128
|
+
}
|
|
129
|
+
if (event.key === 'Enter') { event.preventDefault(); runItem(visibleItems()[active]) }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
all('[data-prompt]').forEach(button => button.addEventListener('click', async () => {
|
|
133
|
+
const text = button.dataset.prompt
|
|
134
|
+
try {
|
|
135
|
+
await navigator.clipboard.writeText(text)
|
|
136
|
+
status.textContent = 'Prompt copied. Paste it into your coding agent; review changes before saving.'
|
|
137
|
+
} catch (_) {
|
|
138
|
+
const input = document.getElementById('fb-prompt-text')
|
|
139
|
+
input.value = text
|
|
140
|
+
openDialog(promptDialog)
|
|
141
|
+
input.focus(); input.select()
|
|
142
|
+
}
|
|
143
|
+
}))
|
|
144
|
+
document.addEventListener('keydown', event => {
|
|
145
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
146
|
+
event.preventDefault()
|
|
147
|
+
if (!promptDialog.open) { if (palette.open) palette.close(); else openPalette() }
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (palette.open || promptDialog.open || event.metaKey || event.ctrlKey || event.altKey) return
|
|
151
|
+
const typing = event.target.matches('input, textarea, select') || event.target.isContentEditable
|
|
152
|
+
if (typing) {
|
|
153
|
+
if (event.key === 'Escape') { search.value = ''; search.dispatchEvent(new Event('input')); event.target.blur() }
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
if (event.target.closest('.fb-table-scroll')) return
|
|
157
|
+
if (event.key === '/') { event.preventDefault(); search.focus(); return }
|
|
158
|
+
if (event.key === 'j' || event.key === 'k' || (event.target.closest('.fb-rail') && ['ArrowDown', 'ArrowUp'].includes(event.key))) {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
const ids = nav.filter(n => !n.classList.contains('hide')).map(n => n.dataset.target)
|
|
161
|
+
const delta = event.key === 'j' || event.key === 'ArrowDown' ? 1 : -1
|
|
162
|
+
const next = ids[Math.max(0, Math.min(ids.length - 1, ids.indexOf(currentId()) + delta))]
|
|
163
|
+
if (next) selectView(next)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
package/bin/lib/render.js
CHANGED
|
@@ -243,6 +243,49 @@ strong{font-weight:600}
|
|
|
243
243
|
.fb-palette-hint{margin-left:auto;font-family:'Geist Mono',monospace;font-size:11px;color:var(--ink-faint)}
|
|
244
244
|
@media(max-width:760px){.fb-rail{flex:0 0 220px}.fb-main-inner{padding:20px}}
|
|
245
245
|
@media(max-width:640px){.fb-body{flex-direction:column}.fb-rail{flex:0 0 auto;max-height:40vh;border-right:none;border-bottom:1px solid var(--line)}.fb-main-inner{padding:16px}}
|
|
246
|
+
/* Daily work: evidence is the focal point; chrome stays quiet. */
|
|
247
|
+
:root{--ink-faint:#68665f;--amber:#86530d}
|
|
248
|
+
html[data-fde-theme="dark"]{--ink-faint:#a39f94}
|
|
249
|
+
button:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible,a:focus-visible,[tabindex="0"]:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
|
250
|
+
.fb-h1{font-family:'Geist',system-ui,sans-serif;font-size:32px;letter-spacing:-.035em}
|
|
251
|
+
.fb-h1:focus{outline:none}
|
|
252
|
+
.fb-sec{font-family:inherit;font-size:14px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--ink)}
|
|
253
|
+
.fb-main{min-width:0}.fb-main-inner{max-width:1280px}.fb-top-grid>*{min-width:0}
|
|
254
|
+
.fb-meta-line{line-height:1.6}.fb-eyebrow{font-family:inherit;font-size:12px}
|
|
255
|
+
.fb-snapshot{font-size:11px;color:var(--ink-faint);line-height:1.6;margin-bottom:24px}
|
|
256
|
+
.fb-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:20px}
|
|
257
|
+
.fb-action{padding:9px 13px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);font-size:12px;font-weight:500;cursor:pointer}
|
|
258
|
+
.fb-action:first-child{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
259
|
+
html[data-fde-theme="dark"] .fb-action:first-child{color:#102219}
|
|
260
|
+
.fb-action:hover{border-color:var(--accent)}.fb-actions>span{font-size:11px;color:var(--ink-faint);margin-left:4px}
|
|
261
|
+
.fb-queue-row,.fb-flag-row{border:0;background:transparent;width:100%;text-align:left;color:var(--ink)}
|
|
262
|
+
.fb-queue-row{padding:14px 10px;align-items:center}.fb-queue-slug{font-family:inherit;font-weight:600;color:var(--ink)}
|
|
263
|
+
.fb-flag-row{padding:10px}.fb-flag-text{font-family:inherit;font-size:13px}
|
|
264
|
+
.fb-value-table{min-width:700px;table-layout:fixed;margin:0}.fb-value-table th,.fb-value-table td{padding:14px 12px;overflow-wrap:anywhere;line-height:1.5}
|
|
265
|
+
.fb-value-table thead th{font-family:inherit;text-transform:none;letter-spacing:0;font-size:12px;font-weight:500}
|
|
266
|
+
.fb-value-table tbody th{font-family:inherit;text-transform:none;letter-spacing:0;font-size:13px;color:var(--ink)}
|
|
267
|
+
.fb-value-state{display:block;margin-top:5px;font-size:11px;font-weight:400}
|
|
268
|
+
.fb-table-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:8px;margin-top:12px}
|
|
269
|
+
.fb-evidence-note{font-size:12px;line-height:1.5;color:var(--ink-faint);margin:8px 0}
|
|
270
|
+
.fb-empty-evidence{padding:18px;border:1px dashed var(--line);border-radius:8px;margin-top:12px;font-size:14px}
|
|
271
|
+
.fb-empty-evidence p{color:var(--ink-soft);max-width:65ch;line-height:1.6;margin:6px 0 0}
|
|
272
|
+
.fb-copy-status{font-size:13px;color:var(--accent);line-height:1.5}.fb-copy-status:empty{display:none}
|
|
273
|
+
.fb-dialog{padding:0;width:520px;max-width:calc(100vw - 32px);max-height:80vh;border:1px solid var(--line);border-radius:12px;background:var(--bg);color:var(--ink);box-shadow:var(--shadow)}
|
|
274
|
+
.fb-dialog::backdrop{background:rgba(0,0,0,.45)}
|
|
275
|
+
.fb-dialog-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;font-size:14px;font-weight:600;gap:12px;border-bottom:1px solid var(--line)}
|
|
276
|
+
.fb-dialog-help{padding:0 16px;font-size:13px;line-height:1.6;color:var(--ink-soft)}
|
|
277
|
+
#fb-prompt-text{display:block;width:calc(100% - 32px);margin:16px;padding:12px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);font:13px/1.6 inherit;resize:vertical}
|
|
278
|
+
.fb-palette-item{width:100%;border:0;background:transparent;text-align:left;color:var(--ink)}
|
|
279
|
+
.fb-empty-search{font-size:13px;line-height:1.6;color:var(--ink-soft);padding:12px}.fb-empty-search[hidden]{display:none}
|
|
280
|
+
.sr-only,.fb-skip:not(:focus){position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
|
|
281
|
+
.fb-skip:focus{position:fixed;top:8px;left:8px;z-index:100;background:var(--bg);color:var(--ink);padding:10px}
|
|
282
|
+
.fb-more-body{overflow-x:auto}.fb-title-row{flex-wrap:wrap}.fb-person{flex-wrap:wrap}.fb-person-note{min-width:140px}
|
|
283
|
+
@media(max-width:1100px){.fb-main-inner{padding:24px}.fb-rail{flex-basis:240px}.fb-grid-wrap{grid-template-columns:1fr}.fb-need{display:none}}
|
|
284
|
+
@media(max-width:640px){.fb-header{padding:12px;gap:8px;flex-wrap:wrap}.fb-today{display:none}.fb-brand{margin-right:auto}.fb-tagline{font-family:inherit}.fb-btn{min-height:34px}.fb-rail{max-height:190px;flex-basis:auto}.fb-main-inner{padding:20px 16px}.fb-h1{font-size:28px}.fb-queue-row{flex-wrap:wrap;gap:8px}.fb-queue-slug{flex-basis:calc(100% - 24px)}.fb-queue-action{flex-basis:100%;padding-left:16px}.fb-queue-touched{padding-left:16px}.fb-flag-row{flex-direction:column;gap:4px}.fb-flag-slug{flex-basis:auto}.fb-grid-wrap{grid-template-columns:minmax(0,1fr)}.fb-log{flex-wrap:wrap}.fb-log-text{flex-basis:100%}.fb-hints-spacer{margin-left:0}.fb-snapshot{margin-bottom:18px}.fb-body{min-height:0}.fb-actions>span{width:100%;margin-top:4px}.fb-app{height:100dvh}}
|
|
285
|
+
@media(max-width:390px){.fb-value-table{min-width:0}.fb-person-name,.fb-person-role{flex:1 1 100%}.fb-rail{max-height:150px}.fb-main-inner{padding:16px 12px}}
|
|
286
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;scroll-behavior:auto!important}}
|
|
287
|
+
@media print{@page{margin:12mm}html,body,.fb-app{height:auto;overflow:visible;background:#fff;color:#000}.fb-header,.fb-rail,.fb-actions,.fb-hints,.fb-copy-status,.fb-dialog,.fb-skip{display:none!important}.fb-body,.fb-main{display:block;overflow:visible}.fb-main-inner{padding:0;max-width:none}.fb-top-grid{grid-template-columns:1fr}.fb-table-scroll{overflow:visible}.fb-value-table{min-width:0;table-layout:fixed;font-size:10px}.fb-value-table th,.fb-value-table td{padding:6px}.fb-view[hidden]{display:none!important}.fb-block{break-inside:avoid}.fb-vitals{background:#fff}*{color:#000!important;box-shadow:none!important}}
|
|
288
|
+
|
|
246
289
|
`
|
|
247
290
|
}
|
|
248
291
|
|
|
@@ -253,143 +296,7 @@ strong{font-weight:600}
|
|
|
253
296
|
// command palette are wired the same way the theme toggle already was here -
|
|
254
297
|
// addEventListener, guarded null checks, no inline handlers.
|
|
255
298
|
function dashScript() {
|
|
256
|
-
return
|
|
257
|
-
"(function(){",
|
|
258
|
-
"'use strict';",
|
|
259
|
-
"var SEL_KEY='fde-fieldbook-sel';",
|
|
260
|
-
"function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}",
|
|
261
|
-
"function all(sel){return Array.prototype.slice.call(document.querySelectorAll(sel));}",
|
|
262
|
-
"",
|
|
263
|
-
"var views=all('.fb-view');",
|
|
264
|
-
"var navItems=all('.fb-nav');",
|
|
265
|
-
"var searchInput=document.getElementById('fb-search');",
|
|
266
|
-
"",
|
|
267
|
-
"function selectView(id){",
|
|
268
|
-
" if(!document.getElementById('view-'+id))return;",
|
|
269
|
-
" views.forEach(function(v){v.hidden=(v.id!=='view-'+id);});",
|
|
270
|
-
" navItems.forEach(function(n){n.classList.toggle('active',n.getAttribute('data-target')===id);});",
|
|
271
|
-
" try{localStorage.setItem(SEL_KEY,id);}catch(e){}",
|
|
272
|
-
"}",
|
|
273
|
-
"navItems.forEach(function(n){n.addEventListener('click',function(){selectView(n.getAttribute('data-target'));});});",
|
|
274
|
-
"all('[data-nav]').forEach(function(n){n.addEventListener('click',function(){selectView(n.getAttribute('data-nav'));});});",
|
|
275
|
-
"",
|
|
276
|
-
"var startSel='today';",
|
|
277
|
-
"try{var s=localStorage.getItem(SEL_KEY);if(s&&document.getElementById('view-'+s))startSel=s;}catch(e){}",
|
|
278
|
-
"selectView(startSel);",
|
|
279
|
-
"",
|
|
280
|
-
"// search: filters the rail, shows a match snippet in place of the default reason line",
|
|
281
|
-
"// NOTE: snippetFor() below returns a string built ONLY from esc()-escaped",
|
|
282
|
-
"// fragments plus hardcoded '<mark>' literals - never raw untrusted markup -",
|
|
283
|
-
"// so assigning it via innerHTML cannot inject a tag from engagement content.",
|
|
284
|
-
"function snippetFor(raw,term){",
|
|
285
|
-
" var i=raw.indexOf(term);",
|
|
286
|
-
" if(i<0)return '';",
|
|
287
|
-
" var start=Math.max(0,i-24),end=Math.min(raw.length,i+term.length+40);",
|
|
288
|
-
" var pre=esc(raw.slice(start,i)),hit=esc(raw.slice(i,i+term.length)),post=esc(raw.slice(i+term.length,end));",
|
|
289
|
-
" return (start>0?'\\u2026':'')+pre+'<mark>'+hit+'</mark>'+post+(end<raw.length?'\\u2026':'');",
|
|
290
|
-
"}",
|
|
291
|
-
"if(searchInput){searchInput.addEventListener('input',function(){",
|
|
292
|
-
" var term=searchInput.value.trim().toLowerCase();",
|
|
293
|
-
" navItems.forEach(function(n){",
|
|
294
|
-
" if(n.getAttribute('data-target')==='today')return;",
|
|
295
|
-
" var raw=(n.getAttribute('data-search')||'').toLowerCase();",
|
|
296
|
-
" var hit=!term||raw.indexOf(term)>-1;",
|
|
297
|
-
" n.classList.toggle('hide',!hit);",
|
|
298
|
-
" var reason=n.querySelector('.fb-nav-reason');",
|
|
299
|
-
" if(!reason)return;",
|
|
300
|
-
" if(term&&hit){var snip=snippetFor(raw,term);reason.innerHTML=snip;reason.classList.toggle('hide',!snip);}",
|
|
301
|
-
" else{reason.innerHTML=n.getAttribute('data-default-reason')||'';reason.classList.toggle('hide',!reason.innerHTML);}",
|
|
302
|
-
" });",
|
|
303
|
-
"});}",
|
|
304
|
-
"",
|
|
305
|
-
"// keyboard: j/k/arrows move rail selection, / focuses search, Esc blurs/closes",
|
|
306
|
-
"function visibleIds(){return navItems.filter(function(n){return !n.classList.contains('hide');}).map(function(n){return n.getAttribute('data-target');});}",
|
|
307
|
-
"function currentId(){var v=document.querySelector('.fb-view:not([hidden])');return v?v.id.replace('view-',''):'today';}",
|
|
308
|
-
"document.addEventListener('keydown',function(ev){",
|
|
309
|
-
" if((ev.metaKey||ev.ctrlKey)&&(ev.key==='k'||ev.key==='K')){ev.preventDefault();openPalette();return;}",
|
|
310
|
-
" if(paletteIsOpen()){if(ev.key==='Escape'){ev.preventDefault();closePalette();}return;}",
|
|
311
|
-
" var t=ev.target,typing=t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable);",
|
|
312
|
-
" if(typing){if(ev.key==='Escape')t.blur();return;}",
|
|
313
|
-
" if(ev.key==='/'){ev.preventDefault();if(searchInput)searchInput.focus();return;}",
|
|
314
|
-
" var down=ev.key==='ArrowDown'||ev.key==='j',up=ev.key==='ArrowUp'||ev.key==='k';",
|
|
315
|
-
" if(down||up){",
|
|
316
|
-
" ev.preventDefault();",
|
|
317
|
-
" var ids=visibleIds();",
|
|
318
|
-
" var idx=ids.indexOf(currentId());",
|
|
319
|
-
" var next=ids[Math.min(ids.length-1,Math.max(0,idx+(down?1:-1)))];",
|
|
320
|
-
" if(next)selectView(next);",
|
|
321
|
-
" }",
|
|
322
|
-
"});",
|
|
323
|
-
"",
|
|
324
|
-
"// command palette: search-filterable nav (today + each client + theme toggle)",
|
|
325
|
-
"var paletteBackdrop=document.getElementById('fb-palette-backdrop');",
|
|
326
|
-
"var paletteInput=document.getElementById('fb-palette-input');",
|
|
327
|
-
"var paletteInner=document.querySelector('.fb-palette');",
|
|
328
|
-
"var paletteItems=all('.fb-palette-item');",
|
|
329
|
-
"var pActive=0;",
|
|
330
|
-
"function paletteIsOpen(){return !!(paletteBackdrop&&paletteBackdrop.classList.contains('open'));}",
|
|
331
|
-
"function visiblePaletteItems(){return paletteItems.filter(function(it){return !it.classList.contains('hide');});}",
|
|
332
|
-
"function highlightPalette(){",
|
|
333
|
-
" var visible=visiblePaletteItems();",
|
|
334
|
-
" paletteItems.forEach(function(it){it.classList.remove('active');});",
|
|
335
|
-
" if(visible[pActive]){visible[pActive].classList.add('active');visible[pActive].scrollIntoView({block:'nearest'});}",
|
|
336
|
-
"}",
|
|
337
|
-
"function filterPalette(q){",
|
|
338
|
-
" q=q.toLowerCase();",
|
|
339
|
-
" paletteItems.forEach(function(it){",
|
|
340
|
-
" var label=(it.getAttribute('data-label')||'').toLowerCase();",
|
|
341
|
-
" var kind=(it.getAttribute('data-kind')||'').toLowerCase();",
|
|
342
|
-
" it.classList.toggle('hide',!(!q||label.indexOf(q)>-1||kind.indexOf(q)>-1));",
|
|
343
|
-
" });",
|
|
344
|
-
" pActive=0;highlightPalette();",
|
|
345
|
-
"}",
|
|
346
|
-
"function openPalette(){",
|
|
347
|
-
" if(!paletteBackdrop)return;",
|
|
348
|
-
" paletteBackdrop.classList.add('open');",
|
|
349
|
-
" if(paletteInput){paletteInput.value='';paletteInput.focus();}",
|
|
350
|
-
" filterPalette('');",
|
|
351
|
-
"}",
|
|
352
|
-
"function closePalette(){if(paletteBackdrop)paletteBackdrop.classList.remove('open');}",
|
|
353
|
-
"function runActive(){",
|
|
354
|
-
" var it=visiblePaletteItems()[pActive];",
|
|
355
|
-
" if(!it)return;",
|
|
356
|
-
" var target=it.getAttribute('data-target');",
|
|
357
|
-
" if(target==='__theme__')toggleTheme();",
|
|
358
|
-
" else if(target)selectView(target);",
|
|
359
|
-
" closePalette();",
|
|
360
|
-
"}",
|
|
361
|
-
"if(paletteBackdrop)paletteBackdrop.addEventListener('click',function(ev){if(ev.target===paletteBackdrop)closePalette();});",
|
|
362
|
-
"if(paletteInner)paletteInner.addEventListener('click',function(ev){ev.stopPropagation();});",
|
|
363
|
-
"var paletteBtn=document.getElementById('fb-palette-btn');",
|
|
364
|
-
"if(paletteBtn)paletteBtn.addEventListener('click',openPalette);",
|
|
365
|
-
"paletteItems.forEach(function(it){",
|
|
366
|
-
" it.addEventListener('click',function(){pActive=visiblePaletteItems().indexOf(it);runActive();});",
|
|
367
|
-
" it.addEventListener('mouseenter',function(){var v=visiblePaletteItems();var i=v.indexOf(it);if(i>-1){pActive=i;highlightPalette();}});",
|
|
368
|
-
"});",
|
|
369
|
-
"if(paletteInput){",
|
|
370
|
-
" paletteInput.addEventListener('input',function(){filterPalette(paletteInput.value);});",
|
|
371
|
-
" paletteInput.addEventListener('keydown',function(ev){",
|
|
372
|
-
" if(ev.key==='Escape'){closePalette();return;}",
|
|
373
|
-
" if(ev.key==='ArrowDown'){ev.preventDefault();pActive=Math.min(visiblePaletteItems().length-1,pActive+1);highlightPalette();return;}",
|
|
374
|
-
" if(ev.key==='ArrowUp'){ev.preventDefault();pActive=Math.max(0,pActive-1);highlightPalette();return;}",
|
|
375
|
-
" if(ev.key==='Enter'){ev.preventDefault();runActive();}",
|
|
376
|
-
" });",
|
|
377
|
-
"}",
|
|
378
|
-
"",
|
|
379
|
-
"// theme toggle - local only, no network; pre-paint script in <head> avoids a flash",
|
|
380
|
-
"var themeBtn=document.getElementById('fde-theme-btn');",
|
|
381
|
-
"function currentTheme(){return document.documentElement.getAttribute('data-fde-theme')==='dark'?'dark':'light';}",
|
|
382
|
-
"function setThemeLabel(){if(themeBtn)themeBtn.textContent=(currentTheme()==='dark'?'light':'dark');}",
|
|
383
|
-
"function toggleTheme(){",
|
|
384
|
-
" var next=currentTheme()==='dark'?'light':'dark';",
|
|
385
|
-
" try{localStorage.setItem('fde-fieldbook-theme',next);}catch(e){}",
|
|
386
|
-
" document.documentElement.setAttribute('data-fde-theme',next);",
|
|
387
|
-
" setThemeLabel();",
|
|
388
|
-
"}",
|
|
389
|
-
"setThemeLabel();",
|
|
390
|
-
"if(themeBtn)themeBtn.addEventListener('click',toggleTheme);",
|
|
391
|
-
"})();",
|
|
392
|
-
].join('\n')
|
|
299
|
+
return '(' + require('./fieldbook-client').toString() + ')()'
|
|
393
300
|
}
|
|
394
301
|
|
|
395
302
|
// ---- HTML builders (one per fieldbook widget) - kept small and named so the
|
|
@@ -399,7 +306,7 @@ function railItemHtml(e) {
|
|
|
399
306
|
const dotClass = dotClassFor(e.signals.trust)
|
|
400
307
|
const touchedClass = e.quiet ? 't-amber' : 't-faint'
|
|
401
308
|
const preview = e.next.length > 58 ? e.next.slice(0, 56) + '…' : e.next
|
|
402
|
-
const defaultReason = e.
|
|
309
|
+
const defaultReason = escapeHtml((e.attention || [])[0]?.text || preview)
|
|
403
310
|
return `<button class="fb-nav" data-target="eng-${e.slug}" data-search="${e.searchBlob}" data-default-reason="${defaultReason}">
|
|
404
311
|
<div class="fb-nav-row"><span class="dot dot-sm ${dotClass}"></span><span class="fb-nav-name">${inlineMd(e.name)}</span></div>
|
|
405
312
|
<div class="fb-nav-meta">${escapeHtml(e.phaseLabel)} · <span class="${touchedClass}">${escapeHtml(e.signals.updated)}</span></div>
|
|
@@ -410,46 +317,88 @@ function railItemHtml(e) {
|
|
|
410
317
|
function queueRowHtml(e) {
|
|
411
318
|
const dotClass = dotClassFor(e.signals.trust)
|
|
412
319
|
const noAction = !e.hasNext
|
|
413
|
-
const actionText = noAction ? '
|
|
320
|
+
const actionText = noAction ? 'Set the next action with your agent' : e.next
|
|
414
321
|
const touchedClass = e.quiet ? 't-amber' : 't-faint'
|
|
415
|
-
return `<
|
|
322
|
+
return `<button type="button" class="fb-row fb-queue-row" data-nav="eng-${e.slug}">
|
|
416
323
|
<span class="dot dot-md ${dotClass}"></span>
|
|
417
324
|
<span class="fb-queue-slug">${inlineMd(e.name)}</span>
|
|
418
325
|
<span class="fb-queue-action${noAction ? ' t-amber' : ''}">${inlineMd(actionText)}</span>
|
|
419
326
|
<span class="fb-queue-touched ${touchedClass}">${escapeHtml(e.signals.updated)}</span>
|
|
420
|
-
</
|
|
327
|
+
</button>`
|
|
421
328
|
}
|
|
422
329
|
|
|
423
330
|
function flagRowHtml(e, text, colorClass) {
|
|
424
|
-
return `<
|
|
331
|
+
return `<button type="button" class="fb-row fb-flag-row" data-nav="eng-${e.slug}">
|
|
425
332
|
<span class="fb-flag-slug">${inlineMd(e.name)}</span>
|
|
426
333
|
<span class="fb-flag-text t-${colorClass}">${escapeHtml(text)}</span>
|
|
427
|
-
</
|
|
334
|
+
</button>`
|
|
428
335
|
}
|
|
429
336
|
|
|
430
337
|
function todayViewHtml({ today, total, attentionCount, highRiskTotal, todayQueue, flagsHtml, hasEngagements }) {
|
|
431
338
|
if (!hasEngagements) {
|
|
432
339
|
return `<div id="view-today" class="fb-view">
|
|
433
|
-
<div class="fb-eyebrow">
|
|
434
|
-
<h1 class="fb-h1">Today</h1>
|
|
340
|
+
<div class="fb-eyebrow">Your engagements</div>
|
|
341
|
+
<h1 tabindex="-1" class="fb-h1">Today</h1>
|
|
435
342
|
<p class="empty">No engagements yet. Start one with <code>fde resume --init <client-name></code>, then re-run <code>fde dashboard</code>.</p>
|
|
436
343
|
</div>`
|
|
437
344
|
}
|
|
438
345
|
return `<div id="view-today" class="fb-view">
|
|
439
|
-
<div class="fb-eyebrow">
|
|
440
|
-
<h1 class="fb-h1">Today</h1>
|
|
346
|
+
<div class="fb-eyebrow">Your engagements</div>
|
|
347
|
+
<h1 tabindex="-1" class="fb-h1">Today</h1>
|
|
441
348
|
<div class="fb-meta-line">${escapeHtml(today)} · ${total} engagement${total === 1 ? '' : 's'} · ${attentionCount} need you · ${highRiskTotal} high risk${highRiskTotal === 1 ? '' : 's'} open</div>
|
|
442
349
|
<div class="fb-block">
|
|
443
|
-
<div class="fb-sec">
|
|
350
|
+
<div class="fb-sec">Next actions</div>
|
|
444
351
|
${todayQueue}
|
|
445
352
|
</div>
|
|
446
353
|
${flagsHtml ? `<div class="fb-block">
|
|
447
|
-
<div class="fb-sec">
|
|
354
|
+
<div class="fb-sec">Needs attention</div>
|
|
448
355
|
${flagsHtml}
|
|
449
356
|
</div>` : ''}
|
|
450
357
|
</div>`
|
|
451
358
|
}
|
|
452
359
|
|
|
360
|
+
function attentionFor(e) {
|
|
361
|
+
const items = []
|
|
362
|
+
const add = (text, tone = 'amber') => items.push({ text, tone })
|
|
363
|
+
if (e.signals.memoryWarn) add('Record needs repair: ' + e.signals.memoryWarn, 'red')
|
|
364
|
+
if (e.highRisks) add(`${e.highRisks} high risk${e.highRisks === 1 ? '' : 's'} open`, 'red')
|
|
365
|
+
if (e.signals.trust === 'RED' || e.signals.trust === 'amber') add('Check in with the customer: trust is ' + trustWord(e.signals.trust), e.signals.trust === 'RED' ? 'red' : 'amber')
|
|
366
|
+
if (e.signals.trust === 'new') add('No dated trust signal yet - ask someone')
|
|
367
|
+
if (!e.hasNext) add('Set the next action')
|
|
368
|
+
const rows = e.valueRows || []
|
|
369
|
+
const missingEvidence = rows.filter(r => r.evidenceMissing).length
|
|
370
|
+
if (missingEvidence) add(`${missingEvidence} outcome${missingEvidence === 1 ? '' : 's'} missing evidence`)
|
|
371
|
+
const claimed = rows.filter(r => r.state === 'claimed').length
|
|
372
|
+
const unmeasured = rows.filter(r => r.state === 'unmeasured').length
|
|
373
|
+
if (claimed) add(`${claimed} measured outcome${claimed === 1 ? '' : 's'} awaiting acceptance`)
|
|
374
|
+
if (unmeasured) add(`${unmeasured} outcome${unmeasured === 1 ? '' : 's'} not yet measured`)
|
|
375
|
+
if (e.signals.stale) add('Reconfirm the dated trust signal')
|
|
376
|
+
if (e.quiet) add('Record last updated ' + e.signals.updated)
|
|
377
|
+
return items
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function agentActionsHtml(e) {
|
|
381
|
+
const prefix = `@fde Confirm the workspace is bound to ${JSON.stringify(e.name)} before using its records. `
|
|
382
|
+
const actions = [
|
|
383
|
+
['Prepare meeting', 'Prepare me for the next customer meeting. Use the recorded decisions, open risks, delivery evidence and next action. Cite sources and flag unknowns.'],
|
|
384
|
+
['Debrief notes', 'Help me debrief a customer meeting. Ask me for the notes, then show proposed changes and conflicts for review before saving.'],
|
|
385
|
+
['Review outcome', 'Review what we promised, measured and recorded as accepted. Check the evidence and customer approver; do not infer acceptance. Draft a concise customer readout.'],
|
|
386
|
+
]
|
|
387
|
+
return `<div class="fb-actions" aria-label="Continue in your coding agent">${actions.map(([label, prompt]) => `<button type="button" class="fb-action" data-prompt="${escapeHtml(prefix + prompt)}">${label}</button>`).join('')}<span>Copy a prompt into your agent</span></div>`
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function deliveryHtml(e) {
|
|
391
|
+
const rows = e.valueRows || []
|
|
392
|
+
const labels = { unmeasured: 'Not yet measured', claimed: 'Awaiting acceptance', accepted: 'Acceptance recorded' }
|
|
393
|
+
const pending = value => !value || /^(?:pending|tbd|unknown|n\/a|none|awaiting)(?:\b|$)/i.test(value)
|
|
394
|
+
const cell = (value, fallback) => pending(value) ? `<span class="t-faint">${escapeHtml(value || fallback)}</span>` : inlineMd(value)
|
|
395
|
+
return `<section class="fb-block fb-delivery" aria-label="Delivery evidence">
|
|
396
|
+
<div class="fb-sec-row"><h2 class="fb-sec">Delivery evidence</h2><span class="fb-count">${rows.length} outcome${rows.length === 1 ? '' : 's'}</span></div>
|
|
397
|
+
<p class="fb-evidence-note">Recorded in delivery.md. Acceptance reflects the saved record, not independent verification.</p>
|
|
398
|
+
${rows.length ? `<div class="fb-table-scroll" role="region" aria-label="Promised, measured and accepted outcomes" tabindex="0"><table class="fb-table fb-value-table"><thead><tr><th scope="col">Outcome</th><th scope="col">Promised</th><th scope="col">Measured</th><th scope="col">Acceptance</th><th scope="col">Evidence</th></tr></thead><tbody>${rows.map(r => `<tr><th scope="row">${inlineMd(r.slice || 'Outcome')}<span class="fb-value-state t-${r.state === 'accepted' ? 'green' : 'amber'}">${labels[r.state]}</span></th><td>${cell(r.promised, 'Not recorded')}</td><td>${cell(r.measured, 'Not yet measured')}</td><td>${cell(r.accepted, 'Not recorded')}</td><td>${cell(r.evidence, 'Not recorded')}</td></tr>`).join('')}</tbody></table></div>` : `<div class="fb-empty-evidence"><strong>No delivery evidence yet</strong><p>Ask your agent to define one useful increment: the expected result, how to measure it, and who will accept it.</p></div>`}
|
|
399
|
+
</section>`
|
|
400
|
+
}
|
|
401
|
+
|
|
453
402
|
function clientViewHtml(e) {
|
|
454
403
|
const dotClass = dotClassFor(e.signals.trust)
|
|
455
404
|
const sectorLine = [
|
|
@@ -460,8 +409,8 @@ function clientViewHtml(e) {
|
|
|
460
409
|
].filter(Boolean).join(' · ')
|
|
461
410
|
|
|
462
411
|
const nowBlock = `<div class="fb-block">
|
|
463
|
-
<div class="fb-sec">
|
|
464
|
-
<div class="fb-now-action${e.hasNext ? '' : ' t-amber'}">${e.hasNext ? inlineMd(e.next) : 'next action
|
|
412
|
+
<div class="fb-sec">Next action</div>
|
|
413
|
+
<div class="fb-now-action${e.hasNext ? '' : ' t-amber'}">${e.hasNext ? inlineMd(e.next) : 'Set the next action'}</div>
|
|
465
414
|
${e.lastSession ? `<div class="fb-now-session">${inlineMd(e.lastSession)}</div>` : ''}
|
|
466
415
|
</div>`
|
|
467
416
|
|
|
@@ -470,7 +419,7 @@ ${e.lastSession ? `<div class="fb-now-session">${inlineMd(e.lastSession)}</div>`
|
|
|
470
419
|
// contrast disappears. Each gets its own line, its own room to finish a
|
|
471
420
|
// thought, not a race to fit before an ellipsis.
|
|
472
421
|
const whyBlock = (e.brief || e.reality || e.realityMissing) ? `<div class="fb-block">
|
|
473
|
-
<div class="fb-sec">
|
|
422
|
+
<div class="fb-sec">The problem to solve</div>
|
|
474
423
|
${e.brief ? `<p class="fb-why"><span class="t-faint">What they asked for:</span> ${inlineMd(e.brief)}</p>` : ''}
|
|
475
424
|
${e.realityMissing ? `<p class="fb-why fb-why-missing"><span class="fb-accent-label">What's actually true:</span> ${escapeHtml(e.realityMissing)}</p>` : e.reality ? `<p class="fb-why fb-why-reality"><span class="fb-accent-label">What's actually true:</span> ${inlineMd(e.reality)}</p>` : ''}
|
|
476
425
|
</div>` : ''
|
|
@@ -524,7 +473,7 @@ ${e.stakeholders.map(p => `<div class="fb-row fb-person">
|
|
|
524
473
|
</div>` : ''
|
|
525
474
|
|
|
526
475
|
const riskBlock = e.risks.length ? `<div class="fb-block">
|
|
527
|
-
<div class="fb-sec-row"><div class="fb-sec">
|
|
476
|
+
<div class="fb-sec-row"><div class="fb-sec">Open risks</div><span class="fb-count">${e.risks.length} open</span></div>
|
|
528
477
|
<div class="fb-list">
|
|
529
478
|
${e.risks.map(r => `<div class="fb-row fb-risk">
|
|
530
479
|
<span class="fb-risk-sev t-${r.severity === 'high' ? 'red' : 'amber'}">${escapeHtml(r.severity)}</span>
|
|
@@ -534,7 +483,7 @@ ${e.risks.map(r => `<div class="fb-row fb-risk">
|
|
|
534
483
|
</div>` : ''
|
|
535
484
|
|
|
536
485
|
const logBlock = e.log.length ? `<div class="fb-block">
|
|
537
|
-
<div class="fb-sec">
|
|
486
|
+
<div class="fb-sec">Decisions & activity</div>
|
|
538
487
|
<div class="fb-list">
|
|
539
488
|
${e.log.map(g => `<div class="fb-row fb-log">
|
|
540
489
|
<span class="fb-log-date">${escapeHtml(formatLogDate(g.date))}</span>
|
|
@@ -561,15 +510,16 @@ ${e.moreSections.map(s => `<details class="fb-more"><summary class="fb-sec fb-mo
|
|
|
561
510
|
return `<div id="view-eng-${e.slug}" class="fb-view" hidden>
|
|
562
511
|
<div class="fb-eyebrow">.fde/${escapeHtml(e.slug)}</div>
|
|
563
512
|
<div class="fb-title-row">
|
|
564
|
-
<h1 class="fb-h1">${inlineMd(e.name)}</h1>
|
|
513
|
+
<h1 tabindex="-1" class="fb-h1">${inlineMd(e.name)}</h1>
|
|
565
514
|
<span class="fb-trust"><span class="dot dot-lg ${dotClass}"></span><span class="t-${dotClass}">${trustWord(e.signals.trust)}</span></span>
|
|
566
515
|
</div>
|
|
567
516
|
<div class="fb-meta-line">${sectorLine}</div>
|
|
517
|
+
${agentActionsHtml(e)}
|
|
568
518
|
<div class="fb-top-grid">
|
|
569
519
|
<div>${nowBlock}${whyBlock}</div>
|
|
570
520
|
${vitalsBlock}
|
|
571
521
|
</div>
|
|
572
|
-
${pairedBlock}${logBlock}${moreBlock}
|
|
522
|
+
${deliveryHtml(e)}${pairedBlock}${logBlock}${moreBlock}
|
|
573
523
|
</div>`
|
|
574
524
|
}
|
|
575
525
|
|
|
@@ -579,26 +529,26 @@ function paletteItemsHtml(ordered) {
|
|
|
579
529
|
...ordered.map(e => ({ kind: 'open', label: e.slug, hint: trustWord(e.signals.trust), target: 'eng-' + e.slug })),
|
|
580
530
|
{ kind: 'action', label: 'toggle theme', hint: 'light / dark', target: '__theme__' },
|
|
581
531
|
]
|
|
582
|
-
return items.map(it => `<
|
|
532
|
+
return items.map(it => `<button type="button" class="fb-palette-item" data-target="${escapeHtml(it.target)}" data-label="${escapeHtml(it.label)}" data-kind="${escapeHtml(it.kind)}">
|
|
583
533
|
<span class="fb-palette-kind">${escapeHtml(it.kind)}</span>
|
|
584
534
|
<span class="fb-palette-label">${escapeHtml(it.label)}</span>
|
|
585
535
|
<span class="fb-palette-hint">${escapeHtml(it.hint)}</span>
|
|
586
|
-
</
|
|
536
|
+
</button>`).join('\n')
|
|
587
537
|
}
|
|
588
538
|
|
|
589
|
-
function buildFieldbookHtml({ engagements, today }) {
|
|
539
|
+
function buildFieldbookHtml({ engagements, today, generatedAt = '' }) {
|
|
540
|
+
engagements = engagements.map(e => ({ ...e, attention: attentionFor(e) }))
|
|
590
541
|
// rail + Today queue share one order: trust-first (red, amber, green)
|
|
591
542
|
const tierRank = { RED: 0, amber: 1, green: 2, new: 3 }
|
|
592
|
-
const ordered = engagements.slice().sort((a, b) => tierRank[a.signals.trust] - tierRank[b.signals.trust])
|
|
593
|
-
const attentionCount = engagements.filter(e => e.
|
|
543
|
+
const ordered = engagements.slice().sort((a, b) => Number(b.attention.length > 0) - Number(a.attention.length > 0) || tierRank[a.signals.trust] - tierRank[b.signals.trust])
|
|
544
|
+
const attentionCount = engagements.filter(e => e.attention.length > 0).length
|
|
594
545
|
const highRiskTotal = engagements.reduce((n, e) => n + e.highRisks, 0)
|
|
595
546
|
|
|
596
547
|
const railItems = ordered.map(railItemHtml).join('\n')
|
|
597
548
|
const todayQueue = ordered.map(queueRowHtml).join('\n') || '<p class="empty">No engagements yet.</p>'
|
|
598
549
|
const flagRows = []
|
|
599
550
|
ordered.forEach(e => {
|
|
600
|
-
|
|
601
|
-
if (e.highRisks) flagRows.push(flagRowHtml(e, `${e.highRisks} high risk${e.highRisks > 1 ? 's' : ''} open`, 'red'))
|
|
551
|
+
e.attention.forEach(item => flagRows.push(flagRowHtml(e, item.text, item.tone)))
|
|
602
552
|
})
|
|
603
553
|
|
|
604
554
|
const todayView = todayViewHtml({
|
|
@@ -612,47 +562,58 @@ function buildFieldbookHtml({ engagements, today }) {
|
|
|
612
562
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
613
563
|
<script>try{var t=localStorage.getItem("fde-fieldbook-theme");if(t==="dark"||(!t&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches))document.documentElement.setAttribute("data-fde-theme","dark");}catch(e){}</script>
|
|
614
564
|
<title>FDE Fieldbook</title><style>${dashStyles()}</style></head><body>
|
|
565
|
+
<a class="fb-skip" href="#fb-main">Skip to content</a>
|
|
615
566
|
<div class="fb-app">
|
|
616
567
|
<header class="fb-header">
|
|
617
568
|
<div class="fb-brand">
|
|
618
569
|
<span class="fb-wordmark">FDEOPS</span>
|
|
619
|
-
<span class="fb-tagline">
|
|
570
|
+
<span class="fb-tagline">Fieldbook</span>
|
|
620
571
|
${engagements.length ? `<span class="fb-need">${attentionCount}/${engagements.length} need you</span>` : ''}
|
|
621
572
|
</div>
|
|
622
573
|
<div class="fb-spacer"></div>
|
|
623
574
|
<span class="fb-today">${escapeHtml(today)}</span>
|
|
624
|
-
<button id="fb-
|
|
575
|
+
<button id="fb-print-btn" class="fb-btn" type="button">Print view</button>
|
|
576
|
+
<button id="fb-palette-btn" aria-label="Open command palette" class="fb-btn" type="button" title="Command palette (Ctrl/Cmd+K)">⌘K</button>
|
|
625
577
|
<button id="fde-theme-btn" class="fb-btn" type="button">dark</button>
|
|
626
578
|
</header>
|
|
627
579
|
<div class="fb-body">
|
|
628
|
-
<aside class="fb-rail">
|
|
629
|
-
<div class="fb-rail-search"><input id="fb-search" class="fb-search" placeholder="search clients, notes, risks…" spellcheck="false"></div>
|
|
580
|
+
<aside class="fb-rail" aria-label="Engagement navigation">
|
|
581
|
+
<div class="fb-rail-search"><input id="fb-search" aria-label="Search engagements and records" class="fb-search" placeholder="search clients, notes, risks…" spellcheck="false"></div>
|
|
630
582
|
<div class="fb-rail-list fb-scroll">
|
|
631
583
|
<button class="fb-nav" data-target="today">
|
|
632
584
|
<div class="fb-nav-row"><span class="fb-nav-today">today</span>${engagements.length ? `<span class="fb-nav-count">${attentionCount} need you</span>` : ''}</div>
|
|
633
585
|
</button>
|
|
634
586
|
${railItems}
|
|
587
|
+
<p id="fb-search-empty" class="fb-empty-search" hidden>No matching engagements. Try a person, decision, or delivery.</p>
|
|
588
|
+
<span id="fb-search-count" class="sr-only" role="status"></span>
|
|
635
589
|
</div>
|
|
636
590
|
</aside>
|
|
637
|
-
<main class="fb-main fb-scroll">
|
|
591
|
+
<main id="fb-main" tabindex="-1" class="fb-main fb-scroll">
|
|
638
592
|
<div class="fb-main-inner">
|
|
593
|
+
<div class="fb-snapshot">Snapshot generated <time datetime="${escapeHtml(generatedAt)}">${escapeHtml(generatedAt ? generatedAt.replace('T', ' ').replace(/\.\d+Z$/, ' UTC') : today)}</time>. Re-run <code>fde dashboard</code> after updating your records.</div>
|
|
639
594
|
${todayView}
|
|
640
595
|
${clientViews}
|
|
596
|
+
<p id="fb-status" class="fb-copy-status" role="status" aria-live="polite"></p>
|
|
641
597
|
<div class="fb-hints">
|
|
642
598
|
<span><span class="t-soft">↑↓ / j k</span> client</span>
|
|
643
599
|
<span><span class="t-soft">/</span> search</span>
|
|
644
600
|
<span><span class="t-soft">⌘K</span> palette</span>
|
|
645
|
-
<span class="fb-hints-spacer">
|
|
601
|
+
<span class="fb-hints-spacer">Local records. No network. Changes are reviewed in your coding agent.</span>
|
|
646
602
|
</div>
|
|
647
603
|
</div>
|
|
648
604
|
</main>
|
|
649
605
|
</div>
|
|
650
|
-
<
|
|
651
|
-
<div class="fb-
|
|
652
|
-
<input id="fb-palette-input" class="fb-palette-input" placeholder="
|
|
606
|
+
<dialog id="fb-palette-dialog" class="fb-dialog" role="dialog" aria-modal="true" aria-label="Command palette">
|
|
607
|
+
<div class="fb-dialog-head"><span>Jump to an engagement</span><button type="button" class="fb-btn" data-close-dialog>Close</button></div>
|
|
608
|
+
<input id="fb-palette-input" class="fb-palette-input" aria-label="Find an engagement or action" placeholder="Find an engagement or action" spellcheck="false">
|
|
653
609
|
<div class="fb-palette-list fb-scroll">${paletteItems}</div>
|
|
654
|
-
|
|
655
|
-
</
|
|
610
|
+
<p id="fb-palette-empty" class="fb-empty-search" hidden>No matching commands.</p>
|
|
611
|
+
</dialog>
|
|
612
|
+
<dialog id="fb-prompt-dialog" class="fb-dialog" role="dialog" aria-modal="true" aria-labelledby="fb-prompt-title">
|
|
613
|
+
<div class="fb-dialog-head"><span id="fb-prompt-title">Copy into your coding agent</span><button type="button" class="fb-btn" data-close-dialog>Close</button></div>
|
|
614
|
+
<p class="fb-dialog-help">Automatic copy is unavailable. Copy the selected prompt, then paste it into your agent. Review any proposed changes there.</p>
|
|
615
|
+
<textarea id="fb-prompt-text" aria-label="Agent prompt" readonly rows="7"></textarea>
|
|
616
|
+
</dialog>
|
|
656
617
|
</div>
|
|
657
618
|
<script>${dashScript()}</script>
|
|
658
619
|
</body></html>`
|