Skip to content

Crow's Nest

The Crow's Nest (servers/gateway/dashboard/) is a server-rendered web interface for managing a Crow instance. (The code directory is still named dashboard/ for backward compatibility; the user-facing name is "Crow's Nest.") It uses no frontend framework — HTML is generated server-side and served directly by the gateway.

Brand Identity

The Crow's Nest uses a cool blue-black palette with indigo accents, defined as CSS custom properties in servers/gateway/dashboard/shared/layout.js.

Color Tokens (Dark — :root)

TokenValueUsage
--crow-bg-deep#0f0f17Page background
--crow-bg-surface#1a1a2eCard/panel backgrounds
--crow-bg-elevated#2d2d3dRaised surfaces, hover states
--crow-border#3d3d4dBorders, dividers
--crow-text-primary#fafaf9Headings, body text
--crow-text-secondary#a8a29eDescriptions, labels
--crow-text-muted#78716cHints, disabled text
--crow-accent#6366f1Primary accent (indigo)
--crow-accent-hover#818cf8Hover state for accent
--crow-accent-muted#2d2854Subtle accent backgrounds
--crow-brand-gold#fbbf24Active nav highlight, branding
--crow-success#22c55eSuccess states
--crow-error#ef4444Error states
--crow-info#38bdf8Informational highlights

Color Tokens (Light — .theme-light)

TokenValue
--crow-bg-deep#fafaf9
--crow-bg-surface#ffffff
--crow-bg-elevated#f5f5f4
--crow-border#e7e5e4
--crow-text-primary#1c1917
--crow-text-secondary#57534e
--crow-text-muted#a8a29e
--crow-accent#4f46e5
--crow-accent-hover#6366f1
--crow-accent-muted#e0e7ff

Typography

  • Headings: Fraunces (serif, variable weight)
  • Body: DM Sans (sans-serif)
  • Code: JetBrains Mono (monospace)

All three are loaded via Google Fonts in the layout <style> block.

Visual Details

  • Card depth via layered box-shadow (subtle glow on elevated surfaces)
  • Gold accent (--crow-brand-gold) on the active sidebar navigation item
  • Illustrated empty states with inline crow SVG icons
  • Login page and setup page display a crow hero graphic

Architecture

