Browse documentation

🧭 Start Here

Blackcap Overview ✨ Blackcap Feature Catalog Installation First Run

🚀 Deploy Blackcap

Platform Stacks and Raspberry Pi Hardware Raspberry Pi Deployment Raspberry Pi Client Services GCP Deployment Application Updates Environment Variables and Secrets Reverse Proxy and TLS Background Jobs and Schedules

🛠️ Administer Blackcap

Organizations Users, Permissions, and Authentication Configuration Workspace Backups and Restore Database Administration Regression Testing Performance and Job Status Audit, Access Activity, and Logging GeoIP and Access Location Data Retention and Purge Support Requests API Tester and Postman Instance Reporting

🍽️ Use Recipes

Recipes and the Recipe Library Recipe Import and Discovery Recipe Editing and Cache Artifacts Recipe Sharing Social Recipe Import AI Recipe Image Generation

📅 Plan Meals

Meal Planner

🛒 Use Shopping Lists

Shopping Lists and Shop a List External and Household Shopping 🧩 Chrome Extension Shop With

🧺 Manage Kitchen Inventory

🧺 Kitchen Inventory

🖥️ Use Displays

Displays and Connections Assigning and Scheduling Display Content Remote Pi Client E-Ink Rendering Menu Refresh and Rendering Noun Project Footer Images

🧑‍🍳 Cook with Let’s Cook

🧑‍🍳 Let’s Cook 🧑‍🍳 Let’s Cook Controls and Timers

🤖 Use and Administer AI

🤖 AI in Blackcap 🤖 AI Providers and Connections 🤖 AI Seeds and Usage

🧩 Use the Chrome Extension

🧩 Blackcap Chrome Extension 🧩 Chrome Extension Recipe Capture 🧩 Chrome Extension Shop With 🧩 Chrome Extension Release and Privacy

🎮 Play Games

🎮 Games and Trivia

🔌 Integrations

Email Integration Cloud Storage Integrations Voice Assistants Shop With Integrations Authentication Providers

⚙️ Develop Blackcap

Application Architecture Database Service and Data Access SQLite and PostgreSQL Database Migrations Background Job Architecture Testing API Architecture Security and Organization Scoping UI, Icons, and Documentation Assets Blackcap-Safe Emoji Documentation Standards Terminology

⚠️ Troubleshoot Blackcap

⚠️ Troubleshooting Deployment Troubleshooting Display Troubleshooting Recipe Import Troubleshooting 🤖 AI Troubleshooting Backup Troubleshooting Database Troubleshooting Diagnostic Organization Clones Support Requests

Displays and Connections

Audience: User, Org Admin, System Admin, Developer, Support Related: Assigning And Scheduling · Remote Pi Client · Eink Rendering · Menu Rendering · Overview · Displays

Blackcap display types include Local E-Ink, Hosted Web Receiver, Kiosk, Remote Pi Client, and Mock displays. Displays are organization-scoped and have current content, default content, render profile, status, connection, preview, and assignment behavior appropriate to their type.

A full-server Raspberry Pi can directly drive one local e-ink panel. Remote Pi Clients pull prepared content from a Blackcap server. Hosted and Kiosk receivers use browser-based display pages.

This layer introduces organization-scoped display targets and connection records while preserving the existing Default-organization attached e-ink behavior.

Implemented display foundation

  • Adds a dedicated Displays Admin page at /admin/displays; the legacy /admin/displays-connections route redirects there.
  • Adds a display tile View tab for the active organization context, plus a separate Configuration tab for fleet/table management.
  • Adds a display detail page for drilling into the current preview, mode, capabilities, client/receiver records, receiver URL, generated test previews, recent render profiles, and recent recipe cache artifacts.
  • Shows current content mode, default content mode, default display, capabilities, heartbeat/status fields, and local e-ink ownership.
  • Supports durable display-content assignment for Menu, Recipe, Current Meal Plan Day/Week/Month, scheduled content, and future modules.
  • Supports bulk targeting through Assign Content flows while keeping default display selection organization-scoped.
  • Supports creating mock, hosted web receiver, kiosk display, hosted web receiver, mock display, and remote Raspberry Pi client display records.
  • Keeps local_eink available only to the Default organization.
  • Adds display/content registries and recipe-cache artifact metadata tables.

Implemented connections foundation

