π₯οΈπ Real-time log viewer and search dashboard built with React, Vite and WebSockets.
The companion frontend for logstream-server. Streams live log entries over WebSocket and provides a full-featured search and inspection UI β no build-time configuration required.
- Real-time log stream β new entries appear instantly via WebSocket, with a live/buffered toggle and slide-in animation
- Full-text search and filters β filter by application, environment, level, category, trace ID, batch ID, date range, or message substring, all persisted across page refreshes
- Expandable rows β click any entry to reveal the full message, structured JSON context with syntax highlighting, user agent, and one-click batch/trace navigation
- Stats drawer β session-level counts by log level, top apps by volume, and total entries received
- Row virtualisation β large result sets (200+ rows) are windowed for smooth scrolling
- Zero build-time config β all connection settings (API URL, WebSocket URL, UI secret) are entered via a settings screen and stored in
localStorage; no.envfile required - Auth-aware β separate read key (
UI_SECRET) and write key (API_SECRET) enforced by the backend; 401 responses and WebSocket auth failures redirect to the settings screen without retry loops - Dark mode only β purpose-built dark theme with colour-coded log level badges
Add screenshots here after first deploy.
| Layer | Choice |
|---|---|
| Framework | React 18 + Vite + TypeScript |
| Styling | Tailwind CSS (no component library) |
| Data fetching | TanStack Query (react-query) |
| Date formatting | date-fns |
| State | React Context + useState (no Redux) |
| Config persistence | localStorage |
- Node.js 18 or later
- A running logstream-server instance
git clone https://github.com/guibranco/logstream-ui.git
cd logstream-ui
npm install
npm run devOpen http://localhost:5173 in your browser. On first load you will see the Connection Settings screen.
There are no .env files or build-time variables. All settings are entered once via the UI and stored in your browser's localStorage under the key logservice:config.
The settings screen appears automatically on first visit and can be re-opened at any time from the β button in the header.
| Field | Description | Example |
|---|---|---|
| API Base URL | The logstream-server HTTP address | https://logs.straccini.com |
| WebSocket URL | The WebSocket endpoint | wss://logs.straccini.com/ws |
| UI Secret | The read-only bearer token (UI_SECRET from the server's .env) |
your-ui-secret-here |
The WebSocket URL is auto-suggested as you type the API URL (https://foo.com β wss://foo.com/ws).
Security note: The UI secret is stored in
localStorageso it survives browser restarts. It is sent as aBearertoken inAuthorizationheaders for HTTP requests and as a?token=query parameter on the WebSocket connection URL (browser WebSocket APIs do not support custom headers). It is never included in the visible browser address bar.
Open β Settings β scroll to the bottom β Clear all saved settings and disconnect. Confirm the dialog to wipe the config and return to the blank settings form.
Alternatively, click Disconnect in the header to clear and return to the settings screen immediately.
The backend enforces two separate keys β the UI only ever uses the read key:
| Key | Who uses it | What it protects |
|---|---|---|
API_SECRET (write key) |
Your backend services | POST /api/logs |
UI_SECRET (read key) |
This UI | GET /api/logs, GET /api/logs/:id, WebSocket |
The health endpoint (GET /api/health) is public and requires no token.
The UI connects to wss://host/ws?token=<UI_SECRET>. On success the server sends { type: "connected" }. If the token is wrong the server sends { type: "error" } and closes the connection β the UI detects this and shows a persistent error banner without retrying (to avoid an infinite auth-fail loop).
Any 401 response clears the stored config and redirects to the settings screen with the message "Session expired or secret rejected β update your settings."
| Element | Description |
|---|---|
| Pulse dot | WebSocket status β green (connected), amber (reconnecting), red (disconnected / auth error) |
| Connection count | Live WebSocket client count from the server |
| Live toggle | When ON, new entries prepend in real-time. When OFF, incoming entries buffer and show a "N new entries" badge |
| Clear live | Clears the in-memory live buffer without affecting search results |
| β Settings | Opens the settings screen pre-filled with current values |
| Disconnect | Clears all saved settings and returns to the settings screen |
All filters are combined and sent as a single GET /api/logs query. The filter state persists in localStorage["logservice:filters"] across page refreshes.
| Filter | Match type |
|---|---|
| Search | Substring on message |
| App Key | Exact |
| App ID | Exact |
| Level | Multi-select pills |
| Category | Partial |
| Date From / To | ISO 8601 range |
| Trace ID | Exact |
| Batch ID | Exact |
Timestamp Β· Level Β· App Key Β· App ID Β· Category Β· Message Β· Trace ID Β· Actions
- Timestamp β shown as relative time ("2 min ago"); full ISO on hover
- Level β colour-coded pill badge (see level colours below)
- Message β truncated to 80 chars; full text in expanded row
- Trace ID β first 8 chars +
β¦; click to copy the full ID to clipboard
Click any row to reveal:
- Full message text
batch_idβ click to auto-fill the Batch ID filteruser_agentcontextβ pretty-printed JSON with syntax highlighting (recursive component, no library)- View all in batch β sets the batch_id filter and re-runs the search
- View trace β sets the trace_id filter and re-runs the search
| Level | Text | Background |
|---|---|---|
debug |
gray-400 |
gray-800 |
info |
sky-400 |
sky-900 |
notice |
teal-400 |
teal-900 |
warning |
amber-400 |
amber-900 |
error |
rose-400 |
rose-900 |
critical |
fuchsia-400 |
fuchsia-900 |
Click Stats βΈ (floating button) to open a side drawer showing:
- Entry counts per level with proportional CSS bars
- Top 5
app_keyvalues seen in the live stream - Total live entries received this session
All stats are derived from the in-memory live buffer β they reset on page reload.
src/
βββ types.ts LogEntry, SearchFilters, LogServiceConfig
βββ store/
β βββ configStore.ts localStorage read / write / clear helpers
βββ context/
β βββ AuthContext.tsx Active config + openSettings() + signOut()
βββ hooks/
β βββ useWebSocket.ts Auto-reconnecting WS hook with backoff
β βββ useLogs.ts TanStack Query wrapper for GET /api/logs
βββ components/
β βββ SettingsScreen.tsx First-visit and edit-mode settings form
β βββ Header.tsx Sticky top bar
β βββ FilterBar.tsx Sticky search + filter controls
β βββ LogTable.tsx Virtualised log entry table
β βββ LogRow.tsx Single table row
β βββ LogDetail.tsx Expanded row detail panel
β βββ LevelBadge.tsx Colour-coded level pill
β βββ StatsDrawer.tsx Session stats slide-in panel
β βββ JsonViewer.tsx Recursive JSON syntax highlighter
β βββ Skeleton.tsx Loading placeholder rows
β βββ AuthErrorBanner.tsx Persistent auth failure banner
βββ App.tsx
βββ main.tsx
npm run buildThe output is in dist/. Serve it with any static host β Nginx, Caddy, Vercel, Netlify, GitHub Pages, or an S3 bucket.
Example Nginx config to serve the built app at the root alongside the API proxy:
# Serve the built React app
location / {
root /var/www/logstream-ui/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
# Proxy the API and WebSocket (same domain, no CORS)
location /api/ {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}# Start the dev server with hot reload
npm run dev
# Type check
npm run type-check
# Lint
npm run lint
# Build + preview the production bundle locally
npm run build && npm run previewOn first load, enter these values in the settings screen:
| Field | Value |
|---|---|
| API Base URL | http://localhost:8081 |
| WebSocket URL | ws://localhost:8080/ws |
| UI Secret | the UI_SECRET from your local .env |
- logstream-server β the PHP 8.3 + ReactPHP backend this UI connects to
- API documentation β live Swagger UI
MIT