V2 of Bookmark-Page, a device portal for broadcast engineering labs, maintained on main.
- V1 (legacy) is on branch
v1. The legacy image isadihilber/bookmark-page. - V2 images are published to
ghcr.io/leeo86/bookmark-page. A git tagv2.x.ypublishes2.x.y,2.x,2, andlatest.
UI icons and Inter fonts are bundled in the image and served from the application itself. After dependencies are installed, production compilation also works without internet access. See the offline verification commands in docs/installation.md.
- Node.js 24.15 or newer (
.nvmrcpins 24.21.0) - pnpm 10.33.3 (
packageManagerinpackage.json)
cp .env.example .env
# Set BOOKMARK_SECRET_KEY in .env
pnpm install
pnpm dev- App: http://127.0.0.1:3000
- Health: http://127.0.0.1:3000/api/health
The process exits unless BOOKMARK_SECRET_KEY is standard Base64 that decodes to 32 bytes. Generate one with:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Set ADMIN_USERNAME and ADMIN_PASSWORD to create the first break-glass admin. SQLite data for local development is app/data/bookmark.db when DATABASE_PATH is left as in .env.example.
pnpm seed:demo fills data/bookmark-demo.db with a studio start page (about 40 devices, two public tabs). Set DATABASE_PATH first if the demo should land in another file. Without BOOKMARK_SECRET_KEY the command uses a local demo key and prints that fact.
The HTTP API is under /api/v1. GET /api/v1/openapi.json is the OpenAPI document and /api/v1/docs lists the routes. Authentication is a session cookie, Authorization: Bearer, or trusted proxy headers. See .env.example.
The portal is a start page with a banner, a side nav of views and filters, and cards by default. Its bundled colourful app icon is the default banner logo; uploading a custom logo replaces it, and removing that upload restores the default. The title and claim come from settings. Icons come from Lucide, an upload, a fetched favicon on a light plate, or a letter coloured from the name. Cards are the default. Tiles and a dense list are the other views; the list remains a table on narrow screens. Reachability changes only the favicon: offline icons are grey with a red dot, unknown icons keep their colour and gain an amber dot, and a device with no check is unmarked. state:offline filters those devices; it is not a stored tag. Actions run on the server from the entry drawer, including templates whose query matches the entry. GET /metrics exposes Prometheus text. The query bar, the active tab, and the view (tiles, cards, or list) are stored in the URL. Views and filter facets live in the collapsible sidebar, which becomes a drawer on narrow screens. Sidebar tags toggle AND (click), OR (Shift-click), or NOT (Alt-click); repeating the selection removes it and existing duplicates. / focuses the query, Esc clears it, the arrow keys move the selection, Enter opens the primary link, and Ctrl/Cmd+K opens the command palette. The whole tile/card and the list name are real links. Hold Shift to select/copy card text without opening it; Alt-click the primary link to request a new window. Ctrl/Cmd and middle-click keep their native behavior. Edit mode, for editors and admins, is in the user menu or Shift+E and lasts for that browser tab. e opens the selected entry only while edit mode is on. Saving an entry that reuses a name or host asks for confirmation before the write. Admins open Admin from the user menu for the title, claim, logo, theme, page background, users, field definitions, tag namespaces, API tokens, Ansible scopes, backup, CSV import, and the audit log. Logos accept PNG, JPEG, WebP or sanitized SVG up to 1 MB; the browser-tab icon accepts PNG or WebP up to 512 KB. GET /api/v1/events is the live-update stream.
Every admin section has a top-right X to return to the portal. File pickers are labelled buttons with a selected filename; library icons are uploaded with Upload icon. Deletion, branding removal and applying a restore use red buttons in both themes. Restore previews stay primary and do not write data. Available in v2.1.0.
The initial portal HTML includes the configured title/logo and the permitted entries with their available image/icon URLs. The current banner image is preloaded; hydration reuses the server state instead of briefly showing the default logo or refetching the initial catalogue. Visibility and field redaction apply before rendering, and personalised HTML is private/no-store. Available in v2.1.0.
Live entry/status notifications reload the current filtered entries and tags. After an SSE disconnect or a failed first connection, the next server ready event also reconciles missed changes while retaining the query and open editor draft. A healthy initial handshake keeps the SSR snapshot without a duplicate bootstrap fetch. Sign-in already refreshes the catalogue. The reconnect correction is available in v2.1.1.
For device icons at a special path, edit an entry's Expert options → Favicon URL or path. Use /vendor/icon.png, a full HTTP(S) image URL, or {{host}}; clear it for automatic discovery. A chosen library/upload icon takes priority. The V1 importer preserves favicon URLs and can map selected user columns to tags through v1_import_tag_fields. See docs/migration-v1.md. Available in v2.1.0.
The collection leeo86.bookmarks is in ansible_collections/leeo86/bookmarks. It manages entries, scopes, tags, fields, filters, and action templates, and it includes inventory_sync and v1_import roles. See docs/ansible.md.
docs/installation.mddocs/configuration.mddocs/query-language.mddocs/ansible.mddocs/migration-v1.mddocs/api.mddocs/auth-entra.mddocs/auth-keycloak.mddocs/auth-header-proxy.mddocs/checks-and-actions.mddocs/design.mddocs/branding.md
Admins download a backup from /admin/backup and import CSV from /admin/csv. A backup includes secret values only when a passphrase is set. Restore can merge or replace, and it can preview the change first. The audit log is /admin/audit. Secret values there are redacted.
pnpm lint
pnpm typecheck
pnpm testdocker compose --env-file .env -f docker/docker-compose.yml up --buildThe container listens on port 3000 and stores the database on the bookmark-data volume at /data/bookmark.db. It runs as the image's non-root node user (uid 1000).
docker/docker-compose.traefik.yml is the same service with Traefik labels. It expects an external Docker network named traefik.
Copyright 2020 Adrian Hilber. Licensed under MIT. See LICENSE.
The V1 importer can also copy the site title and claim independently with v1_import_site_title and v1_import_site_claim (both opt-in).
App and Ansible collection ship together as 2.4.2. Configure a persistent admin token
with ADMIN_API_TOKEN or ADMIN_API_TOKEN_FILE; existing API tokens and module
arguments remain supported. The collection adds settings, icon, filters_sync
and group/leeo86.bookmarks.bookmarks module defaults. It also accepts session
credentials and uploaded icon references such as upload-name:mxl-mixer.
See Ansible for complete examples and migration guidance.
Favicon discovery decodes inline SVG/PNG/WebP/ICO images, tries alternative candidates, retries failures after 5 minutes, 1 hour and then every 6 hours, and preserves the last good image. Editors can inspect failures; admins can refresh missing icons from the icon library. SVG sanitising and visibility checks still apply. See configuration and the changelog.
The v2.2.2 patch streams discovery pages only through </head> or the first
256 KiB, so large self-contained HTML apps retain their early icon links.
Supplied device favicons, opt-in browser image fallback, image checks and V1 controller imports are documented in configuration and migration.
Optional automatic OIDC sign-in and the signed-in account menu are described in configuration. Sidebar facets support one-click namespace switching; see the query guide.