The dedicated Connections page at /admin/connections is metadata-first but operationally useful:

  • Lists organization-scoped connection records for the active organization context.
  • Supports creating organization connection records from a small type registry.
  • Supports safe encrypted secret/token references without showing raw secrets again.
  • Supports enable/disable actions.
  • Supports on-demand metadata health checks that update the connection status and timestamp.
  • Keeps health checks lightweight so the page does not block on provider calls.
  • Keeps Noun Project credentials platform-scoped instead of creating organization-level Noun Project connections.
  • Keeps authentication-provider app configuration platform-scoped. Organization login-provider enablement is managed separately under Configuration → Organization → Login Providers; the login page can carry org context with ?org=org_... or derive it from invites.
  • Keeps Dropbox/Google Drive app keys and search provider credentials in provider configuration; organization connections represent connected accounts/folders or account-level metadata.

Initial organization/display connection record groups include:

  • Shared Drives & Backups: Dropbox Account, Google Drive Account
  • Email Sender Override: optional organization SMTP sender/provider override
  • Display Data & Widgets: Weather Provider
  • Shopping & Product Data: Shopping Provider Account, Barcode / Product Lookup Physical display targets, including the attached local e-ink display, plus remote Raspberry Pi clients, kiosk displays, and hosted webpage/iframe receivers are intentionally configured as Displays, not Connections.

Provider-specific live OAuth flows and full API health checks can build on these records later. For now, the check action validates required metadata and confirms that a secret reference exists when a connection type requires one.

Provider configuration vs. connections

Provider Configuration remains the authority for platform/application-level credentials: Noun Project, authentication-provider OAuth/OIDC app credentials, Dropbox/Drive application keys/secrets, search provider credentials, and platform SMTP defaults. The Connections page should not create rows just because platform credentials exist. It should only show records created to link an organization/user account or an explicit organization override. Login-provider organization enablement is a Configuration concern, not a Connections record.

Legacy placeholder rows created by early configuration scaffolding, such as Noun Project Icons or Primary E-Ink Display Connection, are deleted from connections during migration cleanup. The underlying provider_configs records and real displays records remain intact. Empty scaffold rows for provider-only concepts are not preserved as disabled/moved connection records.

Displays page structure

Displays are now their own Tools page rather than being combined with connections. The page heading uses the same Normal Menu / Recipe Display mode status pill pattern as the rest of Admin. The first tab is View, showing each configured display as a consistently sized tile with current content mode, status, capabilities, and the last preview image when available. Display-type descriptions are kept in compact tooltips instead of large guidance cards so the page stays focused on actual displays. The second tab is Configuration, using the fleet table and bulk update controls for content mode/default changes and add-display setup; configuration-only rows avoid preview file paths and other view-only noise.

The View tab is also the landing point for kiosk/web/input-capable displays. It shows a static representation of what each display last rendered; clicking the preview opens Display Detail, and the tile actions link to receiver/kiosk views when applicable. The detail page is the operational drill-in surface for receiver/client records, current preview state, generated test previews, render profile metadata, cache artifact paths, authenticated receiver URLs, and read-only tokenized receiver URLs for always-on browser/kiosk/iframe consumers.

Display receivers and client devices

The attached local e-ink display, a remote Raspberry Pi client, interactive kiosk browser, tablet, PC, or Blackcap-hosted iframe receiver is a Display because it is a render/content target. It is not an external service connection. Any pairing, receiver route, pull interval, heartbeat interval, expected consumer count, or receiver notes should be configured from the Displays create/configuration flow.

  • remote_pi_client represents a remote Raspberry Pi that will poll Blackcap, render assigned content locally, and report heartbeat/status.
  • kiosk represents an interactive tokenized fullscreen mini-app. It can view the current display content, search/select recipes scoped to the display organization, push the selected recipe to that kiosk display only, view read-only meal plans by day/week/month, return to the default/pushed screen, and start/continue Let’s Cook sessions with compact side reminders when launched from a meal-plan entry.
  • web is the hosted web receiver concept: a passive Blackcap receiver page that can be embedded or shown in a browser/iframe, such as a DAKboard panel.

Connections remain for account links and overrides such as Dropbox/Google Drive connected accounts, optional SMTP sender overrides, weather provider settings, and future shopping/product accounts. A "Primary E-Ink Display Connection" or similar legacy physical/mock display connection should not appear on the Connections page; it belongs to Displays.

Hosted receiver and test preview workflow

Web, kiosk, mock, and remote-client display records expose an authenticated hosted receiver route:

/display-receiver/<display_id>

The receiver page shows the display's last preview image full-screen, includes a compact status bar by default, and polls the display-state endpoint periodically so the last good image remains on screen if the network blips. Authenticated users can open the normal receiver URL from Displays. Display Detail also exposes a read-only tokenized receiver URL that bypasses the normal Admin/Mobile idle timeout for unattended wall displays, kiosk browsers, and iframe consumers such as a DAKboard panel. Receiver preferences are stored per display: refresh interval, whether the status bar is hidden by default, image fit mode, background mode, and the read-only token. Add hide_bar=1, fullscreen=1, or bar=0 to hide the compact status bar for a specific URL; use bar=1 to force it visible even when the display default hides it. Display Detail can rotate the receiver token to revoke older kiosk/iframe URLs and provides an iframe snippet for DAKboard/browser dashboard embedding.

