bgrun 3.12.0 → 3.12.1
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/dashboard/app/api/guard/route.ts +4 -1
- package/dashboard/app/api/guard-events/route.ts +5 -0
- package/dashboard/app/api/history/route.ts +39 -0
- package/dashboard/app/api/processes/route.ts +11 -3
- package/dashboard/app/api/restart/[name]/route.ts +7 -0
- package/dashboard/app/api/start/route.ts +5 -0
- package/dashboard/app/api/stop/[name]/route.ts +4 -1
- package/dashboard/app/api/templates/route.ts +47 -0
- package/dashboard/app/globals.css +384 -29
- package/dashboard/app/page.client.tsx +338 -1
- package/dashboard/app/page.tsx +98 -0
- package/dist/index.js +663 -184
- package/package.json +2 -2
- package/scripts/bgr-startup.ps1 +3 -3
- package/scripts/bgrun-startup.ps1 +91 -0
- package/src/commands/details.ts +17 -3
- package/src/commands/list.ts +37 -4
- package/src/commands/run.ts +21 -3
- package/src/db.ts +115 -0
- package/src/guard.ts +51 -0
- package/src/index.ts +83 -14
- package/src/index_copy.ts +614 -0
- package/src/logger.ts +12 -2
- package/src/platform.ts +87 -50
- package/src/server.ts +87 -3
- package/src/utils.ts +2 -2
|
@@ -288,6 +288,7 @@ function ProcessCard({ p }: { p: ProcessData }) {
|
|
|
288
288
|
<div className="card-header">
|
|
289
289
|
<div className="process-name">
|
|
290
290
|
<span>{p.name}</span>
|
|
291
|
+
{p.group && <span className="group-badge" title={`Group: ${p.group}`}>{p.group}</span>}
|
|
291
292
|
{guarded && <span className="guard-badge" title="Auto-restart enabled">🛡️</span>}
|
|
292
293
|
</div>
|
|
293
294
|
<span className={`status-badge ${p.running ? 'running' : 'stopped'}`}>
|
|
@@ -413,6 +414,7 @@ export default function mount(): () => void {
|
|
|
413
414
|
let isFirstLoad = true;
|
|
414
415
|
let allProcesses: ProcessData[] = [];
|
|
415
416
|
let searchQuery = '';
|
|
417
|
+
let groupQuery = '';
|
|
416
418
|
let searchDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
417
419
|
let collapsedGroups: Set<string> = new Set(JSON.parse(localStorage.getItem('bgr_collapsed_groups') || '[]'));
|
|
418
420
|
let drawerProcess: string | null = null;
|
|
@@ -465,6 +467,48 @@ export default function mount(): () => void {
|
|
|
465
467
|
}
|
|
466
468
|
loadVersion();
|
|
467
469
|
|
|
470
|
+
// ─── Guard Activity Feed ───
|
|
471
|
+
interface GuardEvent {
|
|
472
|
+
time: number;
|
|
473
|
+
name: string;
|
|
474
|
+
action: string;
|
|
475
|
+
success: boolean;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function loadGuardEvents() {
|
|
479
|
+
const listEl = $('guard-activity-list');
|
|
480
|
+
const emptyEl = $('guard-activity-empty');
|
|
481
|
+
if (!listEl) return;
|
|
482
|
+
try {
|
|
483
|
+
const res = await fetch('/api/guard-events');
|
|
484
|
+
const events: GuardEvent[] = await res.json();
|
|
485
|
+
if (events.length === 0) {
|
|
486
|
+
if (emptyEl) emptyEl.style.display = '';
|
|
487
|
+
listEl.innerHTML = '';
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
491
|
+
listEl.replaceChildren(...events.slice(0, 10).map(ev => {
|
|
492
|
+
const date = new Date(ev.time);
|
|
493
|
+
const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
494
|
+
const icon = ev.success ? '↻' : '✕';
|
|
495
|
+
const actionText = ev.action === 'restart' ? 'restarted' : ev.action;
|
|
496
|
+
return (
|
|
497
|
+
<div className={`guard-event ${ev.success ? 'success' : 'failed'}`}>
|
|
498
|
+
<span className="guard-event-time">{timeStr}</span>
|
|
499
|
+
<span className="guard-event-icon">{icon}</span>
|
|
500
|
+
<span className="guard-event-name">{ev.name}</span>
|
|
501
|
+
<span className="guard-event-action">{actionText}</span>
|
|
502
|
+
</div>
|
|
503
|
+
) as unknown as Node;
|
|
504
|
+
}));
|
|
505
|
+
} catch {
|
|
506
|
+
if (emptyEl) emptyEl.style.display = '';
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
loadGuardEvents();
|
|
510
|
+
setInterval(loadGuardEvents, 10000); // Refresh every 10s
|
|
511
|
+
|
|
468
512
|
// ─── Load & Render Processes ───
|
|
469
513
|
|
|
470
514
|
async function loadProcesses() {
|
|
@@ -473,6 +517,7 @@ export default function mount(): () => void {
|
|
|
473
517
|
try {
|
|
474
518
|
const res = await fetch('/api/processes');
|
|
475
519
|
allProcesses = await res.json();
|
|
520
|
+
updateGroupFilter();
|
|
476
521
|
renderFilteredProcesses();
|
|
477
522
|
updateStats(allProcesses);
|
|
478
523
|
} catch (err) {
|
|
@@ -482,18 +527,45 @@ export default function mount(): () => void {
|
|
|
482
527
|
}
|
|
483
528
|
}
|
|
484
529
|
|
|
530
|
+
function updateGroupFilter() {
|
|
531
|
+
const groupFilter = $('group-filter') as HTMLSelectElement;
|
|
532
|
+
if (!groupFilter) return;
|
|
533
|
+
const groups = new Set<string>();
|
|
534
|
+
for (const p of allProcesses) {
|
|
535
|
+
if (p.group) groups.add(p.group);
|
|
536
|
+
}
|
|
537
|
+
const currentValue = groupFilter.value;
|
|
538
|
+
groupFilter.replaceChildren(
|
|
539
|
+
<option value="">All Groups</option> as unknown as Node,
|
|
540
|
+
...Array.from(groups).sort().map(g => <option value={g}>{g}</option> as unknown as Node)
|
|
541
|
+
);
|
|
542
|
+
// Preserve selection if still valid
|
|
543
|
+
if (currentValue && groups.has(currentValue)) {
|
|
544
|
+
groupFilter.value = currentValue;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
485
548
|
function renderFilteredProcesses() {
|
|
486
549
|
// Always sync searchQuery from DOM to prevent desync
|
|
487
550
|
if (searchInput && searchInput.value.toLowerCase().trim() !== searchQuery) {
|
|
488
551
|
searchQuery = searchInput.value.toLowerCase().trim();
|
|
489
552
|
}
|
|
490
|
-
|
|
553
|
+
// Sync groupQuery from dropdown
|
|
554
|
+
const groupFilter = $('group-filter') as HTMLSelectElement;
|
|
555
|
+
if (groupFilter && groupFilter.value !== groupQuery) {
|
|
556
|
+
groupQuery = groupFilter.value;
|
|
557
|
+
}
|
|
558
|
+
let filtered = searchQuery
|
|
491
559
|
? allProcesses.filter(p =>
|
|
492
560
|
p.name.toLowerCase().includes(searchQuery) ||
|
|
493
561
|
p.command.toLowerCase().includes(searchQuery) ||
|
|
494
562
|
(p.port && String(p.port).includes(searchQuery))
|
|
495
563
|
)
|
|
496
564
|
: allProcesses;
|
|
565
|
+
// Apply group filter
|
|
566
|
+
if (groupQuery) {
|
|
567
|
+
filtered = filtered.filter(p => p.group === groupQuery);
|
|
568
|
+
}
|
|
497
569
|
renderProcesses(filtered);
|
|
498
570
|
|
|
499
571
|
// Update search result count badge
|
|
@@ -657,6 +729,14 @@ export default function mount(): () => void {
|
|
|
657
729
|
}, 150);
|
|
658
730
|
});
|
|
659
731
|
|
|
732
|
+
// ─── Group Filter ───
|
|
733
|
+
|
|
734
|
+
const groupFilter = $('group-filter') as HTMLSelectElement;
|
|
735
|
+
groupFilter?.addEventListener('change', () => {
|
|
736
|
+
groupQuery = groupFilter.value;
|
|
737
|
+
renderFilteredProcesses();
|
|
738
|
+
});
|
|
739
|
+
|
|
660
740
|
/** Fetch with cache-bust to force fresh data after mutations */
|
|
661
741
|
async function loadProcessesFresh() {
|
|
662
742
|
isFetching = true;
|
|
@@ -1608,6 +1688,263 @@ export default function mount(): () => void {
|
|
|
1608
1688
|
}
|
|
1609
1689
|
});
|
|
1610
1690
|
|
|
1691
|
+
// ─── Templates Modal ───
|
|
1692
|
+
|
|
1693
|
+
interface TemplateData {
|
|
1694
|
+
name: string;
|
|
1695
|
+
command: string;
|
|
1696
|
+
workdir: string;
|
|
1697
|
+
env: string;
|
|
1698
|
+
group: string;
|
|
1699
|
+
created_at: string;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
let templates: TemplateData[] = [];
|
|
1703
|
+
|
|
1704
|
+
async function loadTemplates() {
|
|
1705
|
+
try {
|
|
1706
|
+
const res = await fetch('/api/templates');
|
|
1707
|
+
if (res.ok) {
|
|
1708
|
+
templates = await res.json();
|
|
1709
|
+
renderTemplates();
|
|
1710
|
+
}
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
console.error('[bgr-dashboard] loadTemplates error:', err);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function renderTemplates() {
|
|
1717
|
+
const list = $('templates-list');
|
|
1718
|
+
if (!list) return;
|
|
1719
|
+
|
|
1720
|
+
if (templates.length === 0) {
|
|
1721
|
+
list.innerHTML = '<div class="templates-empty">No templates saved yet</div>';
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
list.replaceChildren(...templates.map(t => (
|
|
1726
|
+
<div className="template-item">
|
|
1727
|
+
<div className="template-item-info">
|
|
1728
|
+
<div className="template-item-name">{t.name}</div>
|
|
1729
|
+
<div className="template-item-command">{t.command}</div>
|
|
1730
|
+
</div>
|
|
1731
|
+
{t.group && <span className="template-item-group">{t.group}</span>}
|
|
1732
|
+
<div className="template-item-actions">
|
|
1733
|
+
<button className="use-btn" data-use={t.name} title="Use this template">Use</button>
|
|
1734
|
+
<button className="delete-btn" data-delete={t.name} title="Delete template">✕</button>
|
|
1735
|
+
</div>
|
|
1736
|
+
</div>
|
|
1737
|
+
) as unknown as Node));
|
|
1738
|
+
|
|
1739
|
+
// Add click handlers
|
|
1740
|
+
list.querySelectorAll('.use-btn').forEach(btn => {
|
|
1741
|
+
btn.addEventListener('click', (e) => {
|
|
1742
|
+
const name = (e.target as HTMLElement).dataset.use;
|
|
1743
|
+
const tmpl = templates.find(t => t.name === name);
|
|
1744
|
+
if (tmpl) {
|
|
1745
|
+
useTemplate(tmpl);
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
list.querySelectorAll('.delete-btn').forEach(btn => {
|
|
1751
|
+
btn.addEventListener('click', (e) => {
|
|
1752
|
+
const name = (e.target as HTMLElement).dataset.delete;
|
|
1753
|
+
if (name) deleteTemplate(name);
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function openTemplatesModal() {
|
|
1759
|
+
const modal = $('templates-modal');
|
|
1760
|
+
if (modal) modal.classList.add('active');
|
|
1761
|
+
loadTemplates();
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function closeTemplatesModal() {
|
|
1765
|
+
const modal = $('templates-modal');
|
|
1766
|
+
if (modal) modal.classList.remove('active');
|
|
1767
|
+
// Clear form
|
|
1768
|
+
($('template-name') as HTMLInputElement).value = '';
|
|
1769
|
+
($('template-command') as HTMLInputElement).value = '';
|
|
1770
|
+
($('template-directory') as HTMLInputElement).value = '';
|
|
1771
|
+
($('template-group') as HTMLInputElement).value = '';
|
|
1772
|
+
($('template-env') as HTMLInputElement).value = '';
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
async function saveTemplate() {
|
|
1776
|
+
const name = ($('template-name') as HTMLInputElement)?.value?.trim();
|
|
1777
|
+
const command = ($('template-command') as HTMLInputElement)?.value?.trim();
|
|
1778
|
+
const workdir = ($('template-directory') as HTMLInputElement)?.value?.trim();
|
|
1779
|
+
const group = ($('template-group') as HTMLInputElement)?.value?.trim();
|
|
1780
|
+
const env = ($('template-env') as HTMLInputElement)?.value?.trim();
|
|
1781
|
+
|
|
1782
|
+
if (!name || !command) {
|
|
1783
|
+
showToast('Name and command are required', 'error');
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
try {
|
|
1788
|
+
const res = await fetch('/api/templates', {
|
|
1789
|
+
method: 'POST',
|
|
1790
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1791
|
+
body: JSON.stringify({ name, command, workdir, group, env }),
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
if (res.ok) {
|
|
1795
|
+
showToast(`Template "${name}" saved`, 'success');
|
|
1796
|
+
loadTemplates();
|
|
1797
|
+
// Clear form
|
|
1798
|
+
($('template-name') as HTMLInputElement).value = '';
|
|
1799
|
+
($('template-command') as HTMLInputElement).value = '';
|
|
1800
|
+
($('template-directory') as HTMLInputElement).value = '';
|
|
1801
|
+
($('template-group') as HTMLInputElement).value = '';
|
|
1802
|
+
($('template-env') as HTMLInputElement).value = '';
|
|
1803
|
+
} else {
|
|
1804
|
+
showToast('Failed to save template', 'error');
|
|
1805
|
+
}
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
showToast('Failed to save template', 'error');
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
async function deleteTemplate(name: string) {
|
|
1812
|
+
if (!confirm(`Delete template "${name}"?`)) return;
|
|
1813
|
+
|
|
1814
|
+
try {
|
|
1815
|
+
const res = await fetch(`/api/templates?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
1816
|
+
if (res.ok) {
|
|
1817
|
+
showToast(`Template "${name}" deleted`, 'success');
|
|
1818
|
+
loadTemplates();
|
|
1819
|
+
} else {
|
|
1820
|
+
showToast('Failed to delete template', 'error');
|
|
1821
|
+
}
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
showToast('Failed to delete template', 'error');
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function useTemplate(tmpl: TemplateData) {
|
|
1828
|
+
// Fill new process form with template values
|
|
1829
|
+
($('process-name-input') as HTMLInputElement).value = '';
|
|
1830
|
+
($('process-command-input') as HTMLInputElement).value = tmpl.command;
|
|
1831
|
+
($('process-directory-input') as HTMLInputElement).value = tmpl.workdir;
|
|
1832
|
+
closeTemplatesModal();
|
|
1833
|
+
openModal();
|
|
1834
|
+
showToast(`Template "${tmpl.name}" loaded — enter a process name`, 'success');
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
$('templates-btn')?.addEventListener('click', openTemplatesModal);
|
|
1838
|
+
$('templates-modal-close')?.addEventListener('click', closeTemplatesModal);
|
|
1839
|
+
$('template-save-btn')?.addEventListener('click', saveTemplate);
|
|
1840
|
+
$('templates-modal')?.addEventListener('click', (e) => {
|
|
1841
|
+
if ((e.target as Element).classList.contains('modal-overlay')) {
|
|
1842
|
+
closeTemplatesModal();
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
// ─── History Modal ───
|
|
1847
|
+
|
|
1848
|
+
interface HistoryEntry {
|
|
1849
|
+
process_name: string;
|
|
1850
|
+
event: string;
|
|
1851
|
+
pid: number | null;
|
|
1852
|
+
timestamp: string;
|
|
1853
|
+
metadata: Record<string, any>;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
let allHistory: HistoryEntry[] = [];
|
|
1857
|
+
|
|
1858
|
+
async function loadHistory() {
|
|
1859
|
+
try {
|
|
1860
|
+
const res = await fetch('/api/history?limit=100');
|
|
1861
|
+
if (res.ok) {
|
|
1862
|
+
allHistory = await res.json();
|
|
1863
|
+
renderHistory();
|
|
1864
|
+
updateHistoryFilters();
|
|
1865
|
+
}
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
console.error('[bgr-dashboard] loadHistory error:', err);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function updateHistoryFilters() {
|
|
1872
|
+
const processFilter = $('history-process-filter') as HTMLSelectElement;
|
|
1873
|
+
const eventFilter = $('history-event-filter') as HTMLSelectElement;
|
|
1874
|
+
if (!processFilter) return;
|
|
1875
|
+
|
|
1876
|
+
const processNames = new Set<string>();
|
|
1877
|
+
for (const h of allHistory) {
|
|
1878
|
+
processNames.add(h.process_name);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const currentValue = processFilter.value;
|
|
1882
|
+
processFilter.replaceChildren(
|
|
1883
|
+
<option value="">All Processes</option> as unknown as Node,
|
|
1884
|
+
...Array.from(processNames).sort().map(n => <option value={n}>{n}</option> as unknown as Node)
|
|
1885
|
+
);
|
|
1886
|
+
if (currentValue && processNames.has(currentValue)) {
|
|
1887
|
+
processFilter.value = currentValue;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function renderHistory() {
|
|
1892
|
+
const list = $('history-list');
|
|
1893
|
+
const processFilter = $('history-process-filter') as HTMLSelectElement;
|
|
1894
|
+
const eventFilter = $('history-event-filter') as HTMLSelectElement;
|
|
1895
|
+
if (!list) return;
|
|
1896
|
+
|
|
1897
|
+
const processValue = processFilter?.value || '';
|
|
1898
|
+
const eventValue = eventFilter?.value || '';
|
|
1899
|
+
|
|
1900
|
+
let filtered = allHistory;
|
|
1901
|
+
if (processValue) {
|
|
1902
|
+
filtered = filtered.filter(h => h.process_name === processValue);
|
|
1903
|
+
}
|
|
1904
|
+
if (eventValue) {
|
|
1905
|
+
filtered = filtered.filter(h => h.event === eventValue);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
if (filtered.length === 0) {
|
|
1909
|
+
list.innerHTML = '<div class="history-empty">No history found</div>';
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
list.replaceChildren(...filtered.map(h => {
|
|
1914
|
+
const time = new Date(h.timestamp);
|
|
1915
|
+
const timeStr = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ' ' + time.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
1916
|
+
return (
|
|
1917
|
+
<div className="history-item">
|
|
1918
|
+
<span className="history-item-time">{timeStr}</span>
|
|
1919
|
+
<span className="history-item-process">{h.process_name}</span>
|
|
1920
|
+
<span className={`history-item-event ${h.event}`}>{h.event.replace('_', ' ')}</span>
|
|
1921
|
+
{h.pid && <span className="history-item-pid">PID {h.pid}</span>}
|
|
1922
|
+
</div>
|
|
1923
|
+
) as unknown as Node;
|
|
1924
|
+
}));
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function openHistoryModal() {
|
|
1928
|
+
const modal = $('history-modal');
|
|
1929
|
+
if (modal) modal.classList.add('active');
|
|
1930
|
+
loadHistory();
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function closeHistoryModal() {
|
|
1934
|
+
const modal = $('history-modal');
|
|
1935
|
+
if (modal) modal.classList.remove('active');
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
$('history-btn')?.addEventListener('click', openHistoryModal);
|
|
1939
|
+
$('history-modal-close')?.addEventListener('click', closeHistoryModal);
|
|
1940
|
+
$('history-modal')?.addEventListener('click', (e) => {
|
|
1941
|
+
if ((e.target as Element).classList.contains('modal-overlay')) {
|
|
1942
|
+
closeHistoryModal();
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
$('history-process-filter')?.addEventListener('change', renderHistory);
|
|
1946
|
+
$('history-event-filter')?.addEventListener('change', renderHistory);
|
|
1947
|
+
|
|
1611
1948
|
// ─── Toolbar Actions ───
|
|
1612
1949
|
$('refresh-btn')?.addEventListener('click', () => {
|
|
1613
1950
|
loadProcesses();
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -39,6 +39,15 @@ export default function DashboardPage() {
|
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
|
+
{/* Guard Activity Feed */}
|
|
43
|
+
<div className="guard-activity" id="guard-activity">
|
|
44
|
+
<div className="guard-activity-header">
|
|
45
|
+
<span className="guard-activity-title">🛡️ Guard Activity</span>
|
|
46
|
+
<span className="guard-activity-empty" id="guard-activity-empty">No recent activity</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="guard-activity-list" id="guard-activity-list"></div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
42
51
|
{/* Toolbar */}
|
|
43
52
|
<div className="toolbar">
|
|
44
53
|
<div className="toolbar-left">
|
|
@@ -56,6 +65,9 @@ export default function DashboardPage() {
|
|
|
56
65
|
<span className="search-count" id="search-count" style={{ display: 'none' }}></span>
|
|
57
66
|
<span className="search-shortcut">/</span>
|
|
58
67
|
</div>
|
|
68
|
+
<select className="group-filter" id="group-filter">
|
|
69
|
+
<option value="">All Groups</option>
|
|
70
|
+
</select>
|
|
59
71
|
</div>
|
|
60
72
|
<div className="toolbar-right">
|
|
61
73
|
<button className="btn btn-ghost btn-icon" id="refresh-btn" title="Refresh">
|
|
@@ -77,6 +89,20 @@ export default function DashboardPage() {
|
|
|
77
89
|
<button className="btn btn-ghost btn-icon" id="shortcuts-btn" title="Keyboard Shortcuts (?)">
|
|
78
90
|
<span style={{ fontSize: '0.85rem', fontWeight: '700' }}>?</span>
|
|
79
91
|
</button>
|
|
92
|
+
<button className="btn btn-secondary" id="templates-btn">
|
|
93
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
94
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
95
|
+
<polyline points="14 2 14 8 20 8" />
|
|
96
|
+
</svg>
|
|
97
|
+
Templates
|
|
98
|
+
</button>
|
|
99
|
+
<button className="btn btn-ghost" id="history-btn">
|
|
100
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
101
|
+
<circle cx="12" cy="12" r="10" />
|
|
102
|
+
<polyline points="12 6 12 12 16 14" />
|
|
103
|
+
</svg>
|
|
104
|
+
History
|
|
105
|
+
</button>
|
|
80
106
|
<button className="btn btn-primary" id="new-process-btn">
|
|
81
107
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
82
108
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
@@ -226,6 +252,78 @@ export default function DashboardPage() {
|
|
|
226
252
|
</div>
|
|
227
253
|
</div>
|
|
228
254
|
|
|
255
|
+
{/* History Modal */}
|
|
256
|
+
<div className="modal-overlay" id="history-modal">
|
|
257
|
+
<div className="modal modal-wide">
|
|
258
|
+
<div className="modal-header">
|
|
259
|
+
<h3>📜 Process History</h3>
|
|
260
|
+
<button className="modal-close" id="history-modal-close">✕</button>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="modal-body">
|
|
263
|
+
<div className="history-filters">
|
|
264
|
+
<select id="history-process-filter" className="history-select">
|
|
265
|
+
<option value="">All Processes</option>
|
|
266
|
+
</select>
|
|
267
|
+
<select id="history-event-filter" className="history-select">
|
|
268
|
+
<option value="">All Events</option>
|
|
269
|
+
<option value="start">Start</option>
|
|
270
|
+
<option value="stop">Stop</option>
|
|
271
|
+
<option value="restart">Restart</option>
|
|
272
|
+
<option value="guard_on">Guard On</option>
|
|
273
|
+
<option value="guard_off">Guard Off</option>
|
|
274
|
+
</select>
|
|
275
|
+
</div>
|
|
276
|
+
<div className="history-list" id="history-list">
|
|
277
|
+
<div className="history-empty">No history yet</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Templates Modal */}
|
|
284
|
+
<div className="modal-overlay" id="templates-modal">
|
|
285
|
+
<div className="modal modal-wide">
|
|
286
|
+
<div className="modal-header">
|
|
287
|
+
<h3>📋 Process Templates</h3>
|
|
288
|
+
<button className="modal-close" id="templates-modal-close">✕</button>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="modal-body">
|
|
291
|
+
<div className="templates-form">
|
|
292
|
+
<div className="form-row">
|
|
293
|
+
<div className="form-group">
|
|
294
|
+
<label htmlFor="template-name">Template Name</label>
|
|
295
|
+
<input type="text" id="template-name" placeholder="my-template" />
|
|
296
|
+
</div>
|
|
297
|
+
<div className="form-group">
|
|
298
|
+
<label htmlFor="template-command">Command</label>
|
|
299
|
+
<input type="text" id="template-command" placeholder="bun run dev" />
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="form-row">
|
|
303
|
+
<div className="form-group">
|
|
304
|
+
<label htmlFor="template-directory">Working Directory</label>
|
|
305
|
+
<input type="text" id="template-directory" placeholder="/path/to/project" />
|
|
306
|
+
</div>
|
|
307
|
+
<div className="form-group">
|
|
308
|
+
<label htmlFor="template-group">Group</label>
|
|
309
|
+
<input type="text" id="template-group" placeholder="my-group" />
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="form-group">
|
|
313
|
+
<label htmlFor="template-env">Environment (KEY=VAL,KEY2=VAL2)</label>
|
|
314
|
+
<input type="text" id="template-env" placeholder="BGR_KEEP_ALIVE=true,PORT=3000" />
|
|
315
|
+
</div>
|
|
316
|
+
<div className="templates-actions">
|
|
317
|
+
<button className="btn btn-primary" id="template-save-btn">Save Template</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div className="templates-list" id="templates-list">
|
|
321
|
+
<div className="templates-empty">No templates saved yet</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
229
327
|
{/* Keyboard Shortcuts Overlay */}
|
|
230
328
|
<div className="shortcuts-overlay" id="shortcuts-overlay">
|
|
231
329
|
<div className="shortcuts-panel">
|