@lovek4d/kickflip 0.5.0 → 0.6.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/CONTEXT.md +105 -68
- package/README.md +1 -1
- package/SKILL.md +27 -21
- package/bin/kickflip.mjs +27 -18
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -0
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +199 -79
- package/board/build-script.mjs +2 -1
- package/board/domain.mjs +9 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +9 -4
- package/board/on-host.mjs +1 -1
- package/board/onboarding.mjs +11 -0
- package/board/prototype-base.mjs +1 -0
- package/board/prototype-page.mjs +1 -0
- package/board/public/app-arch.js +1 -1
- package/board/public/app-asks.js +6 -6
- package/board/public/app-banner.js +7 -13
- package/board/public/app-cards.js +75 -37
- package/board/public/app-core.js +26 -8
- package/board/public/app-header.js +8 -8
- package/board/public/app-lanes.js +67 -58
- package/board/public/app-needs.js +140 -10
- package/board/public/app-reader.js +1 -1
- package/board/public/app-snapshot.js +46 -72
- package/board/public/app-threads.js +78 -75
- package/board/public/app.html +9 -4
- package/board/public/app.js +114 -38
- package/board/public/attach.js +2 -2
- package/board/public/dialogs.js +9 -39
- package/board/public/hub-kit.js +66 -4
- package/board/public/hub.css +2 -0
- package/board/public/linear-key.js +46 -35
- package/board/public/new-task.js +20 -39
- package/board/public/progress-review.html +46 -0
- package/board/public/progress-review.js +158 -0
- package/board/public/prototype-kit.js +137 -7
- package/board/public/prototype-switcher.js +152 -123
- package/board/public/prototype.html +57 -3
- package/board/public/router.js +7 -6
- package/board/public/shell.js +68 -26
- package/board/public/sidebar.css +1 -0
- package/board/public/sidebar.js +16 -30
- package/board/public/style.css +205 -83
- package/board/public/ui-cards.js +91 -65
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-header.js +27 -18
- package/board/public/ui-hub.js +12 -1
- package/board/public/ui-links.js +7 -7
- package/board/public/ui-order.js +12 -10
- package/board/public/ui-page-header.js +19 -7
- package/board/public/ui-patch.js +7 -33
- package/board/public/ui-progress-bar.js +28 -0
- package/board/public/ui-questions.js +47 -52
- package/board/public/ui-sidebar.js +34 -26
- package/board/public/ui-snapshot.js +43 -52
- package/board/public/ui-threads.js +59 -30
- package/board/public/ui.js +125 -18
- package/board/public/view-hub-settings.js +22 -67
- package/board/public/view-hub.js +19 -4
- package/board/public/view-notes.js +27 -7
- package/board/public/view-questions.js +205 -240
- package/board/public/view-settings.js +313 -76
- package/board/render-check.mjs +10 -0
- package/board/review.mjs +1 -1
- package/board/server.mjs +19 -20
- package/board/stop.mjs +1 -0
- package/board/store.mjs +29 -15
- package/board/sync.mjs +2 -0
- package/board/tickets.mjs +3 -3
- package/board/transcript.mjs +2 -0
- package/board/worktree.mjs +25 -20
- package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
- package/hooks-plugin/hooks/hooks.json +9 -0
- package/hooks-plugin/hooks/usage-gate.mjs +20 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +5 -5
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/linear.mjs +1 -2
- package/hub/live.mjs +2 -2
- package/hub/locks.mjs +1 -1
- package/hub/owner-checks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/presence.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +1 -2
- package/hub/review-send.mjs +1 -0
- package/hub/secrets.mjs +2 -0
- package/hub/self-restart.mjs +2 -0
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -0
- package/hub/slack.mjs +2 -2
- package/hub/stream.mjs +10 -13
- package/hub/try.mjs +7 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +2 -1
- package/shared/audit.mjs +4 -0
- package/shared/card-prs.mjs +1 -1
- package/shared/fake-gh.mjs +17 -4
- package/shared/machine-gate.mjs +2 -0
- package/shared/prototype-frame.js +1 -0
- package/shared/service.mjs +4 -4
- package/shared/usage-gate.mjs +1 -1
- package/board/public/ui-phase-bars.js +0 -17
package/board/public/view-hub.js
CHANGED
|
@@ -39,8 +39,12 @@
|
|
|
39
39
|
const { row, tile } = HubUI;
|
|
40
40
|
const fixHtml = (fix) => fix.command ? `<code>${esc(fix.command)}</code>` : fix.settings
|
|
41
41
|
? `${esc(fix.text)} <button type="button" class="link" data-settings-open="${esc(fix.settings)}">${esc(fix.label)}</button>.` : esc(fix.text);
|
|
42
|
+
// One line per device with notifications on: kind, browser, last seen, and a Remove that forgets it.
|
|
43
|
+
const mine = (() => { try { return localStorage.getItem('kickflip.push.id'); } catch { return null; } })();
|
|
44
|
+
const deviceLine = (d) => `<span class="rd-device">${esc([d.kind, d.browser, `seen ${BoardUI.ago(d.added)}`].filter(Boolean).join(' · '))}${d.id === mine ? ' · <b>this device</b>' : ''} <button type="button" class="link" data-forget-device="${esc(d.id)}">Remove</button></span>`;
|
|
45
|
+
const devices = (it) => it.devices?.length ? `<span class="links">${it.devices.map(deviceLine).join('')}</span>` : '';
|
|
42
46
|
const rows = integrations.map((it) => row({ icon: tile(it.id, it.ready ? 'var(--color-quiet)' : 'var(--color-notice)'), title: esc(it.name), wrap: true,
|
|
43
|
-
sub: it.ready ? `<span class="st ok">ready</span> · ${esc(it.detail)}`
|
|
47
|
+
sub: it.ready ? `<span class="st ok">ready</span> · ${esc(it.detail)}${devices(it)}`
|
|
44
48
|
: `<span class="st wait">not ready</span> · ${esc(it.missing)}<span class="links"><span class="rd-fix">${fixHtml(it.fix)}</span>${it.waits ? `<span class="rd-waits">${esc(it.waits)}</span>` : ''}</span>` })).join('');
|
|
45
49
|
return `<h2 class="label" id="integrations">Integrations</h2><div class="group">${rows}</div>
|
|
46
50
|
<p class="note">A feature that needs an integration stays off until it’s ready, and says why.</p>
|
|
@@ -67,13 +71,14 @@
|
|
|
67
71
|
return `${HubUI.flash(flash)}${HubUI.pauseBanner(d.pause)}${HubUI.holdBanner(d.hold)}${usage}${promptsHtml(d.prompts || [], prompts)}<h2 class="label"${usage ? '' : ' style="margin-top:4px"'}>Health</h2>
|
|
68
72
|
<div class="group">${row({ icon: tile('power', { ok: '#22c55e', wait: '#f59e0b' }[d.hub.tone] || '#ef4444'), title: 'Hub', sub: `${word} ·${esc(d.hub.detail)}${d.awake ? `<span class="links"><span><span class="awake-on">Keeping awake</span> ${esc(d.awake)}</span></span>` : ''}`, wrap: true,
|
|
69
73
|
trail: d.hub.restart ? restartHubButton(restartingHub) : '' })}
|
|
74
|
+
${HubUI.machineRow(d.pause.machine)}
|
|
70
75
|
${d.empty ? row({ title: 'No sites yet', sub: 'In a Claude session, run <code>/kickflip <route></code>.' }) : ''}</div>
|
|
71
76
|
${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembered?.length ? `<h2 class="label">Sessions without a board</h2><div class="group">${rememberedHtml(d.remembered)}</div>` : ''}<p class="note">The hub runs every site and restarts one that stops. A board's links are what the hub owes it: GitHub polling, the fetch of its repo, events (its session and a watcher run, or the hub can wake it), daily wakes and reviewer links. One that just broke reads settling first.</p>
|
|
72
77
|
<h2 class="label">Polling status</h2>
|
|
73
78
|
<div class="group">${nowRow(d.polling, tile('clock', d.polling.count ? 'var(--color-notice)' : 'var(--color-quiet)'))}</div>`;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
const footer = (d) => `<footer>${esc(d.sites)}. Each belongs to one Claude session, which adds it with <code>/kickflip <route></code>. This page refreshes every 30 seconds.<br><span class="host">${esc(d.host)}</span></footer>`;
|
|
81
|
+
const footer = (d) => `<footer>${esc(d.sites)}. Each belongs to one Claude session, which adds it with <code>/kickflip <route></code>. This page refreshes every 30 seconds.<br><span class="host">${esc(d.host)}</span><br><span class="brand"><a href="https://kickflip.sh">kickflip.sh</a> / v${esc(d.version)}</span></footer>`;
|
|
77
82
|
|
|
78
83
|
// iOS shows an app badge only in a Home Screen app with notifications allowed; elsewhere this does nothing.
|
|
79
84
|
function badge(n) {
|
|
@@ -84,7 +89,7 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
84
89
|
(globalThis.AppViews ||= {}).hub = {
|
|
85
90
|
className: 'hub-view view-hub',
|
|
86
91
|
title: () => 'Hub',
|
|
87
|
-
markup: () => `${BoardUI.header({ lead: 'logo', title: 'Hub', time: 'now', logoSrc: '/_hub/kickflip.png'
|
|
92
|
+
markup: () => `${BoardUI.header({ lead: 'logo', title: 'Hub', time: 'now', logoSrc: '/_hub/kickflip.png' })}
|
|
88
93
|
<main class="has-top"><div class="hub-status"></div><section class="hub-settings" aria-labelledby="hubSettingsLabel"><h2 class="label" id="hubSettingsLabel">Hub settings</h2>${HubSettings.markup()}</section><div class="hub-foot"></div></main>`,
|
|
89
94
|
|
|
90
95
|
create: (root, siteRoot, ctx) => BoardView.page(root, siteRoot, (B) => {
|
|
@@ -109,8 +114,11 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
109
114
|
if (!r.ok || !out) throw new Error();
|
|
110
115
|
if (B.alive()) B.toast(`${out.title} is back up, and its session is listening.`);
|
|
111
116
|
} catch { flash = { text: out?.error || `Could not restart ${title}. Is the host awake?`, error: true }; }
|
|
117
|
+
// The row keeps its Restart busy until the hub's data shows the board's new word, and only a refusal loads after
|
|
118
|
+
// (docs/adr/2026-11-05-0200-a-tap-spins-until-the-board-shows-its-change.md).
|
|
119
|
+
if (!flash && B.alive()) await BoardUI.pending.within(load(), () => { B.toast('Still waiting on the board'); load(); });
|
|
112
120
|
restarting.delete(route);
|
|
113
|
-
if (B.alive()) { paint(); load(); }
|
|
121
|
+
if (B.alive()) { paint(); if (flash) load(); }
|
|
114
122
|
}
|
|
115
123
|
// Restart hub asks the host to restart the hub service (POST /_hub/restart-hub), then waits for the hub to answer
|
|
116
124
|
// with other code than it ran (its x-hub-code) and reloads the app; past HUB_WAIT tries it says how to restart by hand.
|
|
@@ -145,7 +153,14 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
|
|
|
145
153
|
flash = { text: `The hub hasn’t come back. On the host, run ${out.command}`, error: true };
|
|
146
154
|
if (B.alive()) paint();
|
|
147
155
|
}
|
|
156
|
+
async function forgetDevice(id) {
|
|
157
|
+
const out = await fetch('/_hub/push/forget', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) }).then((r) => r.ok, () => false);
|
|
158
|
+
if (!out) { flash = { text: 'Could not remove that device. Is the host awake?', error: true }; paint(); return; }
|
|
159
|
+
load();
|
|
160
|
+
}
|
|
148
161
|
B.on(main, 'click', (e) => {
|
|
162
|
+
const forgetter = e.target.closest?.('[data-forget-device]');
|
|
163
|
+
if (forgetter) forgetDevice(forgetter.dataset.forgetDevice);
|
|
149
164
|
const opener = e.target.closest?.('[data-settings-open]');
|
|
150
165
|
if (opener) B.openSettings({ hash: `#${opener.dataset.settingsOpen}` }, opener);
|
|
151
166
|
if (e.target.closest?.('[data-restart-hub]')) restartHub();
|
|
@@ -11,22 +11,42 @@
|
|
|
11
11
|
|
|
12
12
|
create: (root, siteRoot, ctx) => BoardView.page(root, siteRoot, async (B) => {
|
|
13
13
|
const { $, site } = B;
|
|
14
|
-
const { esc, markdown, linkContext, fetchJSON,
|
|
14
|
+
const { esc, markdown, linkContext, fetchJSON, downLanding, copyNote, pageCopy } = BoardUI;
|
|
15
15
|
const key = ctx.view.item || '';
|
|
16
16
|
const wave = ctx.view.wave;
|
|
17
17
|
const el = $('#plan');
|
|
18
18
|
const get = (url, opts = {}) => fetchJSON(url, { ...opts, sleep: B.sleep });
|
|
19
|
+
const paint = (body, items, note = '') => {
|
|
20
|
+
$('#ref').textContent = `${wave ? body.title : body.ref} notes`;
|
|
21
|
+
document.title = $('#ref').textContent;
|
|
22
|
+
el.innerHTML = note + markdown(body.markdown, linkContext(items, key, { base: './' }));
|
|
23
|
+
};
|
|
24
|
+
// While the board can't be reached, the notes' last answer here shows under its copy line, or with none the landing
|
|
25
|
+
// for why (docs/adr/2026-11-06-0135-a-board-out-of-reach-keeps-its-copy-or-shows-a-landing.md).
|
|
26
|
+
const url = site.url(`api/notes?${wave ? `wave=${encodeURIComponent(wave)}` : `item=${encodeURIComponent(key)}`}`);
|
|
27
|
+
let copy = null;
|
|
28
|
+
let live = false;
|
|
29
|
+
let copied = false;
|
|
30
|
+
const waiting = (down) => {
|
|
31
|
+
copy ||= pageCopy.read(url);
|
|
32
|
+
copy.then((c) => {
|
|
33
|
+
if (!B.alive() || live || copied) return;
|
|
34
|
+
if (!c) { el.innerHTML = downLanding(down); return; }
|
|
35
|
+
copied = true;
|
|
36
|
+
paint(c.body, [], copyNote('the notes', c.savedAt));
|
|
37
|
+
});
|
|
38
|
+
};
|
|
19
39
|
try {
|
|
20
40
|
const board = get(site.url('api/board')).then((r) => r.body).catch(() => ({}));
|
|
21
|
-
const
|
|
22
|
-
const r = await get(site.url(`api/notes?${query}`), { onWait: () => { el.innerHTML = restartNotice(); } });
|
|
41
|
+
const r = await get(url, { onWait: (ms, down) => waiting(down) });
|
|
23
42
|
if (!B.alive()) return;
|
|
24
43
|
if (!r.ok) throw new Error(r.body.error || `notes ${r.status}`);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
live = true;
|
|
45
|
+
paint(r.body, (await board).items || []);
|
|
46
|
+
pageCopy.save(url, r.body);
|
|
28
47
|
} catch (e) {
|
|
29
|
-
|
|
48
|
+
if (e.down) waiting(e.down);
|
|
49
|
+
else el.innerHTML = `<p class="muted">Could not load these notes: ${esc(e.message)}</p>`;
|
|
30
50
|
}
|
|
31
51
|
}),
|
|
32
52
|
};
|