Receiver URLs and remote display-client APIs are token-authorized device endpoints, not normal browser user sessions. They intentionally continue to work with valid receiver/client tokens even when an organization requires internal-login MFA.

The Displays page keeps the first-tab display cards compact and user-focused: display-type descriptions live in tooltips, record actions are icon-first with tooltips/ARIA labels, display cards stay at a consistent width, and render/heartbeat timestamps are formatted as MM/DD/YY with minutes only. The Configuration tab keeps fleet-only details there, including display count, content/default controls, concise receiver/client wording, and a compact Add Display form sized for the actual fields.

Display Detail also supports generating a static test preview for non-local-hardware displays. Test previews write to the same isolated display preview path used by rich/mock/web displays, for example:

display_previews/org_<organization_id>/display_<display_id>/current_view.png

The test preview action intentionally does not push to the attached local e-ink display. Default-org e-ink testing should continue to use normal menu/recipe render or deep clean actions so hardware state and preview state remain aligned.

Interactive kiosk mini-app

Kiosk displays now have their own tokenized mini-app route:

/kiosk/<display_id>?token=<receiver_token>

The kiosk route is intentionally simpler than the full Admin or Mobile UI. It is scoped to the kiosk display's organization, uses simplified Admin-style timestamps for last-update status, and supports:

  • viewing the current display preview;
  • browsing recipes before entering a search, with compact token-safe thumbnail tiles, clickable recipe names, one visible description line, and meal-planning-style search/filter controls;
  • searching/selecting recipes for that organization by text, recipe type, and ingredient chips;
  • showing recipe cards with thumbnails, descriptions, and source/type metadata without requiring a logged-in Admin/Mobile browser session; kiosk thumbnail resolution mirrors the normal Admin /recipe-thumbnail/<recipe_id> behavior inside the display organization and intentionally uses only recipe/source images, never made-it photos;
  • rendering a selected recipe on that kiosk display only using the rich full-color display renderer with recipe image and header metadata when available;
  • browsing the meal plan read-only by day, week, or month with a visible period/date header and compact tiled week/month results; month tiles use row-flow wrapping so expanded cards push later rows down instead of overlapping;
  • showing a meal-plan preview on the kiosk display; week previews render as a table with days down the left and one column for each populated meal slot that week, while month previews use a calendar grid that avoids clipped entries and shows remaining counts when content is too dense;
  • returning to the saved default/pushed content screen;
  • entering a local full-screen view for the currently selected display content, with visible exit controls;
  • Let’s Cook session controls for the current/next meal-plan recipe or selected recipe. When a kiosk start comes from a meal-plan entry, the session preserves that entry id so side reminders from the same slot can appear in the Kiosk header and attached e-ink render.

Kiosk idle return is stored per display in content_settings_json.kiosk_idle_return_seconds. The default is 30 minutes (1800 seconds). Display Detail exposes this as Kiosk idle return in minutes. After inactivity, the kiosk calls the return-default endpoint and shows the saved pushed/default screen again in the kiosk full-screen overlay. The page shell is fixed to the viewport so the kiosk header and current preview stay in place while recipe and meal-plan result panels own their own scrolling. The full-screen control affects only the browser kiosk view; the display state remains whichever default, recipe, meal-plan, or later content was selected.

Module / InkyPi is no longer a display type. It remains a content mode (module) that can be pushed to compatible displays later. Any old experimental display_type = module rows are migrated to hosted web receivers while preserving Module / InkyPi as their content mode/default content.

Home screen mode picker mapping

The existing Home mode picker is preserved, but it now represents the current organization's default display content rather than a standalone global app mode.

  • Menu on the Home picker maps to the default display content_mode = menu.
  • Recipe on the Home picker maps to the default display content_mode = recipe.
  • Existing backend callers still see the legacy normal/recipe values so current menu scripts, recipe rendering, Return to Default, mobile render, and status polling continue to work.
  • The Displays page is the fleet control surface for viewing display output, changing individual displays, or bulk-updating many displays.

Initial content modes

The first content modes are intentionally focused:

  • menu — the current published menu source display, usually a Google Sheet but also compatible with supported webpage menu sources
  • recipe — a selected recipe display
  • current_meal_plan — current meal plan display
  • weather — weather display/widget
  • module — future module/plugin content, including candidates from the Inky Pi repository
  • mock_test — preview/test rendering

