reflectt-node 0.1.6 → 0.1.8
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 +13 -0
- package/defaults/gitignore.template +23 -0
- package/dist/boardHealthWorker.d.ts +4 -0
- package/dist/boardHealthWorker.d.ts.map +1 -1
- package/dist/boardHealthWorker.js +36 -1
- package/dist/boardHealthWorker.js.map +1 -1
- package/dist/buildInfo.d.ts.map +1 -1
- package/dist/buildInfo.js +47 -10
- package/dist/buildInfo.js.map +1 -1
- package/dist/chat.d.ts +4 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +6 -2
- package/dist/chat.js.map +1 -1
- package/dist/cli.js +37 -12
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +131 -64
- package/dist/cloud.js.map +1 -1
- package/dist/continuity-loop.d.ts.map +1 -1
- package/dist/continuity-loop.js +297 -29
- package/dist/continuity-loop.js.map +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/deploy-monitor.d.ts +18 -0
- package/dist/deploy-monitor.d.ts.map +1 -0
- package/dist/deploy-monitor.js +165 -0
- package/dist/deploy-monitor.js.map +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +15 -2
- package/dist/events.js.map +1 -1
- package/dist/executionSweeper.d.ts +1 -0
- package/dist/executionSweeper.d.ts.map +1 -1
- package/dist/executionSweeper.js +43 -7
- package/dist/executionSweeper.js.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +17 -3
- package/dist/files.js.map +1 -1
- package/dist/fingerprint.d.ts +30 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +122 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/github-webhook-attribution.d.ts +38 -0
- package/dist/github-webhook-attribution.d.ts.map +1 -0
- package/dist/github-webhook-attribution.js +123 -0
- package/dist/github-webhook-attribution.js.map +1 -0
- package/dist/inbox.d.ts.map +1 -1
- package/dist/inbox.js +4 -0
- package/dist/inbox.js.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/pulse.d.ts +7 -0
- package/dist/pulse.d.ts.map +1 -1
- package/dist/pulse.js +15 -0
- package/dist/pulse.js.map +1 -1
- package/dist/review-state.d.ts +9 -0
- package/dist/review-state.d.ts.map +1 -0
- package/dist/review-state.js +17 -0
- package/dist/review-state.js.map +1 -0
- package/dist/schedule.d.ts +60 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +176 -0
- package/dist/schedule.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +501 -14
- package/dist/server.js.map +1 -1
- package/dist/suppression-ledger.d.ts.map +1 -1
- package/dist/suppression-ledger.js +12 -3
- package/dist/suppression-ledger.js.map +1 -1
- package/dist/system-loop-state.d.ts +1 -1
- package/dist/system-loop-state.d.ts.map +1 -1
- package/dist/system-loop-state.js +1 -0
- package/dist/system-loop-state.js.map +1 -1
- package/dist/tasks.d.ts +9 -1
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +193 -41
- package/dist/tasks.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/usage-tracking.d.ts +26 -0
- package/dist/usage-tracking.d.ts.map +1 -1
- package/dist/usage-tracking.js +91 -4
- package/dist/usage-tracking.js.map +1 -1
- package/package.json +1 -1
- package/public/dashboard.js +136 -56
- package/public/docs.md +18 -0
package/public/dashboard.js
CHANGED
|
@@ -33,6 +33,8 @@ function activatePage(page) {
|
|
|
33
33
|
document.querySelectorAll('.sidebar-link[data-page]').forEach(link => {
|
|
34
34
|
link.classList.toggle('active', link.dataset.page === page);
|
|
35
35
|
});
|
|
36
|
+
// Load doctor page data when shown
|
|
37
|
+
if (page === 'doctor' && typeof loadDoctorPage === 'function') loadDoctorPage();
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function toggleSidebar() {
|
|
@@ -724,9 +726,10 @@ async function loadPresence() {
|
|
|
724
726
|
// ---- Tasks ----
|
|
725
727
|
async function loadTasks(forceFull = false) {
|
|
726
728
|
try {
|
|
727
|
-
|
|
729
|
+
// Force full load if we have no tasks yet (e.g. first load failed)
|
|
730
|
+
const useDelta = !forceFull && lastTaskSync > 0 && allTasks.length > 0;
|
|
728
731
|
const qs = new URLSearchParams();
|
|
729
|
-
qs.set('limit', '
|
|
732
|
+
qs.set('limit', '200');
|
|
730
733
|
if (useDelta) qs.set('updatedSince', String(lastTaskSync));
|
|
731
734
|
|
|
732
735
|
const r = await fetch(BASE + '/tasks?' + qs.toString());
|
|
@@ -1382,16 +1385,8 @@ async function loadSharedArtifacts() {
|
|
|
1382
1385
|
const pinnedRows = pinned.map(p => {
|
|
1383
1386
|
const found = files.find(f => (String(f.name || '')).toLowerCase() === p.name.toLowerCase());
|
|
1384
1387
|
if (!found) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
<div style="font-size:13px;color:var(--text-bright);font-weight:600">${esc(p.label)}</div>
|
|
1388
|
-
<span class="assignee-tag" style="color:var(--yellow)">missing</span>
|
|
1389
|
-
</div>
|
|
1390
|
-
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">
|
|
1391
|
-
To make this available here, create a symlink in the shared workspace:<br/>
|
|
1392
|
-
<code>ln -s ../RYANS-THOUGHTS.md ~/.openclaw/workspace-shared/process/RYANS-THOUGHTS.md</code>
|
|
1393
|
-
</div>
|
|
1394
|
-
</div>`;
|
|
1388
|
+
// Operator notes are optional. If not configured, do not show an operator-only instruction to end users.
|
|
1389
|
+
return '';
|
|
1395
1390
|
}
|
|
1396
1391
|
const href = '/shared/view?path=' + encodeURIComponent(found.path);
|
|
1397
1392
|
return `<div style="padding:10px 12px;border-bottom:1px solid var(--border-subtle)">
|
|
@@ -1420,9 +1415,13 @@ async function loadSharedArtifacts() {
|
|
|
1420
1415
|
</div>`;
|
|
1421
1416
|
}).join('');
|
|
1422
1417
|
|
|
1418
|
+
const pinnedBox = pinnedRows
|
|
1419
|
+
? `<div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${pinnedRows}</div>
|
|
1420
|
+
<div style="height:10px"></div>`
|
|
1421
|
+
: '';
|
|
1422
|
+
|
|
1423
1423
|
body.innerHTML = `
|
|
1424
|
-
|
|
1425
|
-
<div style="height:10px"></div>
|
|
1424
|
+
${pinnedBox}
|
|
1426
1425
|
<div style="font-size:11px;color:var(--text-muted);margin:0 0 8px">Shared workspace directory: <code>process/</code></div>
|
|
1427
1426
|
<div style="border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden">${listRows}</div>
|
|
1428
1427
|
`;
|
|
@@ -1658,12 +1657,7 @@ async function loadDoctorPage() {
|
|
|
1658
1657
|
}
|
|
1659
1658
|
}
|
|
1660
1659
|
|
|
1661
|
-
//
|
|
1662
|
-
const _origActivatePage = activatePage;
|
|
1663
|
-
function activatePage(page) {
|
|
1664
|
-
_origActivatePage(page);
|
|
1665
|
-
if (page === 'doctor') loadDoctorPage();
|
|
1666
|
-
}
|
|
1660
|
+
// Doctor page activation handled in activatePage() above
|
|
1667
1661
|
|
|
1668
1662
|
async function loadReleaseStatus(force = false) {
|
|
1669
1663
|
const badge = document.getElementById('release-badge');
|
|
@@ -1809,6 +1803,18 @@ function normalizeEpochMs(v) {
|
|
|
1809
1803
|
return v;
|
|
1810
1804
|
}
|
|
1811
1805
|
|
|
1806
|
+
function hasReviewerDecision(task) {
|
|
1807
|
+
const meta = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : null;
|
|
1808
|
+
const decision = meta && meta.reviewer_decision && typeof meta.reviewer_decision === 'object'
|
|
1809
|
+
? meta.reviewer_decision
|
|
1810
|
+
: null;
|
|
1811
|
+
return !!decision;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function shouldEscalateReviewerSla(task) {
|
|
1815
|
+
return task && task.slaState === 'breach' && !hasReviewerDecision(task);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1812
1818
|
function renderReviewQueue() {
|
|
1813
1819
|
const panel = document.getElementById('review-queue-panel');
|
|
1814
1820
|
const body = document.getElementById('review-queue-body');
|
|
@@ -1820,11 +1826,23 @@ function renderReviewQueue() {
|
|
|
1820
1826
|
const validating = allTasks
|
|
1821
1827
|
.filter(t => t.status === 'validating')
|
|
1822
1828
|
.map(t => {
|
|
1823
|
-
const
|
|
1829
|
+
const meta = t.metadata || {};
|
|
1830
|
+
const reviewState = typeof meta.review_state === 'string' ? meta.review_state : '';
|
|
1831
|
+
const reviewerDecision = meta.reviewer_decision;
|
|
1832
|
+
|
|
1833
|
+
// If reviewer has acted (needs_author or reviewer_decision recorded), the ball is with the assignee.
|
|
1834
|
+
// We still track an SLA timer, but it should page the assignee (author), not the reviewer.
|
|
1835
|
+
const waitOn = (reviewState === 'needs_author' || reviewerDecision != null) ? 'author' : 'reviewer';
|
|
1836
|
+
|
|
1837
|
+
const rawEntered = waitOn === 'author'
|
|
1838
|
+
? (reviewerDecision && reviewerDecision.decidedAt) || meta.review_last_activity_at || meta.entered_validating_at || t.updatedAt || t.createdAt
|
|
1839
|
+
: meta.entered_validating_at || t.updatedAt || t.createdAt;
|
|
1840
|
+
|
|
1824
1841
|
const enteredAt = normalizeEpochMs(rawEntered) || now;
|
|
1825
1842
|
const timeInReview = Math.min(Math.max(0, now - enteredAt), MAX_REVIEW_MS);
|
|
1826
1843
|
const slaState = getReviewSlaState(timeInReview);
|
|
1827
|
-
|
|
1844
|
+
|
|
1845
|
+
return { ...t, timeInReview, slaState, enteredAt, waitOn, reviewState, hasReviewerDecision: reviewerDecision != null };
|
|
1828
1846
|
})
|
|
1829
1847
|
.sort((a, b) => {
|
|
1830
1848
|
// Breaches first, then by time descending
|
|
@@ -1834,6 +1852,9 @@ function renderReviewQueue() {
|
|
|
1834
1852
|
return b.timeInReview - a.timeInReview;
|
|
1835
1853
|
});
|
|
1836
1854
|
|
|
1855
|
+
const reviewerQueue = validating.filter(t => t.waitOn === 'reviewer');
|
|
1856
|
+
const authorQueue = validating.filter(t => t.waitOn === 'author');
|
|
1857
|
+
|
|
1837
1858
|
if (validating.length === 0) {
|
|
1838
1859
|
panel.style.display = '';
|
|
1839
1860
|
count.textContent = '';
|
|
@@ -1849,16 +1870,29 @@ function renderReviewQueue() {
|
|
|
1849
1870
|
}
|
|
1850
1871
|
|
|
1851
1872
|
panel.style.display = '';
|
|
1852
|
-
|
|
1853
|
-
|
|
1873
|
+
const reviewerBreachCount = reviewerQueue.filter(t => t.slaState === 'breach').length;
|
|
1874
|
+
const authorBreachCount = authorQueue.filter(t => t.slaState === 'breach').length;
|
|
1875
|
+
|
|
1876
|
+
// Primary count is "awaiting reviewer" — author-wait tasks are tracked separately.
|
|
1877
|
+
count.textContent = reviewerQueue.length + ' awaiting review';
|
|
1878
|
+
|
|
1879
|
+
// Update sidebar badge (reviewer queue only)
|
|
1854
1880
|
const navReviewBadge = document.getElementById('nav-review-count');
|
|
1855
|
-
if (navReviewBadge) navReviewBadge.textContent =
|
|
1881
|
+
if (navReviewBadge) navReviewBadge.textContent = String(reviewerQueue.length);
|
|
1882
|
+
|
|
1883
|
+
const headerExtra = (reviewerBreachCount > 0 || authorBreachCount > 0)
|
|
1884
|
+
? ' <span style="color:var(--red);font-size:11px;font-weight:600">'
|
|
1885
|
+
+ (reviewerBreachCount > 0 ? (reviewerBreachCount + ' reviewer breach' + (reviewerBreachCount > 1 ? 'es' : '')) : '')
|
|
1886
|
+
+ (reviewerBreachCount > 0 && authorBreachCount > 0 ? ' · ' : '')
|
|
1887
|
+
+ (authorBreachCount > 0 ? (authorBreachCount + ' author breach' + (authorBreachCount > 1 ? 'es' : '')) : '')
|
|
1888
|
+
+ '</span>'
|
|
1889
|
+
: '';
|
|
1856
1890
|
|
|
1857
|
-
const
|
|
1858
|
-
|
|
1859
|
-
? ' <span style="color:var(--red);font-size:11px;font-weight:600">' + breachCount + ' breach' + (breachCount > 1 ? 'es' : '') + '</span>'
|
|
1891
|
+
const authorExtra = authorQueue.length > 0
|
|
1892
|
+
? ' <span style="color:var(--text-muted);font-size:11px;font-weight:500">· ' + authorQueue.length + ' awaiting author</span>'
|
|
1860
1893
|
: '';
|
|
1861
|
-
|
|
1894
|
+
|
|
1895
|
+
count.innerHTML = reviewerQueue.length + ' awaiting review' + authorExtra + headerExtra;
|
|
1862
1896
|
|
|
1863
1897
|
body.innerHTML = validating.map(t => {
|
|
1864
1898
|
const reviewer = t.reviewer || '<span style="color:var(--yellow)">unassigned</span>';
|
|
@@ -1887,33 +1921,40 @@ function renderReviewQueue() {
|
|
|
1887
1921
|
|
|
1888
1922
|
bindTaskLinkHandlers(body);
|
|
1889
1923
|
|
|
1890
|
-
// SLA breach escalation:
|
|
1891
|
-
|
|
1892
|
-
|
|
1924
|
+
// SLA breach escalation: split reviewer-wait vs author-wait so we page the right person.
|
|
1925
|
+
// shouldEscalateReviewerSla() suppresses escalation when reviewer_decision already exists.
|
|
1926
|
+
if (reviewerBreachCount > 0) {
|
|
1927
|
+
escalateReviewerBreaches(reviewerQueue.filter(shouldEscalateReviewerSla));
|
|
1928
|
+
}
|
|
1929
|
+
if (authorBreachCount > 0) {
|
|
1930
|
+
escalateAuthorBreaches(authorQueue.filter(t => t.slaState === 'breach'));
|
|
1893
1931
|
}
|
|
1894
1932
|
}
|
|
1895
1933
|
|
|
1896
|
-
let
|
|
1934
|
+
let lastReviewerEscalationAt = 0;
|
|
1935
|
+
let lastAuthorEscalationAt = 0;
|
|
1897
1936
|
const REVIEW_ESCALATION_COOLDOWN = 20 * 60 * 1000; // 20m
|
|
1898
1937
|
|
|
1899
|
-
async function
|
|
1938
|
+
async function escalateReviewerBreaches(breachedTasks) {
|
|
1900
1939
|
const now = Date.now();
|
|
1901
|
-
if (now -
|
|
1902
|
-
|
|
1940
|
+
if (now - lastReviewerEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
|
|
1941
|
+
lastReviewerEscalationAt = now;
|
|
1903
1942
|
|
|
1904
1943
|
const lines = breachedTasks.slice(0, 5).map(t => {
|
|
1905
1944
|
const reviewer = t.reviewer || 'unassigned';
|
|
1906
|
-
|
|
1945
|
+
const prUrl = t.metadata?.review_handoff?.pr_url || t.metadata?.review_handoff?.prUrl || '';
|
|
1946
|
+
const prPart = prUrl ? ' — ' + prUrl : '';
|
|
1947
|
+
return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — reviewer: @' + reviewer + ', waiting ' + formatDuration(t.timeInReview) + prPart;
|
|
1907
1948
|
});
|
|
1908
1949
|
|
|
1909
|
-
const content = '
|
|
1950
|
+
const content = 'Review overdue (reviewer SLA):\n' + lines.join('\n');
|
|
1910
1951
|
|
|
1911
1952
|
try {
|
|
1912
1953
|
await fetch(BASE + '/chat/messages', {
|
|
1913
1954
|
method: 'POST',
|
|
1914
1955
|
headers: { 'Content-Type': 'application/json' },
|
|
1915
1956
|
body: JSON.stringify({
|
|
1916
|
-
from: '
|
|
1957
|
+
from: 'dashboard',
|
|
1917
1958
|
content,
|
|
1918
1959
|
channel: 'general',
|
|
1919
1960
|
timestamp: now
|
|
@@ -1924,6 +1965,36 @@ async function escalateReviewBreaches(breachedTasks) {
|
|
|
1924
1965
|
}
|
|
1925
1966
|
}
|
|
1926
1967
|
|
|
1968
|
+
async function escalateAuthorBreaches(breachedTasks) {
|
|
1969
|
+
const now = Date.now();
|
|
1970
|
+
if (now - lastAuthorEscalationAt < REVIEW_ESCALATION_COOLDOWN) return;
|
|
1971
|
+
lastAuthorEscalationAt = now;
|
|
1972
|
+
|
|
1973
|
+
const lines = breachedTasks.slice(0, 5).map(t => {
|
|
1974
|
+
const assignee = t.assignee || 'unassigned';
|
|
1975
|
+
const prUrl = t.metadata?.review_handoff?.pr_url || t.metadata?.review_handoff?.prUrl || '';
|
|
1976
|
+
const prPart = prUrl ? ' — ' + prUrl : '';
|
|
1977
|
+
return '- ' + t.id + ' (' + (t.title || '').slice(0, 50) + ') — assignee: @' + assignee + ', waiting ' + formatDuration(t.timeInReview) + prPart;
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
const content = 'Author action needed (post-review):\n' + lines.join('\n');
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
await fetch(BASE + '/chat/messages', {
|
|
1984
|
+
method: 'POST',
|
|
1985
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1986
|
+
body: JSON.stringify({
|
|
1987
|
+
from: 'dashboard',
|
|
1988
|
+
content,
|
|
1989
|
+
channel: 'general',
|
|
1990
|
+
timestamp: now
|
|
1991
|
+
})
|
|
1992
|
+
});
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
console.error('Failed to escalate author breach:', err);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1927
1998
|
// ---- Feedback ----
|
|
1928
1999
|
let feedbackData = null;
|
|
1929
2000
|
|
|
@@ -2670,7 +2741,7 @@ function openTaskModal(taskId) {
|
|
|
2670
2741
|
loadPrReviewPanel(currentTask);
|
|
2671
2742
|
}
|
|
2672
2743
|
|
|
2673
|
-
function
|
|
2744
|
+
function formatDurationSec(sec) {
|
|
2674
2745
|
if (sec == null) return '';
|
|
2675
2746
|
if (sec < 60) return sec + 's';
|
|
2676
2747
|
return Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
|
|
@@ -2763,7 +2834,7 @@ function renderPrReviewPanel(data) {
|
|
|
2763
2834
|
html += '<div class="ci-check-row">';
|
|
2764
2835
|
html += '<span class="check-icon">' + icon + '</span>';
|
|
2765
2836
|
html += '<span class="check-name">' + esc(c.name) + '</span>';
|
|
2766
|
-
if (c.durationSec != null) html += '<span class="check-duration">' +
|
|
2837
|
+
if (c.durationSec != null) html += '<span class="check-duration">' + formatDurationSec(c.durationSec) + '</span>';
|
|
2767
2838
|
if (c.detailsUrl) html += '<a href="' + esc(c.detailsUrl) + '" target="_blank">logs</a>';
|
|
2768
2839
|
html += '</div>';
|
|
2769
2840
|
});
|
|
@@ -2979,7 +3050,7 @@ async function checkGettingStarted() {
|
|
|
2979
3050
|
// Step 1 done: check if system health loops are ticking (not just uptime > 0)
|
|
2980
3051
|
const hasHeartbeat = !!(health.system?.loops?.lastTickAt || (health.tasks?.total > 0));
|
|
2981
3052
|
const hasTasks = (health.tasks?.total || 0) > 0;
|
|
2982
|
-
const hasMessages = (health.chat?.total || 0) > 0;
|
|
3053
|
+
const hasMessages = (health.chat?.totalMessages || health.chat?.total || 0) > 0;
|
|
2983
3054
|
|
|
2984
3055
|
// Step 1: server running — always done if dashboard loads
|
|
2985
3056
|
const step1 = document.getElementById('gs-preflight');
|
|
@@ -3005,23 +3076,39 @@ async function checkGettingStarted() {
|
|
|
3005
3076
|
step3.querySelector('.gs-icon').textContent = '✓';
|
|
3006
3077
|
}
|
|
3007
3078
|
|
|
3008
|
-
// Auto-hide if all steps done
|
|
3079
|
+
// Auto-hide if all steps done, OR if the system clearly isn't a fresh install
|
|
3009
3080
|
const allDone = panel.querySelectorAll('.gs-step.done').length === 3;
|
|
3010
|
-
|
|
3081
|
+
const clearlyNotFresh = (health.tasks?.total || 0) > 5 || (health.chat?.totalMessages || health.chat?.total || 0) > 100;
|
|
3082
|
+
if (allDone || clearlyNotFresh) {
|
|
3011
3083
|
panel.classList.add('hidden');
|
|
3012
3084
|
}
|
|
3085
|
+
|
|
3086
|
+
// Pre-populate task badge from health before full task fetch completes
|
|
3087
|
+
const taskTotal = health.tasks?.total || 0;
|
|
3088
|
+
const navTaskBadge = document.getElementById('nav-task-count');
|
|
3089
|
+
if (navTaskBadge && taskTotal > 0 && allTasks.length === 0) {
|
|
3090
|
+
navTaskBadge.textContent = taskTotal;
|
|
3091
|
+
}
|
|
3013
3092
|
} catch {}
|
|
3014
3093
|
}
|
|
3015
3094
|
|
|
3016
3095
|
function dismissGettingStarted() {
|
|
3017
3096
|
const panel = document.getElementById('getting-started');
|
|
3018
|
-
if (panel)
|
|
3097
|
+
if (panel) {
|
|
3098
|
+
panel.classList.add('hidden');
|
|
3099
|
+
panel.style.display = 'none'; // belt + suspenders
|
|
3100
|
+
}
|
|
3019
3101
|
try { localStorage.setItem('reflectt-gs-dismissed', '1'); } catch {}
|
|
3020
3102
|
}
|
|
3103
|
+
// Expose globally for onclick handlers (safety net)
|
|
3104
|
+
window.dismissGettingStarted = dismissGettingStarted;
|
|
3105
|
+
window.dismissFirstBootBanner = dismissFirstBootBanner;
|
|
3021
3106
|
|
|
3022
3107
|
updateClock();
|
|
3023
3108
|
setInterval(updateClock, 30000);
|
|
3024
3109
|
checkGettingStarted();
|
|
3110
|
+
// Retry getting-started check after 5s in case health wasn't ready at boot
|
|
3111
|
+
setTimeout(checkGettingStarted, 5000);
|
|
3025
3112
|
refresh();
|
|
3026
3113
|
connectEventStream();
|
|
3027
3114
|
startAdaptiveRefresh();
|
|
@@ -3401,20 +3488,13 @@ function fileIcon(mimeType, name) {
|
|
|
3401
3488
|
}
|
|
3402
3489
|
|
|
3403
3490
|
function formatFileSize(bytes) {
|
|
3404
|
-
if (
|
|
3491
|
+
if (typeof bytes !== 'number' || bytes <= 0) return '0 B';
|
|
3405
3492
|
if (bytes < 1024) return bytes + ' B';
|
|
3406
3493
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
3407
3494
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
3408
3495
|
}
|
|
3409
3496
|
|
|
3410
|
-
|
|
3411
|
-
const d = typeof ts === 'string' ? new Date(ts) : new Date(ts);
|
|
3412
|
-
const s = Math.floor((Date.now() - d.getTime()) / 1000);
|
|
3413
|
-
if (s < 60) return 'just now';
|
|
3414
|
-
if (s < 3600) return Math.floor(s / 60) + 'm ago';
|
|
3415
|
-
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
|
|
3416
|
-
return Math.floor(s / 86400) + 'd ago';
|
|
3417
|
-
}
|
|
3497
|
+
// timeAgo defined above (line ~3286)
|
|
3418
3498
|
|
|
3419
3499
|
async function loadFiles() {
|
|
3420
3500
|
try {
|
|
@@ -3457,7 +3537,7 @@ function renderFiles(files) {
|
|
|
3457
3537
|
return '<div class="file-card" tabindex="0" role="button" aria-label="' + (f.originalName || f.filename) + '">' +
|
|
3458
3538
|
'<div class="thumb" style="display:flex;align-items:center;justify-content:center;height:80px;background:var(--surface-raised);border-radius:var(--radius-sm);overflow:hidden">' + thumb + '</div>' +
|
|
3459
3539
|
'<div class="card-name">' + (f.originalName || f.filename) + '</div>' +
|
|
3460
|
-
'<div class="card-meta">' + formatFileSize(f.
|
|
3540
|
+
'<div class="card-meta">' + formatFileSize(f.sizeBytes) + ' · ' + timeAgo(f.uploadedAt || f.createdAt) + '</div>' +
|
|
3461
3541
|
'<div class="card-actions">' +
|
|
3462
3542
|
'<a href="/files/' + f.id + '" download="' + (f.originalName || f.filename) + '" class="action-btn" aria-label="Download" onclick="event.stopPropagation()">⬇</a>' +
|
|
3463
3543
|
'<button class="action-btn delete" aria-label="Delete" onclick="event.stopPropagation();deleteFile(\'' + f.id + '\')">🗑</button>' +
|
|
@@ -3470,7 +3550,7 @@ function renderFiles(files) {
|
|
|
3470
3550
|
return '<div class="file-list-item" tabindex="0" role="button">' +
|
|
3471
3551
|
'<span class="list-icon">' + icon + '</span>' +
|
|
3472
3552
|
'<span class="list-name">' + (f.originalName || f.filename) + '</span>' +
|
|
3473
|
-
'<span class="list-meta">' + formatFileSize(f.
|
|
3553
|
+
'<span class="list-meta">' + formatFileSize(f.sizeBytes) + '</span>' +
|
|
3474
3554
|
'<span class="list-meta">' + timeAgo(f.uploadedAt || f.createdAt) + '</span>' +
|
|
3475
3555
|
'<div class="list-actions">' +
|
|
3476
3556
|
'<a href="/files/' + f.id + '" download="' + (f.originalName || f.filename) + '" class="action-btn" aria-label="Download" onclick="event.stopPropagation()">⬇</a>' +
|
package/public/docs.md
CHANGED
|
@@ -216,7 +216,10 @@ If your deployment needs quiet-hours behavior today, enforce it in scheduler/gat
|
|
|
216
216
|
| POST | `/tasks/batch-create` | Batch create up to 20 tasks. Body: `{ "tasks": [...], "createdBy": "agent", "deduplicate": true, "dryRun": false }`. Each task follows the same schema as `POST /tasks`. Returns per-task results (created/duplicate/error) with summary counts. Deduplication checks exact title match + fuzzy word overlap (Jaccard >0.6) against active tasks. |
|
|
217
217
|
| GET | `/tasks/heartbeat-status` | All doing tasks with stale comment activity (>30m). Returns `{ threshold, doingTaskCount, staleCount, staleTasks[] }`. Use for monitoring status heartbeat discipline compliance. |
|
|
218
218
|
| GET | `/tasks/board-health` | Board-level health metrics for backlog replenishment. Returns per-agent breakdown (doing, validating, todo, active counts), `needsWork`/`lowWatermark` flags, and `replenishNeeded` trigger (fires when 2+ agents idle or <3 backlog tasks). Query: `include_test=1` to include test-harness tasks. |
|
|
219
|
+
| GET | `/agents` | Agent list — alias for /agents/roles. Returns all agents with roles, WIP status, affinity tags. |
|
|
219
220
|
| GET | `/agents/roles` | Agent role registry with live WIP status. Returns all agents with `name`, `displayName`, `role`, `affinityTags`, `protectedDomains`, `wipCap`, `wipCount`, `overCap`. |
|
|
221
|
+
| POST | `/agents` | Add agent to team. Body: `{ name, role, description?, affinityTags?, wipCap? }`. Hot-reloads TEAM-ROLES.yaml. |
|
|
222
|
+
| DELETE | `/agents/:name` | Remove agent from team. Hot-reloads TEAM-ROLES.yaml. |
|
|
220
223
|
| POST | `/config/identity` | Set an agent's display name. Body: `{ "agent": "agent-1", "displayName": "Juniper" }`. Persists to TEAM-ROLES.yaml, hot-reloads. Dashboard and mentions use display name. |
|
|
221
224
|
| PUT | `/config/team-roles` | Write TEAM-ROLES.yaml. Body: `{ "yaml": "agents:\n - name: link\n role: engineer..." }`. Hot-reloads on save. Used by bootstrap agent to configure team from user intent. |
|
|
222
225
|
| GET | `/resolve/mention/:mention` | Resolve a mention string (name, displayName, or alias) to canonical agent ID. Returns `{ agent, displayName, role }`. |
|
|
@@ -863,6 +866,7 @@ Set via `reflectionNudge` in policy config:
|
|
|
863
866
|
| POST | `/usage/caps` | Create spend cap. Body: `{ scope: "global"\|"agent"\|"team", scope_id?, period: "daily"\|"weekly"\|"monthly", limit_usd, action: "warn"\|"throttle"\|"block" }`. |
|
|
864
867
|
| DELETE | `/usage/caps/:id` | Delete a spend cap. |
|
|
865
868
|
| GET | `/usage/routing-suggestions` | Smart routing savings suggestions (which low-stakes categories could use cheaper models). Query: `since`. |
|
|
869
|
+
| GET | `/costs` | Cost dashboard — aggregated spend for COO/PM monitoring. Query: `days` (1–90, default 7). Returns: `daily_by_model` (spend per model per day), `daily_totals` (per-day rolled-up for threshold alerting), `avg_cost_by_lane` (avg cost per closed task by `qa_bundle.lane`, 30-day floor), `avg_cost_by_agent` (avg cost per closed task per agent + `top_model`, 30-day floor), `top_tasks_by_cost` (top 20 most expensive tasks in window), `summary` (total tokens + cost), `lane_agent_window_days` (actual window used for lane/agent averages). |
|
|
866
870
|
|
|
867
871
|
### Model Pricing (built-in estimates, per 1M tokens)
|
|
868
872
|
| Model | Input | Output |
|
|
@@ -1034,6 +1038,20 @@ Export → import preserves: summary, description, organizer, attendees, locatio
|
|
|
1034
1038
|
| GET | `/calendar/events/:id/export.ics` | Export single event as .ics file. |
|
|
1035
1039
|
| POST | `/calendar/import` | Import events from .ics content. Body: `{ ics: string, organizer?: string }` or raw .ics string. Returns created/updated events. UID-based dedup on re-import. |
|
|
1036
1040
|
|
|
1041
|
+
## Schedule Feed
|
|
1042
|
+
|
|
1043
|
+
Shared time-awareness for the team. Canonical records for deploy windows, focus blocks, and scheduled task work — so agents can coordinate timing without chat.
|
|
1044
|
+
|
|
1045
|
+
**MVP scope (v1):** One-off windows only. No iCal/RRULE, no reminders, no recurring rules. For per-agent availability and recurring blocks use `/calendar/blocks`. For notifications use the Calendar Reminder Engine.
|
|
1046
|
+
|
|
1047
|
+
| Method | Path | Description |
|
|
1048
|
+
|--------|------|-------------|
|
|
1049
|
+
| GET | `/schedule/feed` | Upcoming entries in chronological order. Query: `kinds` (comma-separated: `deploy_window,focus_block,scheduled_task`), `owner`, `after` (epoch ms, default: now), `before` (epoch ms), `limit` (default: 50, max: 200). |
|
|
1050
|
+
| POST | `/schedule/entries` | Create a schedule entry. Body: `{ kind, title, start, end, owner, task_id?, status?, meta? }`. `kind` must be `deploy_window`, `focus_block`, or `scheduled_task`. `start`/`end` are epoch ms. Default status: `open` / `active` / `pending`. |
|
|
1051
|
+
| GET | `/schedule/entries/:id` | Get a single entry by ID. |
|
|
1052
|
+
| PATCH | `/schedule/entries/:id` | Update an entry. Body: `{ title?, start?, end?, status?, meta? }`. |
|
|
1053
|
+
| DELETE | `/schedule/entries/:id` | Delete an entry. Returns 204. |
|
|
1054
|
+
|
|
1037
1055
|
## Remote Node Management
|
|
1038
1056
|
|
|
1039
1057
|
Auth-gated endpoints for managing a reflectt-node instance remotely. Provide `REFLECTT_MANAGE_TOKEN` env var; authenticate via `x-manage-token` header or `Authorization: Bearer <token>`. Loopback (localhost) access is always allowed.
|