diff --git a/app/api/extensions/toggles/[sector]/[slug]/route.ts b/app/api/extensions/toggles/[sector]/[slug]/route.ts
index 7b326dc8..3c8df51d 100644
--- a/app/api/extensions/toggles/[sector]/[slug]/route.ts
+++ b/app/api/extensions/toggles/[sector]/[slug]/route.ts
@@ -1,6 +1,15 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+// Legacy general extensions default to enabled when no toggle row exists
+const LEGACY_GENERAL_EXTENSIONS = [
+ 'receipt-ocr',
+ 'ai-categorization',
+ 'ai-chat',
+ 'push-notifications',
+ 'enable-banking',
+]
+
export async function GET(
_request: Request,
{ params }: { params: Promise<{ sector: string; slug: string }> }
@@ -21,7 +30,14 @@ export async function GET(
.eq('extension_slug', slug)
.single()
- return NextResponse.json({ data: data ?? { enabled: false } })
+ if (data) {
+ return NextResponse.json({ data })
+ }
+
+ // No toggle row: legacy general extensions default to enabled
+ const defaultEnabled =
+ sector === 'general' && LEGACY_GENERAL_EXTENSIONS.includes(slug)
+ return NextResponse.json({ data: { enabled: defaultEnabled } })
}
export async function DELETE(
diff --git a/app/api/supplier-invoices/__tests__/route.test.ts b/app/api/supplier-invoices/__tests__/route.test.ts
index 1c9d3c9a..d23e8631 100644
--- a/app/api/supplier-invoices/__tests__/route.test.ts
+++ b/app/api/supplier-invoices/__tests__/route.test.ts
@@ -12,6 +12,10 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
+vi.mock('@/lib/init', () => ({
+ ensureInitialized: vi.fn(),
+}))
+
const mockFindFiscalPeriod = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
findFiscalPeriod: (...args: unknown[]) => mockFindFiscalPeriod(...args),
diff --git a/app/api/supplier-invoices/route.ts b/app/api/supplier-invoices/route.ts
index 564c4428..e702d6e3 100644
--- a/app/api/supplier-invoices/route.ts
+++ b/app/api/supplier-invoices/route.ts
@@ -1,8 +1,11 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { createSupplierInvoiceRegistrationEntry } from '@/lib/bookkeeping/supplier-invoice-entries'
+import { ensureInitialized } from '@/lib/init'
import type { CreateSupplierInvoiceInput, SupplierInvoice, SupplierInvoiceItem } from '@/types'
+ensureInitialized()
+
export async function GET(request: Request) {
const supabase = await createClient()
diff --git a/app/page.tsx b/app/page.tsx
index 39b0883f..9143e22a 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'
import DashboardNav from '@/components/dashboard/DashboardNav'
import DashboardContent from '@/components/dashboard/DashboardContent'
import type { Deadline, ReceiptQueueSummary } from '@/types'
+import { ChatWidget } from '@/components/chat'
export default async function RootPage() {
const supabase = await createClient()
@@ -190,6 +191,7 @@ export default async function RootPage() {
}}
/>
+
)
diff --git a/components/chat/ChatWidget.tsx b/components/chat/ChatWidget.tsx
index ad50594a..c74ea0c0 100644
--- a/components/chat/ChatWidget.tsx
+++ b/components/chat/ChatWidget.tsx
@@ -10,7 +10,6 @@ export function ChatWidget() {
const [isOpen, setIsOpen] = useState(false)
// Default to true for legacy compatibility (ai-chat defaults to enabled)
const [enabled, setEnabled] = useState(true)
- const [loaded, setLoaded] = useState(false)
// Fetch initial toggle state
useEffect(() => {
@@ -22,8 +21,8 @@ export function ChatWidget() {
// Legacy: enabled by default when no toggle row exists
setEnabled(data?.enabled ?? true)
}
- } finally {
- setLoaded(true)
+ } catch {
+ // Keep default (enabled) on fetch failure
}
}
check()
@@ -42,7 +41,14 @@ export function ChatWidget() {
return () => window.removeEventListener('extension-toggle-changed', handler)
}, [])
- if (!loaded || !enabled) return null
+ // Allow other components to open the chat via custom event
+ useEffect(() => {
+ const handler = () => setIsOpen(true)
+ window.addEventListener('open-ai-chat', handler)
+ return () => window.removeEventListener('open-ai-chat', handler)
+ }, [])
+
+ if (!enabled) return null
return (
<>
diff --git a/components/dashboard/DashboardContent.tsx b/components/dashboard/DashboardContent.tsx
index ce280b6c..97a1362d 100644
--- a/components/dashboard/DashboardContent.tsx
+++ b/components/dashboard/DashboardContent.tsx
@@ -27,6 +27,7 @@ import {
Landmark,
CheckCircle2,
ClipboardList,
+ MessageCircle,
} from 'lucide-react'
import type { CompanySettings, EntityType, Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types'
@@ -179,6 +180,8 @@ export default function DashboardContent({ firstName, settings, summary, onboard
const visibleAlerts = showAllAlerts ? alertItems : alertItems.slice(0, MAX_VISIBLE_ALERTS)
const hasMoreAlerts = alertItems.length > MAX_VISIBLE_ALERTS
+ const openAiChat = () => window.dispatchEvent(new Event('open-ai-chat'))
+
// Quick action items
const quickActions = [
{ href: '/invoices/new', icon: Receipt, label: 'Ny faktura', desc: 'Skapa och skicka', accent: true },
@@ -400,6 +403,18 @@ export default function DashboardContent({ firstName, settings, summary, onboard
)
})}
+ {/* AI assistant quick action */}
+
diff --git a/lib/core/documents/__tests__/document-service.test.ts b/lib/core/documents/__tests__/document-service.test.ts
index 3168f4d6..916b36cc 100644
--- a/lib/core/documents/__tests__/document-service.test.ts
+++ b/lib/core/documents/__tests__/document-service.test.ts
@@ -25,6 +25,8 @@ function makeClient(storageOverrides: Record = {}) {
from: vi.fn().mockImplementation(() => makeBuilder()),
rpc: vi.fn().mockImplementation(async () => results[resultIdx++] ?? { data: null, error: null }),
storage: {
+ getBucket: vi.fn().mockResolvedValue({ data: { id: 'documents' }, error: null }),
+ createBucket: vi.fn().mockResolvedValue({ data: { name: 'documents' }, error: null }),
from: vi.fn().mockReturnValue({
upload: vi.fn().mockResolvedValue({ data: {}, error: null }),
download: vi.fn().mockResolvedValue({
@@ -43,14 +45,16 @@ function makeClient(storageOverrides: Record = {}) {
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(async () => makeClient()),
+ createServiceClient: vi.fn(async () => makeClient()),
}))
-import { uploadDocument, createNewVersion, verifyIntegrity } from '../document-service'
+import { uploadDocument, createNewVersion, verifyIntegrity, _resetBucketVerified } from '../document-service'
import { createClient } from '@/lib/supabase/server'
beforeEach(() => {
vi.clearAllMocks()
eventBus.clear()
+ _resetBucketVerified()
resultIdx = 0
results = []
// Reset the mock to use default makeClient
diff --git a/lib/core/documents/document-service.ts b/lib/core/documents/document-service.ts
index 01a3afc8..1a307b90 100644
--- a/lib/core/documents/document-service.ts
+++ b/lib/core/documents/document-service.ts
@@ -1,4 +1,4 @@
-import { createClient } from '@/lib/supabase/server'
+import { createClient, createServiceClient } from '@/lib/supabase/server'
import { eventBus } from '@/lib/events'
import type { DocumentAttachment, CreateDocumentAttachmentInput, DocumentUploadSource } from '@/types'
@@ -10,6 +10,33 @@ import type { DocumentAttachment, CreateDocumentAttachmentInput, DocumentUploadS
* for documents linked to committed entries.
*/
+let bucketVerified = false
+
+/** @internal Reset bucket verification flag — for testing only */
+export function _resetBucketVerified() {
+ bucketVerified = false
+}
+
+/**
+ * Ensure the 'documents' storage bucket exists, creating it if missing.
+ * Runs once per process lifetime (same pattern as ensureInitialized).
+ */
+async function ensureDocumentsBucket(): Promise {
+ if (bucketVerified) return
+
+ const supabase = await createServiceClient()
+ const { data: bucket } = await supabase.storage.getBucket('documents')
+
+ if (!bucket) {
+ await supabase.storage.createBucket('documents', {
+ public: false,
+ fileSizeLimit: 52428800, // 50 MB
+ })
+ }
+
+ bucketVerified = true
+}
+
/**
* Compute SHA-256 hash of a file buffer
*/
@@ -31,6 +58,7 @@ export async function uploadDocument(
journal_entry_line_id?: string
} = {}
): Promise {
+ await ensureDocumentsBucket()
const supabase = await createClient()
// Compute SHA-256 hash
@@ -102,6 +130,7 @@ export async function createNewVersion(
originalId: string,
file: { name: string; buffer: ArrayBuffer; type?: string }
): Promise {
+ await ensureDocumentsBucket()
const supabase = await createClient()
// Compute SHA-256 hash