Shopping lists are not an initial display mode. Let’s Cook is an active cooking workflow. Client assignment ends any active Let’s Cook session before remote ownership begins. It shows recipe steps, ingredient state, static active timer state, and meal-plan side reminders while restoring the display back to its default content when the session ends.

Local e-ink restriction and Client content

The attached Waveshare 13.3-inch Type K panel is represented by a local_eink display with is_attached_local = 1 and hardware_key = local:eink:primary. It uses a 960×680 four-level grayscale profile.

Blackcap allows only one active local e-ink display, only for the Default organization, and only when the existing host-awareness service identifies the machine as a Raspberry Pi. The create UI and service enforce the rule, while migration 200 adds database uniqueness protection.

The attached display also supports Client as an immediate current content assignment. Client is not a device runtime mode and cannot be scheduled or made default. The Pi server, Admin UI, SQLite database, cron jobs, and services continue running. Entering Client ends any active Let’s Cook session, suspends local schedules, removes those schedules from Upcoming, and blocks every local server hardware push. The always-running blackcap-client.service then becomes the only authorized panel writer.

The remote link is configured on Display Details using a remote Blackcap URL and pairing code. It remains saved when local content is assigned again. Relink replaces the local relationship only after a successful new pairing; Unlink removes it. While Client is unlinked or the remote host is unavailable, the panel retains its last frame.

The local status moves through Waiting for Link, Linked, Remote Displayed, and Client Error. When a remote preview is known to be the current physical frame, Home, Displays, and Display Details show that downloaded image with a 🌐 Remote overlay. Display Busy is reserved for the short period when a local or client writer is actively updating the panel; Client mode and ordinary polling are not busy states.

Organization restores strip display-client credentials by default. An explicit in-place restore option preserves the current installation's live links for matching records; diagnostic clones never copy those credentials.

See remote-pi-client.md for the complete ownership, pairing, grayscale rendering, diagnostics, backup/restore, and deployment contract.

Non-default organization rich preview behavior

Non-default organizations receive a rich/color-capable default mock display rather than an e-ink simulation. The default rich profile is rich-color-1280x720, supports images and rich layout, and writes isolated preview artifacts under:

display_previews/org_<organization_id>/display_<display_id>/current_view.png

Recipe renders for rich/image-capable default displays use a color recipe-card style and include the recipe thumbnail when one is available. The Default organization's attached e-ink display continues to use the legacy black-and-white e-ink render path, display lock, and physical hardware update.

Recipe cache artifact direction

The existing organization directory structure under recipe_cache remains the organization boundary. New cache artifact metadata uses:

  • organization_id
  • recipe_id
  • render_profile_hash
  • artifact_type
  • artifact_role

This allows B&W e-ink artifacts and color/rich artifacts to coexist without collisions while preserving the current Default-org cache behavior.

Example rich organization cache paths:

recipe_cache/org_<organization_id>/render_profiles/rich-color-1280x720/<recipe_id>.pdf
recipe_cache/org_<organization_id>/render_profiles/rich-color-1280x720/<recipe_id>_rendered.png

Scope rules

  • Displays are organization-scoped.
  • Each organization has one default display.
  • The attached local e-ink display is Default-organization-only.
  • Noun Project API credentials are platform/provider-scoped; Noun Project footer placement/sizing and organization keyword rules are organization-scoped configuration.
  • Platform provider configuration owns application-level credentials such as authentication-provider OAuth/OIDC app credentials, Dropbox/Drive app keys, search provider keys, platform SMTP defaults, and Noun Project.
  • Connections are organization/user/display account links or overrides, not the application provider credentials themselves. Organization-scoped examples include connected Dropbox/Drive accounts, optional SMTP sender override, weather provider settings, and shopping/product providers. Login-provider enablement and internal-login MFA policy live under Configuration → Organization → Login Providers. Physical display targets, remote-client pairings, and kiosk/web receiver setup belong to Displays.
  • If an organization does not configure an SMTP override, email falls back to the platform provider while sender name/from behavior remains organization configuration.
  • Raw secrets are never displayed in the UI; blank secret fields preserve existing values when edit support is added later.

Service boundary

Routes remain thin:

Blueprint route -> display_profile_service / connection_service -> repository -> database_service

No large display or connection logic should be added back into inky_admin_app.py.

Next likely steps

  • Add edit forms for existing connections.
  • Add provider-specific live checks for SMTP, cloud backup account connections, and weather.
  • Wire backup provider selection to shared-drive account connection records while keeping app credentials provider-scoped.
  • Add remote-client pairing/token rotation from the Displays detail page.
  • Add QR/copy helpers for unattended DAKboard/kiosk/iframe consumers. Receiver-token rotation/revoke controls now exist on Display Detail.

Receiver status and token controls