┌────────────────────────────────────────┐
│           Panel Registry               │
│  health │ messages │ memory │ blog    │
│  files │ extensions │ settings         │
│  + third-party panels from ~/.crow/    │
├────────────────────────────────────────┤
│           Layout System                │
│  layout(title, content, options)       │
│  Navigation, theme toggle, footer     │
├────────────────────────────────────────┤
│           Auth System                  │
│  scrypt hashing, session cookies      │
│  CSRF tokens, account lockout         │
├────────────────────────────────────────┤
│           Network Security             │
│  IP allowlist (LAN, Tailscale)        │
│  403 for disallowed origins            │
├────────────────────────────────────────┤
│           Express Router               │
│  GET/POST /dashboard/*                 │
└────────────────────────────────────────┘

Panel Registry

Panels are modular sections of the Crow's Nest. Each panel registers itself with:

js
{
  id: 'messages',          // Unique identifier
  name: 'Messages',        // Display name in navigation
  icon: 'mail',            // Icon identifier
  route: '/dashboard/messages',
  navOrder: 1,             // Position in the navigation bar
  handler: async (req, res, { db, layout }) => {
    // Render panel content
  }
}

Built-in panels live in servers/gateway/dashboard/panels/:

PanelFileRoutePurpose
Crow's Nestpanels/health.js/dashboard/nestApp launcher tiles, CPU, RAM, disk usage, Docker containers, DB metrics
Messagespanels/messages.js/dashboard/messagesView peer messages, threads, read status
Memorypanels/memory.js/dashboard/memoryBrowse, search, and manage persistent memories
Blogpanels/blog.js/dashboard/blogManage posts, publish/unpublish, edit
Filespanels/files.js/dashboard/filesBrowse storage, upload, delete, preview
Extensionspanels/extensions.js/dashboard/extensionsBrowse marketplace, install/uninstall add-ons, resource warnings
Settingspanels/settings.js/dashboard/settingsConfiguration, quotas, network rules, contact discovery

Auth System

The Crow's Nest uses its own authentication layer, separate from the gateway's OAuth system.

Password Hashing

Passwords are hashed with Node.js's built-in crypto.scrypt:

js
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
  // Store salt + derivedKey
});

No external dependency required.

Sessions

After login, a session cookie is set with:

  • httpOnly: true — Not accessible to client-side JavaScript
  • sameSite: 'strict' — Prevents CSRF via cross-origin requests
  • secure: true — Only sent over HTTPS (when behind a reverse proxy)
  • Configurable expiry (default: 24 hours)

CSRF Protection

All state-changing requests (POST, PUT, DELETE) require a CSRF token. The token is embedded in forms as a hidden field and validated server-side.

Account Lockout

After 5 failed login attempts within 15 minutes, the account is locked for 30 minutes. This prevents brute-force attacks on the Crow's Nest password.

Layout System

The layout() function wraps panel content in a consistent page structure:

js
function layout(title, content, options = {}) {
  return `<!DOCTYPE html>
  <html data-theme="${options.theme || 'dark'}">
  <head>
    <title>${title} — Crow's Nest</title>
    ${styles}
  </head>
  <body>
    ${navigation(options.activePanel)}
    <main>${content}</main>
    ${footer}
  </body>
  </html>`;
}

Everything is a template literal — no template engine dependency. CSS is inlined in the <head> to avoid a separate static file server.

Network Security

Before any Crow's Nest route executes, middleware checks the request's source IP:

js
const ALLOWED_RANGES = [
  '127.0.0.1/32',       // Localhost
  '::1/128',            // Localhost IPv6
  '10.0.0.0/8',         // LAN Class A
  '172.16.0.0/12',      // LAN Class B
  '192.168.0.0/16',     // LAN Class C
  '100.64.0.0/10',      // Tailscale CGNAT
];

Requests from outside these ranges receive a 403 Forbidden response. To allow access from any IP (e.g., behind a reverse proxy), set CROW_DASHBOARD_PUBLIC=true.

The middleware reads X-Forwarded-For when the gateway is behind a reverse proxy, but only trusts it if the immediate connection comes from a known proxy IP.

App Launcher

The Crow's Nest landing page (the "Crow's Nest" panel, navOrder: 5) includes a Your Apps grid showing installed add-ons as launcher tiles.

How it works

  1. Reads ~/.crow/installed.json and filters entries with type bundle or mcp-server
  2. Loads the add-on manifest to get the display name and webUI field
  3. Calls getAddonLogo(id, 48) from servers/gateway/dashboard/shared/logos.js for the tile icon (falls back to an initial-letter circle)
  4. For Docker-based add-ons, checks container status via docker ps --filter name=<id> with a 30-second module-level cache (_dockerStatusCache Map) to avoid excessive shell commands
  5. Renders a status dot (green = running, gray = stopped) and an "Open" button for add-ons with a webUI manifest field

Home Screen Tile Pipeline

The Nest home screen renders tiles from two sources:

  1. Panel RegistrygetVisiblePanels() returns non-hidden panels sorted by navOrder
  2. Installed bundlesgetNestData() reads ~/.crow/installed.json, loads manifests, checks Docker status

Data flow:

Panel Registry ──→ getVisiblePanels() ──┐
                                        ├──→ buildNestHTML() ──→ Grid
~/.crow/installed.json ──→ getNestData() ──┘

Tile ordering: Built-in panels first (by navOrder), then bundles (by installedAt from installed.json).

Icon resolution (bundles): Branded SVG logo → manifest icon field → first-letter circle fallback.

webUI manifest field

Add-on manifests can declare a webUI object to indicate the add-on has a browser-accessible interface:

json
{
  "webUI": {
    "port": 8080,
    "path": "/",
    "label": "Open Nextcloud"
  }
}

Set webUI to null for headless add-ons (e.g., Ollama). The launcher only shows the "Open" button when webUI is non-null.

Panel Auto-Installation

Add-ons that include a panel field in their manifest.json get their panel file automatically installed during add-on installation and removed during uninstallation. This works for any add-on type (bundle, mcp-server, skill), not just panel-type add-ons.

During install, routes/bundles.js copies the panel file from the add-on's source directory to ~/.crow/panels/ and adds its ID to ~/.crow/panels.json. During uninstall, the panel file is removed and the ID is deleted from the JSON. Example manifest field:

json
{
  "panel": "panels/podcast.js"
}

The Podcast panel (bundles/podcast/panels/podcast.js) is an example: it is installed as a third-party panel when the podcast add-on is installed.

PanelTypeSource
PodcastThird-party (auto-installed)bundles/podcast/panels/podcast.js

Third-Party Panels

Community-created panels live in ~/.crow/panels/. Each panel is a directory or JS file. The Crow's Nest scans this directory on startup and registers any valid panels. Third-party panels receive the same { db, layout, appRoot } context as built-in panels. The appRoot path points to the Crow source root, which panels can use for dynamic imports of shared components (e.g., logos.js, components.js).

Enable panels in ~/.crow/panels.json (a JSON array of panel IDs):

json
["my-panel", "weather"]

An object format with an "enabled" key is also accepted for backward compatibility.

See Creating Panels for a development tutorial.

Notification System

The Crow's Nest includes a notification system with a bell icon and tamagotchi-style dropdown in the top bar.

Schema

The notifications table stores all notifications:

ColumnTypeDescription
typetextreminder, media, peer, or system
sourcetextOrigin identifier (e.g., blog, sharing:message, bundle-installer)
titletextShort headline
bodytextOptional longer description
prioritytextlow, normal, or high
action_urltextDashboard link for click-through
is_readintegerRead status
is_dismissedintegerDismissed status
expires_attextAuto-expiry timestamp

Shared Helper

servers/shared/notifications.js exports two functions:

  • createNotification(db, opts) — Creates a notification after checking user preferences. Returns { id } or null if the type is disabled. Always wrap calls in try/catch to prevent notification failures from breaking primary actions.
  • cleanupNotifications(db) — Removes expired notifications and enforces a 500-notification retention limit. Called by the scheduler tick and the REST GET endpoint.

User Preferences

Users configure which notification types are enabled in Settings → Notifications. Preferences are stored as a JSON object in dashboard_settings under the key notification_prefs:

json
{ "types_enabled": ["reminder", "media", "peer", "system"] }

All types are enabled by default. The createNotification helper checks this before inserting.

Event Sources

EventTypeSource
Blog post publishedmediablog
Incoming P2P sharepeersharing:share
Incoming Nostr messagepeersharing:message
Bundle installedsystembundle-installer
Bundle uninstalledsystembundle-installer
Scheduled reminderreminderscheduler

UI

The notification bell in the top bar shows an unread count badge. Clicking it opens a dropdown with recent notifications, each showing title, time, and source. Notifications can be dismissed individually or cleared in bulk. The REST API at /api/notifications provides JSON access for the dropdown's fetch calls.

No Build Step

The Crow's Nest has no build step, no bundler, and no node_modules of its own. All HTML, CSS, and minimal JavaScript are generated inline by the server. This keeps the UI lightweight and avoids frontend toolchain complexity.

CSS uses custom properties for theming (see the full Brand Identity table above):

css
:root {
  --crow-bg-deep: #0f0f17;
  --crow-bg-surface: #1a1a2e;
  --crow-accent: #6366f1;
  --crow-text-primary: #fafaf9;
  --crow-brand-gold: #fbbf24;
}

.theme-light {
  --crow-bg-deep: #fafaf9;
  --crow-bg-surface: #ffffff;
  --crow-accent: #4f46e5;
  --crow-text-primary: #1c1917;
}

Released under the MIT License.