diff --git a/.dev.vars.example b/.dev.vars.example index 8fde51d17..7e1adf0a4 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -48,14 +48,18 @@ TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA GOOGLE_CLIENT_ID=your_google_client_id_here GOOGLE_CLIENT_SECRET=your_google_client_secret_here -# Optional: Public base URL for this worker (used to build OAuth redirect URIs) -APP_BASE_URL=http://localhost:8788 +# Optional: Public base URL for this worker (OAuth redirects, email links, +# hosted Privacy/Terms absolute URLs, sidebar legal links). Must match the +# origin you open in the browser — including port (wrangler often uses 8787 +# or 8788). Without this, in-process API calls may emit `http://localhost/...` +# without a port and TFV / footer links break locally. +APP_BASE_URL=http://127.0.0.1:8787 # Optional: Legal document links (self-host operators). When either URL is set, account-creating # public forms (agent signup, guest join) require an acceptance checkbox, and the acceptance blob # (URL + timestamp + IP + country) is stored on the user row. Unset = feature is entirely off. -# TERMS_URL=https://yourdomain.com/terms -# PRIVACY_URL=https://yourdomain.com/privacy +# TERMS_URL / PRIVACY_URL removed — configure per-tenant Privacy & Terms in +# Settings → Compliance (hosted /legal/:slug/… or custom website URLs). # Optional: Cloudflare account credentials — required for silo mode (per-tenant D1 databases) # Create an API token at dash.cloudflare.com → My Profile → API Tokens with D1:Edit permission diff --git a/CLAUDE.md b/CLAUDE.md index b6567cbdf..e2385d8fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -178,8 +178,7 @@ OpenInspection runs as ONE Cloudflare Worker (cloudflare/react-router-hono-fulls | `INSPECTION_DOC` | No | Durable Object binding (`class_name: InspectionDocDO`) for collaborative inspection editing (#181 — Yjs CRDT host; one DO per inspection, tenant-scoped `idFromName`). Declared in `wrangler.jsonc` (committed) and must be added to `wrangler.saas.jsonc` (gitignored) with the matching `v2` SQLite-class migration. When absent the collab routes return `501` and fail closed — editing falls back to a single-client Y.Doc with no realtime sync. See `docs/developers/collab-editing.md`. | | `TURNSTILE_SECRET_KEY` | No | Server-side Turnstile verification — `POST /api/book` enforces this when set. Use test secret `1x0000000000000000000000000000000AA` for local dev. | | `APP_BASE_URL` | No | Public URL for OAuth and link generation | -| `TERMS_URL` | No | Optional URL of the operator's Terms of Service. When set (with or without `PRIVACY_URL`), account-creating public forms require an acceptance checkbox and stamp an acceptance record on the user row. | -| `PRIVACY_URL` | No | Optional URL of the operator's Privacy Policy. When set, public pages render a privacy-notice footer link. | +| `APP_BASE_URL` | No | Public origin used when building absolute links (reports, hosted `/legal/:tenant/…` Privacy & Terms). | | `RESEND_API_KEY`| No | Platform-default email delivery (Resend). Tenants may switch to their OWN Resend key + verified sender via Settings → Communication (per-tenant override; the email pipeline resolves own-vs-platform explicitly). | | `GEMINI_API_KEY`| No | DEPRECATED as a platform key — AI assistance is strictly bring-your-own-key: `AIService` reads the tenant's own stored key (Settings → Advanced) and ignores this env. AI features stay off until a tenant configures a key. | | `APP_MODE` | No | `standalone` (default) or `saas` — controls tenant resolution | diff --git a/app/components/ConfirmDialog.tsx b/app/components/ConfirmDialog.tsx index eef323db7..8c6a275c5 100644 --- a/app/components/ConfirmDialog.tsx +++ b/app/components/ConfirmDialog.tsx @@ -15,7 +15,7 @@ export function ConfirmDialog({ const confirmClass = tone === "danger" ? "bg-ih-bad-fg text-white hover:opacity-90" - : "bg-ih-primary text-white hover:opacity-90"; + : "bg-ih-primary text-ih-fg-inverse hover:opacity-90"; return ( fileInputRef.current?.click()} - className="rounded-md bg-ih-primary px-3 py-1.5 text-[13px] font-bold text-white transition-opacity hover:opacity-90" + className="rounded-md bg-ih-primary px-3 py-1.5 text-[13px] font-bold text-ih-fg-inverse transition-opacity hover:opacity-90" > {m.documents_choose_file()} diff --git a/app/components/LegalCheckbox.tsx b/app/components/LegalCheckbox.tsx deleted file mode 100644 index 0cc4de3d3..000000000 --- a/app/components/LegalCheckbox.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Reusable legal acceptance checkbox for account-creating public forms. - * Renders only when at least one of termsUrl / privacyUrl is configured. - */ - -import type { LegalLinks } from "~/lib/legal-links.server"; -import { m } from "~/paraglide/messages"; - -export function LegalCheckbox({ legal }: { legal: LegalLinks }) { - return ( - - ); -} diff --git a/app/components/PublicLegalFooter.tsx b/app/components/PublicLegalFooter.tsx new file mode 100644 index 000000000..6031eb1fb --- /dev/null +++ b/app/components/PublicLegalFooter.tsx @@ -0,0 +1,31 @@ +import { m } from "~/paraglide/messages"; + +/** Compact Privacy · Terms footer for public tenant surfaces. */ +export function PublicLegalFooter({ + privacyUrl, + termsUrl, + className = "", +}: { + privacyUrl?: string | null; + termsUrl?: string | null; + className?: string; +}) { + if (!privacyUrl && !termsUrl) return null; + return ( + + ); +} diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 14e026bb9..aff33bcd1 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -1,12 +1,13 @@ import { useState, useRef } from "react"; import { NavLink, useRouteLoaderData } from "react-router"; -import { useSessionContext } from "~/hooks/useSessionContext"; +import { useSessionContext, useUnreadMessages } from "~/hooks/useSessionContext"; import { writeSidebarCookie, type UiPrefs } from "~/lib/ui-prefs"; import { IC, WORKSPACE_ITEMS } from "~/components/sidebar/nav-items"; import { SidebarGroup } from "~/components/sidebar/SidebarGroup"; import { UserMenuPopover } from "~/components/sidebar/UserMenuPopover"; import { MobileHeader } from "~/components/sidebar/MobileHeader"; import { useCommandPalette } from "~/components/CommandPalette"; +import { StaffNoticeBell } from "~/components/notices/StaffNoticeBell"; import { Avatar } from "@core/shared-ui"; import { m } from "~/paraglide/messages"; @@ -23,6 +24,7 @@ export function Sidebar() { const [userMenuOpen, setUserMenuOpen] = useState(false); const userMenuRef = useRef(null); const ctx = useSessionContext(); + const unreadMessages = useUnreadMessages(); const { openPalette } = useCommandPalette(); const companyName = ctx?.branding?.companyName || "OpenInspection"; @@ -65,9 +67,12 @@ export function Sidebar() { {!collapsed && ( <> {companyName} - - - + {/* Notices — the same bell + panel the client and agent portals + use (design §3.15). It was a link to /notifications; "sent to + me" is a glance, and the page stays for the full history. */} + + + )} @@ -93,7 +98,7 @@ export function Sidebar() { {/* Nav */}