Display Detail includes receiver controls for web/kiosk/mock/remote-client display targets:

  • refresh interval, clamped to 10-300 seconds;
  • default status-bar visibility;
  • normal authenticated receiver URL;
  • read-only token receiver URL for unattended devices;
  • fullscreen/status-hidden URL;
  • status-bar-forced URL when the default is hidden;
  • receiver token rotation to revoke old links.

Receiver page loads and display-state polling write a best-effort display heartbeat and mark the display as receiver_active. These heartbeat writes are intentionally non-blocking so a passive receiver keeps showing the last good preview if the status API or network briefly fails.

Remote Raspberry Pi clients

Remote Raspberry Pi clients remain display targets rather than Connections. Create Remote Raspberry Pi Client Display, generate its short-lived eight-character Pair Code on Display Details, and pair through POST /api/display-client/pair. The initial supported preset is the Waveshare 13.3-inch Type K panel at 960×680 four-level grayscale.

A paired client uses bearer-token requests to poll /api/display-client/state, download /api/display-client/preview, report /api/display-client/heartbeat, and revoke its own token through /api/display-client/unlink. State responses do not embed the durable token in preview URLs. Display Details shows state/preview pulls, heartbeat, and physical-display acknowledgement metadata.

The simulator remains available for API-only diagnostics. The persistent client service now performs atomic image download, four-level grayscale normalization, physical panel output, and heartbeat acknowledgement. On a full Pi server, the same service runs with --local-attached and sleeps unless the attached display is assigned Client content.

Performance Notes

Display receiver and remote client polling are intentionally lightweight. Receiver state polling should use the lightweight display state context rather than the full Display Detail context, and heartbeat writes are throttled so a kiosk/iframe page does not create a SQLite write on every refresh. Remote Raspberry Pi clients use their configured interval—30 seconds by default—but the state API advertises five seconds during an active Let’s Cook session. GCP regenerates pending remote-client Let’s Cook previews on a separate two-second watcher cadence. Display registry/default repair checks are also cached per process and run periodically instead of on every display service call.

Display content assignment foundation

This phase adds a durable display content assignment model for the next generation of display targeting. Content is treated in the preferred product order of Current Meal Plan → Recipe → Menu → Client (attached local e-ink only) → future Module / InkyPi Content. Displays remain the fleet control surface, while Home continues to control only the organization default display.

Assignments are stored separately from the display configuration so the app can distinguish display capabilities from the content currently assigned to a display. Each active display can have a current assignment and a default assignment. Assignment rows store the content mode, content reference, display settings, delivery status, and schedule-ready fields such as display_at and optional end_at. The model includes server/client delivery milestones, including a server-side ready state for hosted receivers and remote Raspberry Pi clients where Blackcap has rendered the content but the client has not necessarily acknowledged that it is displayed yet.

When a display is created, its default must be one concrete renderable view: Menu, Daily Meal Plan, Weekly Meal Plan, Today & Tomorrow Meal Plan, or Monthly Meal Plan. Meal-plan choices map to current_meal_plan plus an explicit view, anchor_policy, and period-refresh setting. Creation writes both current and default assignments and queues the initial render/push, so a new display does not remain in an ambiguous generic Meal Plan state.

Scheduled display instants use a provider-neutral UTC model. Browser datetime-local values are interpreted in the owning organization's IANA time zone at the assignment-service boundary, converted to aware UTC ISO text, and stored in the existing assignment timestamp columns. Explicit offsets are honored. The scheduler reads candidate rows through the assignment repository and compares aware UTC values in Python; SQLite host local time, PostgreSQL session time zone, cron TZ, and the operating-system time zone do not determine whether an assignment is due. Legacy naive assignment rows remain compatible and are interpreted in the owning organization's configured time zone. UI countdowns and scheduler evaluation call the same normalization service.

Meal plan display assignments are dynamic instead of pre-generating schedule rows for every day/week/month rollover. Their settings store the selected view (day, week, or month), an anchor policy such as current_week, a last_period_key, and refresh_on_period_change. A future smart display-content refresh job can compare period keys and source meal-plan changes, mark assignments stale, and queue re-rendering only for displays that are still using meal plan content.

The Home page replaces the old two-state Menu/Recipe toggle with a compact default-display content selector. Home actions always apply to the organization default display. If the organization has more than one display configured, Home shows an informational Default: <display name> pill but does not include a display selector; changing which display is default remains a Displays-page action. Recipe rendering from Home records the selected recipe and multiplier as the default display's current Recipe assignment.

