Skip to content
415 changes: 286 additions & 129 deletions gearbox/internal/framework/templates/layouts/base.templ

Large diffs are not rendered by default.

44 changes: 36 additions & 8 deletions gearbox/internal/framework/templates/pages/alerts.templ
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ templ AlertsPage(user *models.User, servers []models.BoxConfig, perms *models.Us
<div id="page-header-source" class="hidden">
<div class="flex-1"></div>
<div class="flex items-center space-x-3">
<!-- Search input -->
<input
type="text"
id="alert-search"
placeholder="Search alerts..."
oninput="filterAlerts(this.value)"
class="w-48 h-[38px] px-3 py-1.5 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<!-- Search input moved to the global HeaderSearch
(issue #92). filterAlerts() is wired through a
window.gearbox.filter.register call by alerts.js. -->
<!-- Status filter -->
<select
id="status-filter"
Expand Down Expand Up @@ -354,6 +349,39 @@ templ AlertsPage(user *models.User, servers []models.BoxConfig, perms *models.Us
refreshAlertsData();
initSSE();
}

// HeaderSearch + command palette wiring (issue #92).
if (window.gearbox && window.gearbox.filter) {
window.gearbox.filter.register({
placeholder: 'Search alerts…',
onInput: function (q) { if (typeof filterAlerts === 'function') filterAlerts(q || ''); },
onClear: function () { if (typeof filterAlerts === 'function') filterAlerts(''); },
});
}
if (window.gearbox && window.gearbox.commands) {
const cmds = window.gearbox.commands;
const statusSel = document.getElementById('status-filter');
if (statusSel) {
Array.from(statusSel.options).forEach(function (opt) {
cmds.register({
id: 'alerts.status.' + opt.value,
label: 'Status: ' + opt.text,
subtitle: 'Show ' + opt.text.toLowerCase(),
group: 'Alerts',
run: function () {
statusSel.value = opt.value;
if (typeof changeStatusFilter === 'function') changeStatusFilter(opt.value);
},
});
});
}
cmds.register({
id: 'alerts.refresh',
label: 'Refresh alerts',
group: 'Alerts',
run: function () { if (typeof refreshAlertsData === 'function') refreshAlertsData(); },
});
}
});

// Clean up SSE on page unload
Expand Down
10 changes: 10 additions & 0 deletions gearbox/internal/framework/templates/pages/certificates.templ
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,16 @@ templ Certificates(user *models.User, servers []models.BoxConfig, perms *models.
setupPageHeader();
loadCertificates();
initSSE();

// Command palette wiring (issue #92).
if (window.gearbox && window.gearbox.commands) {
window.gearbox.commands.register({
id: 'certificates.refresh',
label: 'Refresh certificate list',
group: 'Certificates',
run: function () { if (typeof loadCertificates === 'function') loadCertificates(); },
});
}
});

// Cleanup on page unload
Expand Down
145 changes: 107 additions & 38 deletions gearbox/internal/framework/templates/pages/logs.templ
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ templ Logs(user *models.User, servers []models.BoxConfig) {

<!-- Hidden container for page header content - will be moved to main header via JS.
Title + box selector are now in the global header breadcrumb +
chip; this slot only carries page-specific controls. -->
chip; this slot only carries page-specific controls.

The filter input itself has moved to the global HeaderSearch
(issue #92); this template still renders the source / lines /
severity selects and the action buttons. Logs registers its
own filter + commands via window.gearbox in the page script
below so the search bar drives filterLogs() and the palette
drives every other toolbar action. -->
<div id="page-header-source" class="hidden">
<!-- Right side spacer pushes controls to the far right of the header -->
<div class="flex-1"></div>
Expand Down Expand Up @@ -39,26 +46,8 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
</select>
}

<!-- Filter input and severity -->
<div class="relative">
<input
type="text"
id="filter-input"
placeholder="Filter..."
class="h-[38px] w-32 px-3 py-1.5 pr-8 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
oninput="filterLogs()"
/>
<button
type="button"
id="filter-clear"
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hidden"
onclick="clearLogFilter()"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Severity select (kept here; filterLogs() also reads it).
The text filter input moved to the global HeaderSearch. -->
<select
id="severity-filter"
class="h-[38px] px-3 py-1.5 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
Expand Down Expand Up @@ -457,10 +446,14 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
}
}

