diff --git a/.cursor/skills/playwright-testing/browser-apis/iframes.md b/.cursor/skills/playwright-testing/browser-apis/iframes.md index 145e050ff..155cc1c1b 100644 --- a/.cursor/skills/playwright-testing/browser-apis/iframes.md +++ b/.cursor/skills/playwright-testing/browser-apis/iframes.md @@ -372,7 +372,7 @@ test("mock iframe response", async ({ page }) => {
Mocked widget content
`, diff --git a/.cursor/skills/playwright-testing/core/locators.md b/.cursor/skills/playwright-testing/core/locators.md index f806635d6..afe3af361 100644 --- a/.cursor/skills/playwright-testing/core/locators.md +++ b/.cursor/skills/playwright-testing/core/locators.md @@ -100,7 +100,7 @@ use: { Usage: ```typescript -// HTML: +// React: page.getByTestId("submit-btn"); ``` diff --git a/.cursor/skills/vercel-react-best-practices/AGENTS.md b/.cursor/skills/vercel-react-best-practices/AGENTS.md index 94c3c8441..2b839ab51 100644 --- a/.cursor/skills/vercel-react-best-practices/AGENTS.md +++ b/.cursor/skills/vercel-react-best-practices/AGENTS.md @@ -549,6 +549,8 @@ Preload heavy bundles before they're needed to reduce perceived latency. **Example: preload on hover/focus** ```tsx +import { Button } from '@/components/ui/button' + function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== 'undefined') { @@ -557,13 +559,13 @@ function EditorButton({ onClick }: { onClick: () => void }) { } return ( - + ) } ``` @@ -1239,11 +1241,12 @@ function StaticContent() { **For mutations:** ```tsx +import { Button } from '@/components/ui/button' import { useSWRMutation } from 'swr/mutation' function UpdateButton() { const { trigger } = useSWRMutation('/api/user', updateUser) - return + return } ``` @@ -1369,6 +1372,8 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i **Incorrect: subscribes to all searchParams changes** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const searchParams = useSearchParams() @@ -1377,13 +1382,15 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` **Correct: reads on demand, no subscription** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { const params = new URLSearchParams(window.location.search) @@ -1391,7 +1398,7 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` @@ -1549,6 +1556,8 @@ If a side effect is triggered by a specific user action (submit, click, drag), r **Incorrect: event modeled as state + effect** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const [submitted, setSubmitted] = useState(false) const theme = useContext(ThemeContext) @@ -1560,13 +1569,15 @@ function Form() { } }, [submitted, theme]) - return + return } ``` **Correct: do it in the handler** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const theme = useContext(ThemeContext) @@ -1575,7 +1586,7 @@ function Form() { showToast('Registered', theme) } - return + return } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md b/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md index 700050406..0662ef81b 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +++ b/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md @@ -12,6 +12,8 @@ Preload heavy bundles before they're needed to reduce perceived latency. **Example (preload on hover/focus):** ```tsx +import { Button } from "@/components/ui/button" + function EditorButton({ onClick }: { onClick: () => void }) { const preload = () => { if (typeof window !== 'undefined') { @@ -20,13 +22,13 @@ function EditorButton({ onClick }: { onClick: () => void }) { } return ( - + ) } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md b/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md index 2a430f27f..22d419bca 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +++ b/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md @@ -45,11 +45,12 @@ function StaticContent() { **For mutations:** ```tsx +import { Button } from '@/components/ui/button' import { useSWRMutation } from 'swr/mutation' function UpdateButton() { const { trigger } = useSWRMutation('/api/user', updateUser) - return + return } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md index e867c95f0..94410bc5b 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +++ b/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md @@ -12,6 +12,8 @@ Don't subscribe to dynamic state (searchParams, localStorage) if you only read i **Incorrect (subscribes to all searchParams changes):** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const searchParams = useSearchParams() @@ -20,13 +22,15 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` **Correct (reads on demand, no subscription):** ```tsx +import { Button } from '@/components/ui/button' + function ShareButton({ chatId }: { chatId: string }) { const handleShare = () => { const params = new URLSearchParams(window.location.search) @@ -34,6 +38,6 @@ function ShareButton({ chatId }: { chatId: string }) { shareChat(chatId, { ref }) } - return + return } ``` diff --git a/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md b/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md index dd58a1af0..299815d69 100644 --- a/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +++ b/.cursor/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md @@ -12,6 +12,8 @@ If a side effect is triggered by a specific user action (submit, click, drag), r **Incorrect (event modeled as state + effect):** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const [submitted, setSubmitted] = useState(false) const theme = useContext(ThemeContext) @@ -23,13 +25,15 @@ function Form() { } }, [submitted, theme]) - return + return } ``` **Correct (do it in the handler):** ```tsx +import { Button } from '@/components/ui/button' + function Form() { const theme = useContext(ThemeContext) @@ -38,7 +42,7 @@ function Form() { showToast('Registered', theme) } - return + return } ``` diff --git a/docker/docker-compose.deps-only.yml b/docker/docker-compose.deps-only.yml index ee09a4d5b..31dcd8b26 100644 --- a/docker/docker-compose.deps-only.yml +++ b/docker/docker-compose.deps-only.yml @@ -83,7 +83,7 @@ services: retries: 5 zero-cache: - image: rocicorp/zero:0.26.2 + image: rocicorp/zero:1.4.0 ports: - "${ZERO_CACHE_PORT:-4848}:4848" extra_hosts: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 28b00a044..5338a649e 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -179,7 +179,7 @@ services: # - celery_worker zero-cache: - image: rocicorp/zero:0.26.2 + image: rocicorp/zero:1.4.0 ports: - "${ZERO_CACHE_PORT:-4848}:4848" extra_hosts: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 10cace249..18147a189 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -163,7 +163,7 @@ services: # restart: unless-stopped zero-cache: - image: rocicorp/zero:0.26.2 + image: rocicorp/zero:1.4.0 ports: - "${ZERO_CACHE_PORT:-5929}:4848" extra_hosts: diff --git a/surfsense_backend/alembic/versions/116_create_zero_publication.py b/surfsense_backend/alembic/versions/116_create_zero_publication.py index ff74952a9..927673c35 100644 --- a/surfsense_backend/alembic/versions/116_create_zero_publication.py +++ b/surfsense_backend/alembic/versions/116_create_zero_publication.py @@ -5,6 +5,17 @@ queries via Zero, instead of replicating all tables in public schema. See: https://zero.rocicorp.dev/docs/zero-cache-config#app-publications +NOTE for future migration authors: this is the ONLY migration allowed +to use bare ``CREATE PUBLICATION``. All subsequent mutations of +``zero_publication`` MUST use the ``COMMENT ON PUBLICATION`` bookend +pattern wrapping an ``ALTER PUBLICATION ... SET TABLE`` -- copy the +``upgrade()`` function from migration +``143_force_zero_publication_resync.py`` as your starting template. +Raw ``DROP``/``CREATE PUBLICATION`` in new migrations would +re-introduce bug #1355 (zero-cache stuck on a stale replica snapshot +because Zero >= 1.0's change-streamer never sees the schema-change +event). + Revision ID: 116 Revises: 115 """ diff --git a/surfsense_backend/alembic/versions/117_optimize_zero_publication_column_lists.py b/surfsense_backend/alembic/versions/117_optimize_zero_publication_column_lists.py index 3ad5a043b..c21ed2bf0 100644 --- a/surfsense_backend/alembic/versions/117_optimize_zero_publication_column_lists.py +++ b/surfsense_backend/alembic/versions/117_optimize_zero_publication_column_lists.py @@ -17,6 +17,16 @@ IMPORTANT — before AND after running this migration: 3. Delete / reset the zero-cache data volume 4. Restart zero-cache (it will do a fresh initial sync) +DO NOT COPY THIS PATTERN. The ``DROP PUBLICATION`` + ``CREATE +PUBLICATION`` dance below is the pre-#1355 anti-pattern: on Zero >= +1.0 it does not reliably wake the zero-cache change-streamer and can +leave the replica pinned to a stale snapshot. This file is +grandfathered in because it has already shipped to users; new +publication mutations MUST use the ``COMMENT ON PUBLICATION`` bookend +pattern wrapping an ``ALTER PUBLICATION ... SET TABLE`` -- copy the +``upgrade()`` function from migration +``143_force_zero_publication_resync.py`` as your starting template. + Revision ID: 117 Revises: 116 """ diff --git a/surfsense_backend/alembic/versions/118_add_local_folder_sync_and_versioning.py b/surfsense_backend/alembic/versions/118_add_local_folder_sync_and_versioning.py index 1fef9fbcb..1dce24e56 100644 --- a/surfsense_backend/alembic/versions/118_add_local_folder_sync_and_versioning.py +++ b/surfsense_backend/alembic/versions/118_add_local_folder_sync_and_versioning.py @@ -1,5 +1,16 @@ """Add LOCAL_FOLDER_FILE document type, folder metadata, and document_versions table +DO NOT COPY THIS PATTERN. The bare ``ALTER PUBLICATION ... ADD/DROP +TABLE`` calls below pre-date the ``COMMENT ON PUBLICATION`` bookend +fix for bug #1355: on Zero >= 1.0 they do not reliably wake the +zero-cache change-streamer and can leave the replica pinned to a +stale snapshot. This file is grandfathered in because it has already +shipped to users; new publication mutations MUST use the +``COMMENT ON PUBLICATION`` bookend pattern wrapping an +``ALTER PUBLICATION ... SET TABLE`` -- copy the ``upgrade()`` function +from migration ``143_force_zero_publication_resync.py`` as your +starting template. + Revision ID: 118 Revises: 117 """ diff --git a/surfsense_backend/alembic/versions/139_add_user_to_zero_publication.py b/surfsense_backend/alembic/versions/139_add_user_to_zero_publication.py index 83c96a429..646049e3c 100644 --- a/surfsense_backend/alembic/versions/139_add_user_to_zero_publication.py +++ b/surfsense_backend/alembic/versions/139_add_user_to_zero_publication.py @@ -21,6 +21,16 @@ IMPORTANT - before AND after running this migration: 3. Delete / reset the zero-cache data volume 4. Restart zero-cache (it will do a fresh initial sync) +DO NOT COPY THIS PATTERN. The ``DROP PUBLICATION`` + ``CREATE +PUBLICATION`` dance below is the pre-#1355 anti-pattern: on Zero >= +1.0 it does not reliably wake the zero-cache change-streamer and can +leave the replica pinned to a stale snapshot. This file is +grandfathered in because it has already shipped to users; new +publication mutations MUST use the ``COMMENT ON PUBLICATION`` bookend +pattern wrapping an ``ALTER PUBLICATION ... SET TABLE`` -- copy the +``upgrade()`` function from migration +``143_force_zero_publication_resync.py`` as your starting template. + Revision ID: 139 Revises: 138 """ diff --git a/surfsense_backend/alembic/versions/140_premium_tokens_to_credit_micros.py b/surfsense_backend/alembic/versions/140_premium_tokens_to_credit_micros.py index 64aa699e8..ff88ac34e 100644 --- a/surfsense_backend/alembic/versions/140_premium_tokens_to_credit_micros.py +++ b/surfsense_backend/alembic/versions/140_premium_tokens_to_credit_micros.py @@ -32,6 +32,16 @@ Skipping the zero-cache stop will deadlock at the ACCESS EXCLUSIVE LOCK on "user". Skipping the data-volume reset will leave IndexedDB clients seeing column-not-found errors from a stale catalog snapshot. +DO NOT COPY THIS PATTERN. The ``DROP PUBLICATION`` + ``CREATE +PUBLICATION`` dance below is the pre-#1355 anti-pattern: on Zero >= +1.0 it does not reliably wake the zero-cache change-streamer and can +leave the replica pinned to a stale snapshot. This file is +grandfathered in because it has already shipped to users; new +publication mutations MUST use the ``COMMENT ON PUBLICATION`` bookend +pattern wrapping an ``ALTER PUBLICATION ... SET TABLE`` -- copy the +``upgrade()`` function from migration +``143_force_zero_publication_resync.py`` as your starting template. + Revision ID: 140 Revises: 139 """ diff --git a/surfsense_backend/alembic/versions/143_force_zero_publication_resync.py b/surfsense_backend/alembic/versions/143_force_zero_publication_resync.py new file mode 100644 index 000000000..147cbde56 --- /dev/null +++ b/surfsense_backend/alembic/versions/143_force_zero_publication_resync.py @@ -0,0 +1,142 @@ +"""force zero-cache to resync after upgrading to Zero >= 1.0 + +Re-emits the current ``zero_publication`` shape using +``ALTER PUBLICATION ... SET TABLE`` wrapped in +``COMMENT ON PUBLICATION`` bookends. This is the publication-change +hook documented for Zero ``>=1.0``: + + https://zero.rocicorp.dev/docs/connecting-to-postgres#publication-changes + +Background +---------- +Migrations 117 / 139 / 140 mutated ``zero_publication`` using +``DROP PUBLICATION`` + ``CREATE PUBLICATION``. On Zero 0.26.2 that +sequence did not reliably wake the zero-cache change-streamer, so +affected installs ended up with a SQLite replica file (in the +``surfsense-zero-cache`` volume) that was snapshotted against the +pre-``user`` publication. The frontend Zero schema includes a +``userTable`` query, which then failed with +``SchemaVersionNotSupported`` and triggered the default +``onUpdateNeeded`` -> ``location.reload()`` every WebSocket keepalive +interval (~60s). See bug #1355. + +This migration emits the canonical publication shape one more time, +this time using a pattern that fires Postgres event triggers and +Zero's schema-change hook. With ``ZERO_AUTO_RESET=true`` (the default) +and Zero ``>=1.0``, zero-cache responds by wiping its replica and +doing a fresh initial sync from the corrected publication. + +The publication shape itself is unchanged versus migration 140 -- on +installs whose replica is already correct, this is a no-op aside +from the harmless event-trigger fire. + +Revision ID: 143 +Revises: 142 +""" + +from collections.abc import Sequence + +import sqlalchemy as sa + +from alembic import op + +revision: str = "143" +down_revision: str | None = "142" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +PUBLICATION_NAME = "zero_publication" + +# Must stay in sync with the column lists in migrations 117 / 139 / 140. +DOCUMENT_COLS = [ + "id", + "title", + "document_type", + "search_space_id", + "folder_id", + "created_by_id", + "status", + "created_at", + "updated_at", +] + +USER_COLS = [ + "id", + "pages_limit", + "pages_used", + "premium_credit_micros_limit", + "premium_credit_micros_used", +] + + +def _has_zero_version(conn, table: str) -> bool: + return ( + conn.execute( + sa.text( + "SELECT 1 FROM information_schema.columns " + "WHERE table_name = :tbl AND column_name = '_0_version'" + ), + {"tbl": table}, + ).fetchone() + is not None + ) + + +def _build_set_table_ddl( + *, documents_has_zero_ver: bool, user_has_zero_ver: bool +) -> str: + doc_cols = DOCUMENT_COLS + (['"_0_version"'] if documents_has_zero_ver else []) + user_cols = USER_COLS + (['"_0_version"'] if user_has_zero_ver else []) + doc_col_list = ", ".join(doc_cols) + user_col_list = ", ".join(user_cols) + return ( + f"ALTER PUBLICATION {PUBLICATION_NAME} SET TABLE " + f"notifications, " + f"documents ({doc_col_list}), " + f"folders, " + f"search_source_connectors, " + f"new_chat_messages, " + f"chat_comments, " + f"chat_session_state, " + f'"user" ({user_col_list})' + ) + + +def upgrade() -> None: + conn = op.get_bind() + + exists = conn.execute( + sa.text("SELECT 1 FROM pg_publication WHERE pubname = :name"), + {"name": PUBLICATION_NAME}, + ).fetchone() + if not exists: + return + + documents_has_zero_ver = _has_zero_version(conn, "documents") + user_has_zero_ver = _has_zero_version(conn, "user") + + # The COMMENT-ALTER-COMMENT trio MUST run in a single transaction so + # Zero observes them as one schema-change event. Alembic's outer + # transaction already covers us, but a SAVEPOINT keeps the trio + # atomic with asyncpg, matching the pattern used in migrations + # 117 / 139 / 140. + tx = conn.begin_nested() if conn.in_transaction() else conn.begin() + with tx: + conn.execute( + sa.text(f"COMMENT ON PUBLICATION {PUBLICATION_NAME} IS 'pre-143-resync'") + ) + conn.execute( + sa.text( + _build_set_table_ddl( + documents_has_zero_ver=documents_has_zero_ver, + user_has_zero_ver=user_has_zero_ver, + ) + ) + ) + conn.execute( + sa.text(f"COMMENT ON PUBLICATION {PUBLICATION_NAME} IS 'post-143-resync'") + ) + + +def downgrade() -> None: + """No-op. The publication shape is unchanged versus migration 140.""" diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py index 0d702be4c..ccc5c49e2 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/tools/search_surfsense_docs.py @@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.db import SurfsenseDocsChunk, SurfsenseDocsDocument from app.utils.document_converters import embed_text +from app.utils.surfsense_docs import surfsense_docs_public_url def format_surfsense_docs_results(results: list[tuple]) -> str: @@ -19,13 +20,14 @@ def format_surfsense_docs_results(results: list[tuple]) -> str: # Group chunks by document grouped: dict[int, dict] = {} for chunk, doc in results: + public_url = surfsense_docs_public_url(doc.source) if doc.id not in grouped: grouped[doc.id] = { "document_id": f"doc-{doc.id}", "document_type": "SURFSENSE_DOCS", "title": doc.title, - "url": doc.source, - "metadata": {"source": doc.source}, + "url": public_url, + "metadata": {"source": doc.source, "public_url": public_url}, "chunks": [], } grouped[doc.id]["chunks"].append( diff --git a/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py b/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py index 2965f2f02..d8a0efac7 100644 --- a/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py +++ b/surfsense_backend/app/agents/new_chat/tools/search_surfsense_docs.py @@ -17,6 +17,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.db import SurfsenseDocsChunk, SurfsenseDocsDocument, async_session_maker from app.utils.document_converters import embed_text +from app.utils.surfsense_docs import surfsense_docs_public_url def format_surfsense_docs_results(results: list[tuple]) -> str: @@ -40,13 +41,14 @@ def format_surfsense_docs_results(results: list[tuple]) -> str: # Group chunks by document grouped: dict[int, dict] = {} for chunk, doc in results: + public_url = surfsense_docs_public_url(doc.source) if doc.id not in grouped: grouped[doc.id] = { "document_id": f"doc-{doc.id}", "document_type": "SURFSENSE_DOCS", "title": doc.title, - "url": doc.source, - "metadata": {"source": doc.source}, + "url": public_url, + "metadata": {"source": doc.source, "public_url": public_url}, "chunks": [], } grouped[doc.id]["chunks"].append( diff --git a/surfsense_backend/app/routes/surfsense_docs_routes.py b/surfsense_backend/app/routes/surfsense_docs_routes.py index e1713e8a3..0d5428dec 100644 --- a/surfsense_backend/app/routes/surfsense_docs_routes.py +++ b/surfsense_backend/app/routes/surfsense_docs_routes.py @@ -24,6 +24,7 @@ from app.schemas.surfsense_docs import ( SurfsenseDocsDocumentWithChunksRead, ) from app.users import current_active_user +from app.utils.surfsense_docs import surfsense_docs_public_url router = APIRouter() @@ -76,6 +77,7 @@ async def get_surfsense_doc_by_chunk_id( id=document.id, title=document.title, source=document.source, + public_url=surfsense_docs_public_url(document.source), content=document.content, chunks=[ SurfsenseDocsChunkRead(id=c.id, content=c.content) @@ -146,6 +148,7 @@ async def list_surfsense_docs( id=doc.id, title=doc.title, source=doc.source, + public_url=surfsense_docs_public_url(doc.source), content=doc.content, created_at=doc.created_at, updated_at=doc.updated_at, diff --git a/surfsense_backend/app/schemas/surfsense_docs.py b/surfsense_backend/app/schemas/surfsense_docs.py index ce32c0ef8..3adf25032 100644 --- a/surfsense_backend/app/schemas/surfsense_docs.py +++ b/surfsense_backend/app/schemas/surfsense_docs.py @@ -22,6 +22,7 @@ class SurfsenseDocsDocumentRead(BaseModel): id: int title: str source: str + public_url: str content: str created_at: datetime | None = None updated_at: datetime | None = None @@ -35,6 +36,7 @@ class SurfsenseDocsDocumentWithChunksRead(BaseModel): id: int title: str source: str + public_url: str content: str chunks: list[SurfsenseDocsChunkRead] diff --git a/surfsense_backend/app/tasks/chat/stream_new_chat.py b/surfsense_backend/app/tasks/chat/stream_new_chat.py index 2219ad022..9a69b6164 100644 --- a/surfsense_backend/app/tasks/chat/stream_new_chat.py +++ b/surfsense_backend/app/tasks/chat/stream_new_chat.py @@ -81,6 +81,7 @@ from app.tasks.chat.streaming.helpers.interrupt_inspector import ( ) from app.utils.content_utils import bootstrap_history_from_db from app.utils.perf import get_perf_logger, log_system_snapshot, trim_native_heap +from app.utils.surfsense_docs import surfsense_docs_public_url from app.utils.user_message_multimodal import build_human_message_content _background_tasks: set[asyncio.Task] = set() @@ -216,14 +217,17 @@ def format_mentioned_surfsense_docs_as_context( ) for doc in documents: - metadata_json = json.dumps({"source": doc.source}, ensure_ascii=False) + public_url = surfsense_docs_public_url(doc.source) + metadata_json = json.dumps( + {"source": doc.source, "public_url": public_url}, ensure_ascii=False + ) context_parts.append("{error.title}
{error.message}
- + )} @@ -191,21 +194,23 @@ export function LocalLoginForm() { }`} disabled={isLoggingIn} /> - + - + {authType === "LOCAL" && ( diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx index c336e757c..42a9182e9 100644 --- a/surfsense_web/app/(home)/login/page.tsx +++ b/surfsense_web/app/(home)/login/page.tsx @@ -6,6 +6,7 @@ import { useTranslations } from "next-intl"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; import { Logo } from "@/components/Logo"; +import { Button } from "@/components/ui/button"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { setRedirectPath } from "@/lib/auth-utils"; @@ -154,10 +155,12 @@ function LoginContent() {{urlError.title}
{urlError.message}
- + )} diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx index 00f142567..1fd1a4ecb 100644 --- a/surfsense_web/app/(home)/register/page.tsx +++ b/surfsense_web/app/(home)/register/page.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from "react"; import { type ExternalToast, toast } from "sonner"; import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { Logo } from "@/components/Logo"; +import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { getBearerToken } from "@/lib/auth-utils"; @@ -199,11 +200,13 @@ export default function RegisterPage() {{error.title}
{error.message}
- + )} @@ -295,18 +298,18 @@ export default function RegisterPage() { /> - +