The Displays page is the advanced fleet surface. The View tab and Configuration tab now expose an Assign Content modal that can target one display, selected displays, or all displays selected inside the modal. The modal follows the preferred content order, supports meal-plan Day/Week/Month settings per display, requires a specific recipe selection for Recipe content, stores recipe layout and multiplier settings, and supports Show Now, Make Default, or Show Now + Make Default actions through the display assignment service layer. Client is shown only for the attached local e-ink target and is restricted to Show Now/current content. Show Now actions queue an actual display-content push job. Menu and Recipe content use the display-aware render scripts with explicit --display-id; Meal Plan content renders the selected Day/Week/Month preview per target display. Virtual receivers move to ready after server rendering, remote Pi clients move toward displaying when they fetch the preview, and the attached local e-ink display is updated only while holding the shared display lock.

Meal Plan display rendering is display-profile aware and optimized for lower-resolution e-ink targets. The shared renderer lives at inky_admin/renderers/meal_plan_renderer.py and is used by kiosk previews, display-content push jobs, and smart refresh work so the views stay consistent instead of duplicating meal-plan rendering in the kiosk service. It removes the large decorative Meal Plan header, uses a compact mm/dd/yy - mm/dd/yy period label, keeps week day labels date-free, only includes populated meal-slot columns in week view, enlarges meal-slot text, renders supported emojis from the Blackcap emoji cache instead of relying on native font glyphs, and reserves bottom space for the organization-scoped Noun Project Footer when footer placement includes Meal Plan. Week slot columns and day/month slot cards are ordered by the organization-configured meal-slot order. Each populated slot renders a display-only meal summary: the first recipe-backed item is the main recipe and the remaining items become sides, such as Spaghetti & Meatballs w/Bread and Spinach Salad. If a slot has no recipe-backed entry, the first visible item becomes the main item. Color/image-capable displays can include the main recipe thumbnail; B&W/e-ink displays remain text-first. Footer icons are resolved from the organization/default noun cache and the organization's noun_project_rules rows; if a matched icon is missing and Noun Project credentials are configured, the renderer can download and seed the cache the same way the Menu renderer does.

inky_menu.py is now assignment/content-mode aware. Manual Menu assignment still restores the last rendered menu image when Menu is the target content for the target display instead of running smart refresh. Scheduled/unbound menu refresh finds displays currently showing Menu across all organizations, probes each organization's menu source cheaply, and only launches Playwright when the source changed, the probe is uncertain, or a full refresh was requested. When rendering is required, work is grouped by organization and effective render profile so displays with different resolution/color capabilities receive correctly sized output. Color-capable menu targets preserve RGB output, while attached B&W e-ink hardware keeps the legacy monochrome path.

Unbound inky_menu.py is the hourly menu-refresh dispatcher. It does not assume the default organization or default display. The flow is:

  1. Find all active displays where current content is Menu.
  2. Group those displays by organization.
  3. For each organization, run a lightweight source probe once against the configured menu URL.
  4. If the source fingerprint matches the last successfully rendered baseline, skip every Menu display in that organization without launching Playwright.
  5. If the source changed, the probe is uncertain, or --full-refresh was passed, run Playwright and render by organization + render profile.
  6. Each display compares against its own last rendered menu artifact before updating.

The source probe stores hidden org-scoped JSON under menu.source_probe_state. It records the menu URL, HTTP metadata, body length/hash, source fingerprint, check time, and rendered baseline. Google Sheets pubhtml sources use a stable spreadsheet-style fingerprint where possible; normal webpages use normalized visible text. Playwright remains the authoritative image exporter when Blackcap needs to update actual menu display output.

Display-profile-aware recipe push cache reuse

Recipe pushes reuse a cached 1x asset only when the cached artifact matches the target display render profile. The attached local e-ink display may still use the legacy/default recipe cache fields and files. Web receivers, kiosk displays, mock displays, and remote Raspberry Pi clients use the recipe_cache_artifacts metadata table and exact render_profile_hash matching so a 960×680 black-and-white e-ink render is not stretched onto a 1920×1080 rich receiver or reused for a different client profile.

When a 1x saved/default-layout recipe is pushed to a display and no matching profile artifact exists yet, the assignment render builds and registers that display-profile artifact. Later pushes to the same display/profile can reuse it. Non-1x multiplier pushes and explicit alternate layout pushes remain on-demand display renders because those outputs are action-specific rather than reusable profile cache artifacts.

Dynamic meal-plan display refresh

Display assignments for Current Meal Plan are dynamic rather than one-off schedule records. Each assignment stores its view (day, week, or month), anchor policy (current_day, current_week, current_month, or specific_date), and period key. When meal-plan data changes, Blackcap marks affected active meal-plan assignments stale and coalesces the hardware refresh instead of pushing immediately after every recipe or note edit. The organization setting meal_planner.display_refresh_debounce_seconds controls the quiet period before displays currently showing that meal-plan content are refreshed; the default is 300 seconds. Set it to 0 to restore immediate refresh behavior. The setting is edited under Configuration → Organization → Meal Planner in the same panel as week-start and slot settings, not on a separate duplicate Meal Planner page.