// Filter a single line element
// Filter state. The text filter used to live in a #filter-input
// element on this page; it's now driven by the global
// HeaderSearch (issue #92). __logFilterQuery is updated by the
// gear-registered filter callback in DOMContentLoaded below, and
// filterLogs() / filterSingleLine() read from it instead.
let __logFilterQuery = '';
function filterSingleLine(lineEl) {
const input = document.getElementById('filter-input');
const filter = input ? input.value.toLowerCase() : '';
const filter = __logFilterQuery;
const severityFilter = document.getElementById('severity-filter');
const severityValue = severityFilter ? severityFilter.value : 'all';

Expand Down Expand Up @@ -752,19 +745,14 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
}

function filterLogs() {
const input = document.getElementById('filter-input');
const filter = input.value.toLowerCase();
// __logFilterQuery is maintained by the HeaderSearch
// filter registration in DOMContentLoaded (lower-cased on
// write). filterSingleLine() reads both that and the
// severity dropdown.
const filter = __logFilterQuery;
const severityFilter = document.getElementById('severity-filter').value;
const container = document.getElementById('log-container');
const lines = container.getElementsByClassName('log-line');
const clearBtn = document.getElementById('filter-clear');

// Show/hide clear button
if (filter.length > 0) {
clearBtn.classList.remove('hidden');
} else {
clearBtn.classList.add('hidden');
}

for (let line of lines) {
const textMatch = filter === '' || line.textContent.toLowerCase().includes(filter);
Expand All @@ -786,11 +774,11 @@ templ Logs(user *models.User, servers []models.BoxConfig) {
}

function clearLogFilter() {
const input = document.getElementById('filter-input');
input.value = '';
document.getElementById('filter-clear').classList.add('hidden');
// Programmatic-clear path. The HeaderSearch's own clear
// button calls this through the filter registration's
// onClear handler.
__logFilterQuery = '';
filterLogs();
input.focus();
}

function escapeHtml(text) {
Expand Down Expand Up @@ -1053,6 +1041,87 @@ templ Logs(user *models.User, servers []models.BoxConfig) {

// Initialize SSE for real-time log streaming
initSSE();

// Register with the unified HeaderSearch + command palette
// (issue #92). Each keystroke in search mode updates the
// internal __logFilterQuery and re-runs filterLogs(); the
// command palette gets entries for every toolbar button so
// they're all reachable via Cmd+K → ">" → fuzzy search.
if (window.gearbox && window.gearbox.filter) {
window.gearbox.filter.register({
placeholder: 'Filter log lines…',
onInput: function (q) {
__logFilterQuery = (q || '').toLowerCase();
filterLogs();
},
onClear: function () {
__logFilterQuery = '';
filterLogs();
},
});
}
if (window.gearbox && window.gearbox.commands) {
const cmds = window.gearbox.commands;
// Log source — one entry per server-side option. If
// the user adds a third log type in the future this
// loop picks it up automatically.
const logSel = document.getElementById('log-select');
if (logSel) {
Array.from(logSel.options).forEach(function (opt) {
cmds.register({
id: 'logs.source.' + opt.value,
label: 'Log source: ' + opt.text,
subtitle: 'Switch the log feed to ' + opt.text.toLowerCase(),
group: 'Logs',
run: function () {
logSel.value = opt.value;
if (typeof onLogSourceChange === 'function') onLogSourceChange();
},
});
});
}
// Line counts — exposed as palette entries so the user
// can keyboard-pick "200" without opening the dropdown.
const linesSel = document.getElementById('lines-select');
if (linesSel) {
Array.from(linesSel.options).forEach(function (opt) {
cmds.register({
id: 'logs.lines.' + opt.value,
label: 'Show ' + opt.text + ' lines',
subtitle: 'Fetch the last ' + opt.text + ' log lines',
group: 'Logs',
run: function () {
linesSel.value = opt.value;
if (typeof loadLogs === 'function') loadLogs();
},
});
});
}
// Severity — same pattern.
const sevSel = document.getElementById('severity-filter');
if (sevSel) {
Array.from(sevSel.options).forEach(function (opt) {
cmds.register({
id: 'logs.severity.' + opt.value,
label: 'Severity: ' + opt.text,
subtitle: 'Show only ' + opt.text.toLowerCase() + ' lines',
group: 'Logs',
run: function () {
sevSel.value = opt.value;
if (typeof filterLogs === 'function') filterLogs();
},
});
});
}
cmds.register({ id: 'logs.refresh', label: 'Refresh logs', group: 'Logs',
subtitle: 'Re-fetch the log feed', run: function () { if (typeof manualRefresh === 'function') manualRefresh(); }});
cmds.register({ id: 'logs.copy', label: 'Copy logs', group: 'Logs',
subtitle: 'Copy currently-visible lines to the clipboard', run: function () { if (typeof copyLogs === 'function') copyLogs(); }});
cmds.register({ id: 'logs.download', label: 'Download logs', group: 'Logs',
subtitle: 'Save visible lines as a .log file', run: function () { if (typeof downloadLogs === 'function') downloadLogs(); }});
cmds.register({ id: 'logs.fullscreen', label: 'Toggle fullscreen', group: 'Logs',
subtitle: 'Cycle: normal → browser fullscreen → true fullscreen', run: function () { if (typeof toggleFullscreen === 'function') toggleFullscreen(); }});
}
});

// Clean up SSE on page unload
Expand Down
21 changes: 21 additions & 0 deletions gearbox/internal/framework/templates/pages/metrics.templ
Original file line number Diff line number Diff line change
Expand Up @@ -2822,6 +2822,27 @@ templ Metrics(user *models.User, servers []models.BoxConfig) {
if (slider) {
slider.addEventListener('input', updateResolutionLabel);
}

// Command palette wiring (issue #92).
if (window.gearbox && window.gearbox.commands) {
const cmds = window.gearbox.commands;
cmds.register({
id: 'metrics.toggle-edit',
label: 'Toggle edit mode',
subtitle: 'Enter drag-to-arrange + resize mode for chart tiles',
group: 'Metrics',
run: function () {
const btn = document.getElementById('metrics-edit-toggle');
if (btn) btn.click();
},
});
cmds.register({
id: 'metrics.refresh',
label: 'Refresh metrics',
group: 'Metrics',
run: function () { if (typeof loadHistoryData === 'function') loadHistoryData(); },
});
}
});

// Cleanup on page unload
Expand Down
21 changes: 9 additions & 12 deletions gearbox/internal/framework/templates/pages/os_updates.templ
Original file line number Diff line number Diff line change
Expand Up @@ -200,18 +200,15 @@ templ OSUpdatesPage(data OSUpdatesPageData) {
</button>
}
}
<span class="datagrid-search-wrap">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"></path>
</svg>
<input
type="search"
id="pkg-search"
placeholder="Search packages..."
class="h-[38px] pr-3 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 w-56"
autocomplete="off"
/>
</span>
<!-- #pkg-search moved to the global HeaderSearch
(issue #92). The element is kept here as a
hidden input so the Tabulator-backed
datagrid (createDataGrid({searchInput:
'#pkg-search'})) can still latch onto it;
a window.gearbox.filter registration in
os-updates-page.js writes to .value and
dispatches 'input' on every keystroke. -->
<input type="search" id="pkg-search" class="hidden" autocomplete="off"/>
<select
id="pkg-view-filter"
onchange="onPkgViewFilterChange(this.value)"
Expand Down
Loading
Loading