- persistence: tag created/updated Open WebUI chats with a homarr-user:<id> tag
(via chat meta.tags) so conversations can be found back per Homarr user even
when an integration/account is shared
- voice recording: distinguish a denied/dismissed microphone permission from a
generic mic failure and show an actionable message
- widget: surface the models query loading and connection-error states in the
chat empty state instead of silently showing 'no models'
- tests: cover the final-SSE-frame flush, the default request timeout (and the
streaming opt-out), and multimodal chat parsing round-trip
Replace the bottom-right floating chat button with app-wide launchers, as
requested in review:
- side panel: a right-side Drawer (OpenWebUiChat) reusing the chat widget,
mounted app-wide and shown only to authed users who own an Open WebUI
integration
- header entry: an 'AI Chat' item in the user-avatar menu (like the Docker item)
- keybind: mod+shift+a toggles the panel (hotkeys.toggleAiChat)
- spotlight: a '?' 'Ask AI' mode that starts a conversation from the typed query
and opens the panel pre-filled; also surfaced in the Cmd+K help/mode list when
the integration is present
- the panel and spotlight are bridged to the app via an openWebUiChatEvent
window event (mirrors the existing media-request search event)
The Chat widget gains an optional initialQuery that auto-sends once on open.
Per ajnart's review (and CodeRabbit withdrawing its own MCP comment on the
predecessor PR), the Open WebUI chat isn't a meaningful MCP surface:
- remove the .meta({ mcp }) metadata from every openWebUi procedure
Remaining CodeRabbit comments from the predecessor PR that weren't carried over:
- processWeb: constrain the URL to http(s) and reject loopback/metadata hosts
(private LAN ranges stay allowed for self-hosters); it is also creator-gated
- uploadFile/transcribe: bound the base64 payload and validate filename/type
- docs: align the example URL with the integration default port (8080)
- new chat: clear the composer draft so it doesn't leak into the fresh chat
- web ingestion: keep the typed URL when ingestion fails
- persistence: fall back to the default title for image-only opening turns, and
don't persist a partial reply from a failed turn as finished history
- multimodal round-trip: make the native chat/history contracts accept the same
text-or-parts content union as completions, and flatten (not drop) non-text
nodes when rebuilding transcripts so attachment-backed chats aren't lost.
Image data URLs are still kept out of persisted history by design.
API / router (packages/api):
- open-webui: expose serializable procedures as MCP tools via .meta({ mcp })
and gate knowledge/files/notes reads behind protectedProcedure + interact so
anonymous board viewers can't enumerate private source content
- integration create: wrap integration + creator-permission + secret writes in
a single cross-driver transaction so a partial failure can't leave an
unusable integration
- integration test: scope the permission lookup to the created integration
Integration (packages/integrations):
- open-webui: log only safe metadata (not raw completion chunks) on parse
failure, flush the final buffered SSE frame before treating the stream as
truncated, and apply a default request timeout (streaming opts out)
Widget (packages/widgets):
- collapse knowledge rows by default to avoid an N+1 getKnowledgeFiles burst
- strip markdown images from assistant output to block untrusted fetches
- reconcile the selected model when the model list changes
- clear draft attachments when switching to a saved chat
- dedupe async note/chat attaches after the fetch resolves
- discard stale persist responses via a session token so a cleared chat can't
be rebound to an old Open WebUI chat id
- release the mic stream on mid-start failures and mark the recording cancelled
on unmount so partial audio isn't transcribed
The PR only shipped a SQLite creator_id migration; MySQL and PostgreSQL were
missing (CodeRabbit) and the SQLite one lacked ON DELETE SET NULL. Regenerate
on top of dev's widget_secret base:
- sqlite 0041: ADD creator_id ... REFERENCES user(id) ON DELETE SET NULL
- mysql 0043: ADD creator_id + FK ON DELETE set null (hand-written snapshot
from dev's 0042 to avoid touching the pre-existing custom_widget_secret FK
that must stay under MySQL's 64-char identifier limit)
- postgresql 0009: ADD creator_id + FK ON DELETE set null
Schema already declares onDelete: set null on integration.creatorId.
Resolve conflicts by keeping both openWebUi and dev-side registry entries
(integration/widget kinds, creators, docs sitemap). Take dev's next.config
turbopack settings. Reset SQLite migration base to dev's 0040_add_widget_secret;
the integration.creator_id migrations are regenerated for all dialects in a
follow-up commit.
* fix(widget-secrets): add missing widget_secret migration and add error notifications
The widget_secret table was defined in all three Drizzle schema files but
no SQL migration was ever generated to create it, causing setSecret to
fail with 'no such table: widget_secret' in production (drizzle-kit push
in development hid the problem).
- Add migration SQL for sqlite (0040), mysql (0042), postgresql (0008)
- Update journal entries for all three dialects
- Add onError handlers to setSecret/deleteSecret mutations so errors
are shown as red notifications instead of silently swallowed
Fixes#6186
* fix(widget-secrets): add missing drizzle snapshots for merge-url and widget_secret migrations
The merge_custom_widget_url migration (0039/0041/0007) was committed
without a corresponding snapshot, breaking drizzle-kit generate for
all future migrations. This adds:
- 0039/0041/0007 snapshots reflecting the merge-url changes
(base_url+endpoint -> url, add enabled column)
- 0040/0042/0008 snapshots reflecting the widget_secret table
All snapshot chains now match journal entries (41/43/9 entries each).
The refactor from integration-based to option-based config changed the
useQuery enabled condition from 'integrationIds.length > 0' (always true
when an integration was linked) to 'query.length > 0'. The combination
of the debounced search value and the enabled toggle inside the nested
useOptions callback caused the query to never fire, making stations
unsearchable in the edit form.
Removing the enabled guard lets the query always run. The 300ms debounce
already prevents excessive requests.