The refresh logic is handled by inky_admin/services/display_content_refresh_service.py and renders through the shared inky_admin/renderers/meal_plan_renderer.py. It intentionally does not create future rows for every day/week/month rollover. A periodic safety-net runner is available:

/home/pi/inky_env/bin/python3 /home/pi/Blackcap-Pi/run_display_content_refresh.py

The runner checks for period rollover, stale dynamic assignments, scheduled display-content starts/ends, and auto-show Meal Plan recipe windows. When launched by cron/systemd, those pushes run inline instead of in daemon threads so the one-shot process does not exit before the work happens. Local e-ink hardware pushes continue to use the shared display lock file so meal-plan pushes do not collide with menu refresh, recipe rendering, deep clean, or restore operations. A higher-level shared scheduler lock coordinates this runner with inky_menu.py; menu refresh creates a priority request and the display-content refresh runner yields between safe work units.

The standard scheduled-operations installer runs tools/run_scheduled_task.py blackcap-display-refresh ... run_display_content_refresh.py once per minute. That cadence means a due action may execute on the first poll after its exact instant, normally within about one minute. No specific cron or PostgreSQL session time zone is required for assignment correctness. Debug-level schedule diagnostics record safe organization/display/assignment identifiers, raw and normalized timestamps, canonical UTC now, provider, organization time zone, and the execute/skip decision; successful state transitions are logged once at info level.

Explicit display content modes

Runtime display state is now treated as explicit content mode state rather than the older two-state Normal/Recipe model. The default display can be in current_meal_plan, recipe, menu, client, or future module/weather content modes. client is the official status mode for an attached local e-ink display whose panel is controlled by a paired remote Blackcap server. Legacy normal values are accepted only as backward-compatible aliases for menu, and status APIs/banners surface the actual default-display content mode rather than a stale organization runtime fallback.

Shared scheduler locking and menu priority

inky_menu.py and run_display_content_refresh.py use a shared scheduler lock separate from the lower-level e-ink hardware lock:

  • /tmp/blackcap_scheduler.lock is a real flock lock for high-level scheduler jobs.
  • /tmp/blackcap_menu_priority.request is a short-lived PID/boot-aware request file that lets menu refresh take priority.
  • Top-level menu runs, including UI-triggered bound runs such as inky_menu.py --org-id org_... --display-id disp_..., create a priority request and wait for the scheduler lock.
  • Bound child workers launched by an already-locked unbound fan-out set BLACKCAP_SCHEDULER_LOCK_HELD=1 so they do not deadlock their parent.
  • The display-content refresh runner exits or yields cleanly when menu priority is pending or running.

This keeps menu refresh from being skipped by the every-minute display refresh runner while preventing the two jobs from mutating display assignments at the same time.

Display content current/default visibility update

Display View and Display Detail now treat Assign Content as the primary way to change display content. The older quick content-mode dropdowns have been removed from display tiles and detail status areas so operators do not accidentally change mode without selecting the required recipe, meal-plan view, default behavior, or schedule settings. Tiles and detail pages show both the current assignment and the default assignment, and scheduled/expiring current assignments show their timing plus the default content they will revert to. The Assign Content modal supports optional display_at and end_at fields; end_at is optional and the periodic display-content refresh runner can return an expired current assignment to that display's default content.

Scheduled display content queue

The display assignment model supports multiple future scheduled pushes per display. Current/default assignments remain unique, while scheduled assignments are queued and activated by run_display_content_refresh.py. Display summaries show Default first, Current only when it differs, and the nearest Upcoming assignment with a compact countdown. Display Detail lists all outstanding scheduled pushes and allows deleting them before activation. Auto-show Meal Plan recipe windows are exposed as virtual scheduled content rows so users can see, suppress, or delete them before they start or while active. Active auto-show rows show the expected revert time without adding a duplicate return-to-default row.

Scheduled start/end visibility and refresh-runner behavior

Scheduled display content now treats upcoming starts and upcoming ends/reverts as timeline events. Display summaries show only the nearest upcoming event with a compact countdown, while Display Detail lists all future starts and any active current assignment with a pending end_at. Deleting a future scheduled row cancels that future push; deleting an active current row that is past its start but before its end cancels only the pending end/revert action and leaves the current display content in place.

The display-content refresh runner reports stale_count as the remaining actionable stale current meal-plan assignments after inline refresh work completes. It also reports marked_stale_count for assignments it marked during that run. This keeps cron output from repeatedly showing stale default assignments that are not currently displayed and therefore do not need to be pushed immediately.

