diff --git a/src/client/components/DashboardLanding.tsx b/src/client/components/DashboardLanding.tsx
index 9db113c..3a1b848 100644
--- a/src/client/components/DashboardLanding.tsx
+++ b/src/client/components/DashboardLanding.tsx
@@ -1,26 +1,25 @@
import { useEffect } from 'hono/jsx'
import { openAppSelectorModal } from '../modals'
-import { apps, isNarrow, setCurrentView, setSelectedApp } from '../state'
+import { dashboardTab, isNarrow, setCurrentView, setDashboardTab, setSelectedApp } from '../state'
import {
AppSelectorChevron,
DashboardContainer,
DashboardHeader,
DashboardTitle,
SettingsGear,
- StatusDot,
- StatusDotLink,
- StatusDotsRow,
+ Tab,
+ TabBar,
+ TabContent,
} from '../styles'
-import { update } from '../update'
-import { UnifiedLogs, initUnifiedLogs } from './UnifiedLogs'
+import { UnifiedLogs, initUnifiedLogs, scrollLogsToBottom } from './UnifiedLogs'
+import { Urls } from './Urls'
import { Vitals, initVitals } from './Vitals'
-let activeTooltip: string | null = null
-
export function DashboardLanding({ render }: { render: () => void }) {
useEffect(() => {
initUnifiedLogs()
initVitals()
+ if (dashboardTab === 'logs') scrollLogsToBottom()
}, [])
const narrow = isNarrow || undefined
@@ -31,6 +30,12 @@ export function DashboardLanding({ render }: { render: () => void }) {
render()
}
+ const switchTab = (tab: typeof dashboardTab) => {
+ setDashboardTab(tab)
+ render()
+ if (tab === 'logs') scrollLogsToBottom()
+ }
+
return (
void }) {
-
- {[...apps.filter(a => !a.tool), ...apps.filter(a => a.tool)].map(app => (
- {
- e.preventDefault()
- if (isNarrow && activeTooltip !== app.name) {
- activeTooltip = app.name
- render()
- return
- }
- activeTooltip = null
- setSelectedApp(app.name)
- update()
- }}
- >
-
-
- ))}
-
+
+ switchTab('urls')}>URLs
+ switchTab('logs')}>Logs
+ switchTab('metrics')}>Metrics
+
-
+
+
+
-
+
+
+
+
+
+
+
)
}
diff --git a/src/client/components/UnifiedLogs.tsx b/src/client/components/UnifiedLogs.tsx
index 569fbd1..cd4a070 100644
--- a/src/client/components/UnifiedLogs.tsx
+++ b/src/client/components/UnifiedLogs.tsx
@@ -105,6 +105,13 @@ function renderLogs() {
})
}
+export function scrollLogsToBottom() {
+ requestAnimationFrame(() => {
+ const el = document.getElementById('unified-logs-body')
+ if (el) el.scrollTop = el.scrollHeight
+ })
+}
+
export function initUnifiedLogs() {
if (_source) return
_source = new EventSource('/api/system/logs/stream')
diff --git a/src/client/components/Urls.tsx b/src/client/components/Urls.tsx
new file mode 100644
index 0000000..60aa5fd
--- /dev/null
+++ b/src/client/components/Urls.tsx
@@ -0,0 +1,42 @@
+import { buildAppUrl } from '../../shared/urls'
+import { apps } from '../state'
+import {
+ EmptyState,
+ StatusDot,
+ UrlLeft,
+ UrlLink,
+ UrlList,
+ UrlPort,
+ UrlRow,
+} from '../styles'
+
+export function Urls() {
+ const nonTools = apps.filter(a => !a.tool)
+
+ if (nonTools.length === 0) {
+ return No apps installed
+ }
+
+ return (
+
+ {nonTools.map(app => {
+ const url = buildAppUrl(app.name, location.origin)
+ const running = app.state === 'running'
+
+ return (
+
+
+
+ {running ? (
+ {url}
+ ) : (
+ {app.name}
+ )}
+
+ {app.port ? :{app.port} : null}
+
+ )
+ })}
+
+ )
+}
diff --git a/src/client/state.ts b/src/client/state.ts
index a01c574..f5c97b3 100644
--- a/src/client/state.ts
+++ b/src/client/state.ts
@@ -1,10 +1,13 @@
import type { App } from '../shared/types'
+export type DashboardTab = 'urls' | 'logs' | 'metrics'
+
// UI state (survives re-renders)
export let currentView: 'dashboard' | 'settings' = 'dashboard'
export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches
export let selectedApp: string | null = localStorage.getItem('selectedApp')
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
+export let dashboardTab: DashboardTab = (localStorage.getItem('dashboardTab') as DashboardTab) || 'urls'
export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps'
// Server state (from SSE)
@@ -14,6 +17,11 @@ export let apps: App[] = []
export let appTabs: Record = JSON.parse(localStorage.getItem('appTabs') || '{}')
// State setters
+export function setDashboardTab(tab: DashboardTab) {
+ dashboardTab = tab
+ localStorage.setItem('dashboardTab', tab)
+}
+
export function setCurrentView(view: 'dashboard' | 'settings') {
currentView = view
}
diff --git a/src/client/styles/dashboard.ts b/src/client/styles/dashboard.ts
index c144429..9e3d9b7 100644
--- a/src/client/styles/dashboard.ts
+++ b/src/client/styles/dashboard.ts
@@ -202,3 +202,59 @@ export const LogStatus = define('LogStatus', {
warning: { color: '#f59e0b' },
},
})
+
+// URL List
+export const UrlLeft = define('UrlLeft', {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+ minWidth: 0,
+})
+
+export const UrlLink = define('UrlLink', {
+ base: 'a',
+ color: theme('colors-link'),
+ textDecoration: 'none',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ selectors: {
+ '&:hover': { textDecoration: 'underline' },
+ },
+})
+
+export const UrlList = define('UrlList', {
+ width: '100%',
+ minWidth: 400,
+ maxWidth: 800,
+ display: 'flex',
+ flexDirection: 'column',
+ background: theme('colors-bgElement'),
+ border: `1px solid ${theme('colors-border')}`,
+ borderRadius: theme('radius-md'),
+ overflow: 'hidden',
+})
+
+export const UrlPort = define('UrlPort', {
+ fontFamily: theme('fonts-mono'),
+ fontSize: 12,
+ color: theme('colors-textFaint'),
+ flexShrink: 0,
+})
+
+export const UrlRow = define('UrlRow', {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '10px 16px',
+ fontFamily: theme('fonts-mono'),
+ fontSize: 13,
+ selectors: {
+ '&:hover': {
+ background: theme('colors-bgHover'),
+ },
+ '&:not(:last-child)': {
+ borderBottom: `1px solid ${theme('colors-border')}`,
+ },
+ },
+})
diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts
index 402c6b7..3e33da7 100644
--- a/src/client/styles/index.ts
+++ b/src/client/styles/index.ts
@@ -15,6 +15,11 @@ export {
LogStatus,
LogText,
LogTimestamp,
+ UrlLeft,
+ UrlLink,
+ UrlList,
+ UrlPort,
+ UrlRow,
VitalCard,
VitalLabel,
VitalsSection,
diff --git a/src/client/styles/misc.ts b/src/client/styles/misc.ts
index 1b85faf..d598041 100644
--- a/src/client/styles/misc.ts
+++ b/src/client/styles/misc.ts
@@ -127,6 +127,12 @@ export const TabBar = define('TabBar', {
display: 'flex',
gap: 24,
marginBottom: 20,
+ variants: {
+ centered: {
+ justifyContent: 'center',
+ marginBottom: 0,
+ },
+ },
})
export const Tab = define('Tab', {