Compare commits

...

2 Commits

Author SHA1 Message Date
271ff151a1 single out toes logs on dashboard 2026-02-15 07:47:41 -08:00
3eef4c2a0e fix ts errors 2026-02-15 07:40:58 -08:00
6 changed files with 82 additions and 10 deletions

View File

@ -86,9 +86,9 @@ function parseTime(s: string): { hour: number, minute: number } | null {
// 12h: "7am", "7pm", "7:30am", "7:30pm", "12am", "12:00pm" // 12h: "7am", "7pm", "7:30am", "7:30pm", "12am", "12:00pm"
const m12 = s.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/i) const m12 = s.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/i)
if (m12) { if (m12) {
let hour = parseInt(m12[1]) let hour = parseInt(m12[1]!)
const minute = m12[2] ? parseInt(m12[2]) : 0 const minute = m12[2] ? parseInt(m12[2]) : 0
const period = m12[3].toLowerCase() const period = m12[3]!.toLowerCase()
if (hour < 1 || hour > 12 || minute > 59) return null if (hour < 1 || hour > 12 || minute > 59) return null
if (period === 'am' && hour === 12) hour = 0 if (period === 'am' && hour === 12) hour = 0
else if (period === 'pm' && hour !== 12) hour += 12 else if (period === 'pm' && hour !== 12) hour += 12
@ -98,8 +98,8 @@ function parseTime(s: string): { hour: number, minute: number } | null {
// 24h: "14:00", "0:00", "23:59" // 24h: "14:00", "0:00", "23:59"
const m24 = s.match(/^(\d{1,2}):(\d{2})$/) const m24 = s.match(/^(\d{1,2}):(\d{2})$/)
if (m24) { if (m24) {
const hour = parseInt(m24[1]) const hour = parseInt(m24[1]!)
const minute = parseInt(m24[2]) const minute = parseInt(m24[2]!)
if (hour > 23 || minute > 59) return null if (hour > 23 || minute > 59) return null
return { hour, minute } return { hour, minute }
} }

View File

@ -4,6 +4,8 @@ import {
LogsBody, LogsBody,
LogsHeader, LogsHeader,
LogsSection, LogsSection,
LogsTab,
LogsTabs,
LogsTitle, LogsTitle,
LogText, LogText,
LogTimestamp, LogTimestamp,
@ -16,8 +18,11 @@ export interface UnifiedLogLine {
text: string text: string
} }
type LogFilter = 'all' | 'toes'
const MAX_LOGS = 200 const MAX_LOGS = 200
let _filter: LogFilter = 'all'
let _logs: UnifiedLogLine[] = [] let _logs: UnifiedLogLine[] = []
let _source: EventSource | undefined let _source: EventSource | undefined
@ -54,7 +59,6 @@ function parseLogText(text: string): { method?: string, path?: string, status?:
function LogLineEntry({ log }: { log: UnifiedLogLine }) { function LogLineEntry({ log }: { log: UnifiedLogLine }) {
const parsed = parseLogText(log.text) const parsed = parseLogText(log.text)
const statusColor = getStatusColor(parsed.status) const statusColor = getStatusColor(parsed.status)
return ( return (
<LogEntry> <LogEntry>
<LogTimestamp>{formatTime(log.time)}</LogTimestamp> <LogTimestamp>{formatTime(log.time)}</LogTimestamp>
@ -66,21 +70,31 @@ function LogLineEntry({ log }: { log: UnifiedLogLine }) {
) )
} }
const filteredLogs = (): UnifiedLogLine[] =>
_filter === 'toes' ? _logs.filter(l => l.app === 'toes') : _logs
function setFilter(filter: LogFilter) {
_filter = filter
renderLogs()
}
function LogsBodyContent() { function LogsBodyContent() {
const logs = filteredLogs()
return ( return (
<> <>
{_logs.length === 0 ? ( {logs.length === 0 ? (
<LogEntry> <LogEntry>
<LogText style={{ color: 'var(--colors-textFaint)' }}>No activity yet</LogText> <LogText style={{ color: 'var(--colors-textFaint)' }}>No activity yet</LogText>
</LogEntry> </LogEntry>
) : ( ) : (
_logs.map((log, i) => <LogLineEntry key={i} log={log} />) logs.map((log, i) => <LogLineEntry key={i} log={log} />)
)} )}
</> </>
) )
} }
function renderLogs() { function renderLogs() {
update('#unified-logs-tabs', <LogsTabsBar />)
update('#unified-logs-body', <LogsBodyContent />) update('#unified-logs-body', <LogsBodyContent />)
// Auto-scroll after render // Auto-scroll after render
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -101,11 +115,21 @@ export function initUnifiedLogs() {
} }
} }
function LogsTabsBar() {
return (
<LogsTabs>
<LogsTab variant={_filter === 'all' ? 'active' : undefined} onClick={() => setFilter('all')}>All</LogsTab>
<LogsTab variant={_filter === 'toes' ? 'active' : undefined} onClick={() => setFilter('toes')}>Toes</LogsTab>
</LogsTabs>
)
}
export function UnifiedLogs() { export function UnifiedLogs() {
return ( return (
<LogsSection> <LogsSection>
<LogsHeader> <LogsHeader>
<LogsTitle>Logs</LogsTitle> <LogsTitle>Logs</LogsTitle>
<div id="unified-logs-tabs"><LogsTabsBar /></div>
</LogsHeader> </LogsHeader>
<LogsBody id="unified-logs-body"> <LogsBody id="unified-logs-body">
<LogsBodyContent /> <LogsBodyContent />

View File

@ -97,6 +97,36 @@ export const LogsClearButton = define('LogsClearButton', {
}, },
}) })
export const LogsTabs = define('LogsTabs', {
display: 'flex',
gap: 0,
})
export const LogsTab = define('LogsTab', {
base: 'button',
background: 'none',
border: 'none',
borderBottom: '2px solid transparent',
padding: '0 8px 8px',
fontSize: 11,
fontWeight: 600,
color: theme('colors-textFaint'),
cursor: 'pointer',
textTransform: 'uppercase',
letterSpacing: '0.05em',
selectors: {
'&:hover': {
color: theme('colors-text'),
},
},
variants: {
active: {
color: theme('colors-text'),
borderBottomColor: theme('colors-text'),
},
},
})
export const LogsBody = define('LogsBody', { export const LogsBody = define('LogsBody', {
height: 200, height: 200,
overflow: 'auto', overflow: 'auto',

View File

@ -9,6 +9,8 @@ export {
LogsClearButton, LogsClearButton,
LogsHeader, LogsHeader,
LogsSection, LogsSection,
LogsTab,
LogsTabs,
LogsTitle, LogsTitle,
LogStatus, LogStatus,
LogText, LogText,

View File

@ -1,4 +1,5 @@
import { allApps, APPS_DIR, onChange } from '$apps' import { allApps, APPS_DIR, onChange } from '$apps'
import { onHostLog } from '../tui'
import { Hype } from '@because/hype' import { Hype } from '@because/hype'
import { cpus, platform, totalmem } from 'os' import { cpus, platform, totalmem } from 'os'
import { join } from 'path' import { join } from 'path'
@ -258,7 +259,17 @@ function collectLogs() {
} }
} }
function pushHostLog(text: string) {
const line: UnifiedLogLine = { time: Date.now(), app: 'toes', text }
unifiedLogs.push(line)
if (unifiedLogs.length > MAX_UNIFIED_LOGS) unifiedLogs.shift()
for (const listener of unifiedLogListeners) listener(line)
}
// Subscribe to app changes to collect logs // Subscribe to app changes to collect logs
onChange(collectLogs) onChange(collectLogs)
// Subscribe to host-level log messages
onHostLog(pushHostLog)
export default router export default router

View File

@ -7,8 +7,13 @@ let _apps: App[] = []
let _enabled = (process.stdout.isTTY ?? false) && !process.env.DEBUG let _enabled = (process.stdout.isTTY ?? false) && !process.env.DEBUG
let _lastRender = 0 let _lastRender = 0
let _renderTimer: Timer | undefined let _renderTimer: Timer | undefined
let _hostLogListeners = new Set<(text: string) => void>()
let _showEmoji = false let _showEmoji = false
export const onHostLog = (cb: (text: string) => void) => {
_hostLogListeners.add(cb)
}
export const setShowEmoji = (show: boolean) => { export const setShowEmoji = (show: boolean) => {
_showEmoji = show _showEmoji = show
scheduleRender() scheduleRender()
@ -21,9 +26,9 @@ export function appLog(app: App, ...msg: string[]) {
} }
export function hostLog(...msg: string[]) { export function hostLog(...msg: string[]) {
if (!_enabled) { const text = msg.join(' ')
console.log('🐾', msg.join(' ')) if (!_enabled) console.log('🐾', text)
} for (const listener of _hostLogListeners) listener(text)
} }
export function setApps(apps: App[]) { export function setApps(apps: App[]) {