Future scheduled recipe activations render fresh at activation time instead of reusing old legacy recipe preview assets. Profile-specific recipe artifacts can still be built by the render path for later reuse, but scheduled pushes avoid sending a stale or low-resolution cached image to the physical display.

Assignment status and Database Admin coverage

Recipe Library now links into the Displays Assign Content workflow so a single saved recipe can be sent to one or more displays with the existing recipe layout, multiplier, display-at, and end/revert controls. Meal Planner also links the current Week or Month view into the same Assign Content modal. Display summaries continue to show Default first, Current only when different, and the nearest upcoming scheduled start or end/revert event. Active end/revert actions are shown as an upcoming return to default rather than as a duplicate "Ends in" line.

The Database page includes display-content assignment resources, assignment batches, and remote display-client jobs in addition to display targets, render profiles, display clients, content modes, and recipe cache artifacts.

Page-local assignment dialogs

  • Recipe Library and Meal Planner now use page-local lightweight Send to Display(s) dialogs instead of redirecting to the Displays page. Recipe Library send is per recipe only; Meal Planner send targets the current day/week/month meal-plan content model.
  • Display content summaries now treat Default as the primary state and suppress redundant Current rows when Current effectively matches Default after normalizing meal-plan and recipe settings.

Let’s Cook display rendering settings

Display Rendering Settings now include two Let’s Cook-specific controls:

  • Let’s Cook Current Step Display Mode (voice_lets_cook_display_mode) chooses full recipe, voice-initiated current-step only, or current-step for all Let’s Cook sessions rendered on that display. Admin and Mobile remain full-step control surfaces even when the selected render display is one-step-at-a-time.
  • Show Step 0 setup/prep section when displaying the entire Let’s Cook recipe (show_setup_step_in_full_lets_cook) is a display-level checkbox. It defaults to false and affects only Let’s Cook session rendering, not saved recipe rendering/PDF/image generation. Current-step mode still shows generated Setup/Step 0 automatically when setup items exist.

The organization-level Show full recipe before step-by-step cooking preference controls whether current-step render surfaces begin with a full-recipe review phase before moving to Setup/Step 0 or Step 1. That setting lives on the Organization page near Preferred Recipe Units because it is a cooking workflow preference rather than a display capability.

Assign Content and Return to Default

Assign modal and access tooltip details

Every time the shared Assign Content modal opens from Home, Displays, or Display Details, Set as default begins unchecked. A prior assignment choice is never carried into a later modal activation.

Display access country/LAN tooltips continue to report the unique-IP count and recent addresses, but the address preview is de-duplicated before rendering. Repeated accesses from the same IP therefore show that IP once rather than repeating it for every stored access-source row.

The shared Assign Content modal appears from Home, Displays, and Display Details. It supports the same core content choices wherever the action is valid:

  • Recipe
  • Meal Plan
  • Menu
  • Let’s Cook

Meal Plan assignments support Today, Today & Tomorrow, Week, and Month. Today & Tomorrow is intended for a compact two-day kitchen planning view and follows the same display capability profile as the other meal-plan renderers.

Return to Default is available at multiple display management levels:

  • a bulk button next to Assign Content on the Displays page
  • a small per-display tile button when that display is not showing default content
  • a Display Details header button when that display is not showing default content
  • the Home Displays panel

Return to Default uses the display’s configured default content mode and settings rather than assuming Menu. If the default content is a meal plan, recipe, or another supported mode, that default is restored and rendered.

Display deletion safeguards

Display deletion is available from Display Details and as an icon-only 🗑️ action with a tooltip in the Display Configuration table to users with display configuration permission. Blackcap blocks deletion of the organization’s only active display and blocks deletion while that display has an active Let’s Cook session. Deleting the default display requires selecting another active display from the same organization. Replacement-default assignment, scheduled/current assignment cleanup, operational dependent cleanup, and display deletion are transactional. Historical rows that use the schema’s existing ON DELETE SET NULL behavior are retained.

The Display Details template treats the deletion-safety model defensively so an older/mixed deployment cannot turn the entire details page into a 500 response when that optional context is absent. Server-side checks remain authoritative for only-display, active Let’s Cook, organization scope, and replacement-default requirements.

More information

  • Assigning and Scheduling Content — Send content immediately or schedule future display assignments and return-to-default behavior.
  • E-Ink Rendering — Understand render profiles, grayscale rules, hardware delivery, locking, and physical-panel constraints.
  • Menu Rendering — Configure and operate Menu content generation and scheduled refresh behavior.
  • Noun Project Footer Images — Configure, match, cache, and render Noun Project ingredient icons in display footers.
  • Remote Pi Client — Link and operate a Raspberry Pi display client controlled by another Blackcap server.
On this page