Files
dobetternorge-tools/account.php
T
daveadmin b78ab1e257 Fix Bedrock advocate synthesis: engine guard, response_format, Claude engine option
- DeepResearchAgent: engine guard now accepts dbn_legal_v3, claude_sonnet, claude_haiku
  (previously stripped these to azure_mini, breaking dbn_legal_v3 selection and
  preventing Claude engines from reaching the correct synthesis branch)
- DbnBedrockGateway: remove response_format=json_object from chat payload — LiteLLM
  converts this to a tool-use constraint for Bedrock, routing output into tool_calls
  instead of content (root cause of the {} empty brief)
- advocate.php: add Claude Sonnet 4.6 (AWS Bedrock) engine option
- account, billing, dashboard, nav, min-sak: pending UI/flow changes from prior sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:22:12 +02:00

1256 lines
73 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
require_once __DIR__ . '/includes/PricingCatalog.php';
require_once __DIR__ . '/includes/CaseStore.php';
require_once __DIR__ . '/includes/CaseResults.php';
dbnToolsRequirePageAuth('/account.php');
$uiLang = dbnToolsCurrentLanguage();
$langSuffix = $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '';
$isSso = dbnToolsIsFreeTier();
$userId = $isSso ? (int)($_SESSION['dbn_tools_sso_uid'] ?? 0) : 0;
$authUser = dbnToolsAuthenticatedUser();
$email = (string)($authUser['email'] ?? '');
$detail = array_merge([
'balance' => 0, 'bonus_balance' => 0, 'tier' => $isSso ? 'free' : 'caveau',
'storage_used_bytes' => 0, 'storage_quota_bytes' => 0,
'survey_completed_at' => null, 'subscription_period_end' => null,
'trial_active' => false, 'trial_days_remaining' => 0, 'trial_expires_at' => null,
], $userId > 0 ? (FreeTier::balanceDetail($userId) ?: []) : []);
$tier = (string)$detail['tier'];
$isPaidTier = in_array($tier, ['plus', 'pro'], true);
$effective = (int)$detail['balance'] + (int)$detail['bonus_balance'];
$storageBytes = (int)$detail['storage_used_bytes'];
$quotaBytes = (int)$detail['storage_quota_bytes'];
$storageMb = $storageBytes > 0 ? round($storageBytes / 1048576, 1) : 0;
$quotaMb = $quotaBytes > 0 ? (int)round($quotaBytes / 1048576) : 0;
$storagePct = $quotaBytes > 0 ? min(100, (int)round(($storageBytes / $quotaBytes) * 100)) : 0;
$tierLabels = [
'free' => ['Free', '#f3f4f6', '#374151'],
'plus' => ['Plus', '#ddd6fe', '#5b21b6'],
'pro' => ['Pro Familie', '#bfdbfe', '#1e40af'],
'caveau' => ['CaveauAI', '#d1fae5', '#065f46'],
];
$tierLabel = $tierLabels[$tier] ?? $tierLabels['free'];
$profile = $isSso ? dbnToolsMainUserProfile($userId) : null;
$profileVal = static function (string $key) use ($profile): string {
return (string)($profile[$key] ?? '');
};
$seatLimit = $tier === 'pro' ? 3 : 1;
$docs = $isPaidTier ? CaseStore::listDocs($userId) : [];
$results = $isPaidTier ? CaseResults::listForUser($userId, 50) : [];
$recent = [];
if ($userId > 0) {
try {
$stmt = dbnmDb()->prepare(
'SELECT tool, credits_used, created_at FROM user_tool_usage_log
WHERE user_id = ? ORDER BY created_at DESC LIMIT 25'
);
$stmt->execute([$userId]);
$recent = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$recent = [];
}
}
$paidToast = isset($_GET['paid']) && $_GET['paid'] === '1';
// ── Localized strings (en / no / uk / pl) ──────────────────────────────────
$L = [
'en' => [
'page_title' => 'My account',
'page_lede' => 'Plan, profile, case storage, analyses, usage and MCP access — all in one place.',
'signed_in_as' => 'Signed in as',
'jump_plan' => 'Plan',
'jump_profile' => 'Profile',
'jump_workspace' => 'Workspace',
'jump_case' => 'My Case',
'jump_analyses' => 'Saved analyses',
'jump_usage' => 'Usage',
'jump_mcp' => 'MCP',
'trial_badge' => 'Trial — %d days left',
'sec_plan' => 'Plan & credits',
'current_plan' => 'Current plan',
'renews_on' => 'Renews on',
'no_active_sub' => 'No active subscription.',
'see_plans' => 'See all plans',
'available_credits' => 'Available credits',
'monthly' => 'monthly',
'prepaid' => 'prepaid',
'manage_subscription'=> 'Manage subscription',
'upgrade_plan' => 'Upgrade plan',
'top_up_credits' => 'Top up credits',
'payment_confirmed' => 'Payment confirmed. If you just subscribed, it may take a few seconds for your account to update.',
'bonus_survey_title' => 'Bonus credits (survey)',
'bonus_survey_done' => '✓ You have already received 25 bonus credits for completing the survey.',
'bonus_survey_pitch' => 'Answer 5 short questions about your needs and receive 25 bonus credits — never expire.',
'take_survey_btn' => 'Take the survey',
'portal_loading' => 'Connecting…',
'portal_error' => 'Could not open the portal.',
'network_error' => 'Network error: ',
'sec_profile' => 'Profile details',
'profile_optional' => 'Optional',
'f_display_name' => 'Display name',
'f_phone' => 'Phone',
'f_address_1' => 'Address line 1',
'f_address_2' => 'Address line 2',
'f_postal' => 'Postal code',
'f_city' => 'City',
'f_region' => 'Region',
'f_country' => 'Country',
'save_profile' => 'Save profile',
'profile_saving' => 'Saving…',
'profile_saved' => 'Saved',
'profile_error_default' => 'Could not save profile',
'sec_workspace' => 'Workspace access',
'w_email' => 'Email',
'w_seats' => 'Seats',
'w_role' => 'Role',
'w_owner' => 'Owner',
'w_plan_storage' => 'Plan storage',
'w_seats_single' => 'Single-user workspace',
'w_upgrade_for_storage' => 'Upgrade for My Case storage',
'sec_case' => 'My Case',
'case_lede' => 'Upload your case documents once. All tools can reference your private case when you tick "Use my case as context".',
'case_storage' => 'Storage',
'case_documents_count' => '%d documents',
'case_drop_pdf' => 'Drop PDF files here, or click to browse',
'case_hint' => 'Max 25 MB per file · OCR runs automatically · all stored encrypted in the EU',
'case_no_docs' => 'No documents yet. Upload your first PDF above.',
'case_uploaded_ok' => 'Uploaded: %s. OCR starts automatically.',
'case_over_25mb' => '%s is over 25 MB — split the file first.',
'case_unknown_err' => 'Unknown error',
'case_delete_failed' => 'Delete failed.',
'case_pages' => 'pages',
'case_uploaded_label' => 'uploaded',
'case_delete' => 'Delete',
'case_confirm_delete' => 'Delete this document for good?',
'case_ocr_pending' => 'Queued',
'case_ocr_running' => 'OCR in progress',
'case_ocr_ready' => 'Ready',
'case_ocr_failed' => 'Failed',
'case_free_gate_title' => 'My Case — build your own case',
'case_free_gate_pitch' => 'Upload your case documents once, and let <strong>all</strong> the tools work on your private corpus.',
'case_free_gate_b1' => '📄 Private document bank with OCR',
'case_free_gate_b2' => '🔍 Hybrid search (BM25 + vector) on your case',
'case_free_gate_b3' => '🧠 All tools can reference your own case',
'case_free_gate_b4' => '🇪🇺 Everything stored in the EU (Germany/Finland/Norway)',
'case_free_gate_cta' => 'See plans from NOK 129/mo',
'sec_analyses' => 'Saved analyses',
'analyses_lede' => 'All results from Correspondence, Advocate, CWS, Deep analysis, Discrepancy and Timeline gather here — ready to reopen, rerun or export.',
'no_analyses' => 'No saved analyses yet. Run a tool to save your first.',
'used_case_badge' => 'Use my case',
'pinned_title' => 'Pinned',
'pin' => 'Pin',
'unpin' => 'Unpin',
'open_analysis' => 'Open',
'delete_analysis' => 'Delete',
'confirm_delete_analysis' => 'Delete this analysis for good?',
'pin_failed' => 'Pin failed.',
'sec_usage' => 'Recent usage (last 25)',
'usage_col_tool' => 'Tool / event',
'usage_col_credits' => 'Credits',
'usage_col_when' => 'When',
'no_usage' => 'No usage recorded yet.',
'sec_mcp' => 'Developers & MCP',
'mcp_desc' => 'Connect Claude Desktop, Claude Code, Cursor, or any MCP-compatible client to the full tool suite.',
'mcp_token_lbl' => 'API token',
'mcp_no_token' => 'No active token',
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
'mcp_remote_lbl' => 'Remote HTTP — Cursor, Zed, Windsurf',
'mcp_copy' => 'Copy',
'mcp_manage' => 'Manage tokens →',
'mcp_full_setup' => 'Full setup guide & token management →',
'mcp_locked' => 'MCP tokens require Plus or Pro — upgrade to connect AI clients.',
'mcp_loading' => 'Loading…',
],
'no' => [
'page_title' => 'Min konto',
'page_lede' => 'Plan, profil, sak-lagring, analyser, bruk og MCP-tilgang — alt på ett sted.',
'signed_in_as' => 'Innlogget som',
'jump_plan' => 'Plan',
'jump_profile' => 'Profil',
'jump_workspace' => 'Arbeidsområde',
'jump_case' => 'Min Sak',
'jump_analyses' => 'Lagrede analyser',
'jump_usage' => 'Bruk',
'jump_mcp' => 'MCP',
'trial_badge' => 'Prøveperiode — %d dager igjen',
'sec_plan' => 'Plan & kreditter',
'current_plan' => 'Nåværende plan',
'renews_on' => 'Fornyes',
'no_active_sub' => 'Ingen aktiv abonnement.',
'see_plans' => 'Se alle planer',
'available_credits' => 'Tilgjengelige kreditter',
'monthly' => 'månedlige',
'prepaid' => 'forhåndsbetalte',
'manage_subscription'=> 'Administrer abonnement',
'upgrade_plan' => 'Oppgrader plan',
'top_up_credits' => 'Kjøp kreditter',
'payment_confirmed' => 'Betalingen er bekreftet. Hvis du nettopp abonnerte, kan det ta noen sekunder før kontoen oppdateres.',
'bonus_survey_title' => 'Bonus-kreditter (undersøkelse)',
'bonus_survey_done' => '✓ Du har allerede mottatt 25 bonuskreditter for å fylle ut undersøkelsen.',
'bonus_survey_pitch' => 'Svar på 5 korte spørsmål om dine behov og motta 25 bonus-kreditter — utløper aldri.',
'take_survey_btn' => 'Ta undersøkelsen',
'portal_loading' => 'Kobler til…',
'portal_error' => 'Kunne ikke åpne portalen.',
'network_error' => 'Nettverksfeil: ',
'sec_profile' => 'Profildetaljer',
'profile_optional' => 'Valgfritt',
'f_display_name' => 'Visningsnavn',
'f_phone' => 'Telefon',
'f_address_1' => 'Adresselinje 1',
'f_address_2' => 'Adresselinje 2',
'f_postal' => 'Postnummer',
'f_city' => 'Sted',
'f_region' => 'Fylke',
'f_country' => 'Land',
'save_profile' => 'Lagre profil',
'profile_saving' => 'Lagrer…',
'profile_saved' => 'Lagret',
'profile_error_default' => 'Kunne ikke lagre profilen',
'sec_workspace' => 'Arbeidsområde-tilgang',
'w_email' => 'E-post',
'w_seats' => 'Plasser',
'w_role' => 'Rolle',
'w_owner' => 'Eier',
'w_plan_storage' => 'Plan-lagring',
'w_seats_single' => 'Enkeltbruker-arbeidsområde',
'w_upgrade_for_storage' => 'Oppgrader for Min Sak-lagring',
'sec_case' => 'Min Sak',
'case_lede' => 'Last opp dokumentene dine én gang. Alle verktøyene kan deretter referere til din private sak når du krysser av "Bruk min sak som kontekst".',
'case_storage' => 'Lagring',
'case_documents_count' => '%d dokumenter',
'case_drop_pdf' => 'Slipp PDF-filer her, eller klikk for å bla',
'case_hint' => 'Maks 25 MB per fil · OCR kjøres automatisk · alt lagres kryptert i EU',
'case_no_docs' => 'Ingen dokumenter ennå. Last opp din første PDF over.',
'case_uploaded_ok' => 'Lastet opp: %s. OCR starter automatisk.',
'case_over_25mb' => '%s er over 25 MB — del opp filen først.',
'case_unknown_err' => 'Ukjent feil',
'case_delete_failed' => 'Sletting feilet.',
'case_pages' => 'sider',
'case_uploaded_label' => 'lastet opp',
'case_delete' => 'Slett',
'case_confirm_delete' => 'Slette dette dokumentet for godt?',
'case_ocr_pending' => 'I kø',
'case_ocr_running' => 'OCR pågår',
'case_ocr_ready' => 'Klar',
'case_ocr_failed' => 'Feilet',
'case_free_gate_title' => 'Min Sak — bygg din egen sak',
'case_free_gate_pitch' => 'Last opp dokumentene fra saken din én gang, og la <strong>alle</strong> verktøyene jobbe på din private korpus.',
'case_free_gate_b1' => '📄 Privat dokumentbank med OCR',
'case_free_gate_b2' => '🔍 Hybrid søk (BM25 + vektor) i din sak',
'case_free_gate_b3' => '🧠 Alle verktøy kan referere til din egen sak',
'case_free_gate_b4' => '🇪🇺 Alt lagres i EU (Tyskland/Finland/Norge)',
'case_free_gate_cta' => 'Se planer fra NOK 129/mo',
'sec_analyses' => 'Lagrede analyser',
'analyses_lede' => 'Alle resultater fra Korrespondanse, Advokat, BVJ, Dyp analyse, Motstrid og Tidslinje samles her — klar til å gjenåpnes, kjøres på nytt eller eksporteres.',
'no_analyses' => 'Ingen lagrede analyser ennå. Kjør et verktøy for å lagre din første.',
'used_case_badge' => 'Bruk min sak',
'pinned_title' => 'Festet',
'pin' => 'Fest',
'unpin' => 'Løsne',
'open_analysis' => 'Åpne',
'delete_analysis' => 'Slett',
'confirm_delete_analysis' => 'Slette denne analysen for godt?',
'pin_failed' => 'Festing feilet.',
'sec_usage' => 'Nylig bruk (siste 25)',
'usage_col_tool' => 'Verktøy / hendelse',
'usage_col_credits' => 'Kreditter',
'usage_col_when' => 'Tidspunkt',
'no_usage' => 'Ingen bruk registrert ennå.',
'sec_mcp' => 'Utviklere & MCP',
'mcp_desc' => 'Koble Claude Desktop, Claude Code, Cursor eller annen MCP-klient til hele verktøysuiten.',
'mcp_token_lbl' => 'API-token',
'mcp_no_token' => 'Ingen aktiv token',
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
'mcp_remote_lbl' => 'Ekstern HTTP — Cursor, Zed, Windsurf',
'mcp_copy' => 'Kopier',
'mcp_manage' => 'Administrer tokens →',
'mcp_full_setup' => 'Full oppsettsguide & token-administrasjon →',
'mcp_locked' => 'MCP-tokens krever Plus eller Pro — oppgrader for å koble til AI-klienter.',
'mcp_loading' => 'Laster…',
],
'uk' => [
'page_title' => 'Мій обліковий запис',
'page_lede' => 'План, профіль, сховище справ, аналізи, використання та доступ MCP — все в одному місці.',
'signed_in_as' => 'Ввійшли як',
'jump_plan' => 'План',
'jump_profile' => 'Профіль',
'jump_workspace' => 'Робочий простір',
'jump_case' => 'Моя справа',
'jump_analyses' => 'Збережені аналізи',
'jump_usage' => 'Використання',
'jump_mcp' => 'MCP',
'trial_badge' => 'Пробний — %d дн. залишилось',
'sec_plan' => 'План і кредити',
'current_plan' => 'Поточний план',
'renews_on' => 'Поновлюється',
'no_active_sub' => 'Немає активної підписки.',
'see_plans' => 'Переглянути всі плани',
'available_credits' => 'Доступні кредити',
'monthly' => 'місячних',
'prepaid' => 'передоплачених',
'manage_subscription'=> 'Керувати підпискою',
'upgrade_plan' => 'Покращити план',
'top_up_credits' => 'Поповнити кредити',
'payment_confirmed' => 'Платіж підтверджено. Якщо ви щойно підписалися, оновлення облікового запису може зайняти кілька секунд.',
'bonus_survey_title' => 'Бонусні кредити (опитування)',
'bonus_survey_done' => '✓ Ви вже отримали 25 бонусних кредитів за заповнення опитування.',
'bonus_survey_pitch' => 'Дайте відповідь на 5 коротких запитань про ваші потреби та отримайте 25 бонусних кредитів — без терміну дії.',
'take_survey_btn' => 'Пройти опитування',
'portal_loading' => 'З\'єднання…',
'portal_error' => 'Не вдалося відкрити портал.',
'network_error' => 'Помилка мережі: ',
'sec_profile' => 'Дані профілю',
'profile_optional' => 'Необов\'язково',
'f_display_name' => 'Ім\'я для показу',
'f_phone' => 'Телефон',
'f_address_1' => 'Адреса, рядок 1',
'f_address_2' => 'Адреса, рядок 2',
'f_postal' => 'Поштовий індекс',
'f_city' => 'Місто',
'f_region' => 'Регіон',
'f_country' => 'Країна',
'save_profile' => 'Зберегти профіль',
'profile_saving' => 'Збереження…',
'profile_saved' => 'Збережено',
'profile_error_default' => 'Не вдалося зберегти профіль',
'sec_workspace' => 'Доступ до робочого простору',
'w_email' => 'Email',
'w_seats' => 'Місця',
'w_role' => 'Роль',
'w_owner' => 'Власник',
'w_plan_storage' => 'Сховище плану',
'w_seats_single' => 'Робочий простір на одного користувача',
'w_upgrade_for_storage' => 'Оновіть для сховища «Моя справа»',
'sec_case' => 'Моя справа',
'case_lede' => 'Завантажте документи вашої справи один раз. Усі інструменти зможуть посилатися на вашу приватну справу, коли ви поставите галочку «Використати мою справу як контекст».',
'case_storage' => 'Сховище',
'case_documents_count' => '%d документів',
'case_drop_pdf' => 'Перетягніть PDF-файли сюди або клацніть, щоб вибрати',
'case_hint' => 'Макс. 25 МБ на файл · OCR запускається автоматично · все зберігається зашифрованим у ЄС',
'case_no_docs' => 'Поки що немає документів. Завантажте перший PDF вище.',
'case_uploaded_ok' => 'Завантажено: %s. OCR починається автоматично.',
'case_over_25mb' => '%s перевищує 25 МБ — розділіть файл спочатку.',
'case_unknown_err' => 'Невідома помилка',
'case_delete_failed' => 'Видалення не вдалося.',
'case_pages' => 'стор.',
'case_uploaded_label' => 'завантажено',
'case_delete' => 'Видалити',
'case_confirm_delete' => 'Видалити цей документ назавжди?',
'case_ocr_pending' => 'У черзі',
'case_ocr_running' => 'OCR виконується',
'case_ocr_ready' => 'Готово',
'case_ocr_failed' => 'Не вдалося',
'case_free_gate_title' => 'Моя справа — побудуйте власну справу',
'case_free_gate_pitch' => 'Завантажте документи з вашої справи один раз, і нехай <strong>усі</strong> інструменти працюють з вашим приватним корпусом.',
'case_free_gate_b1' => '📄 Приватний банк документів з OCR',
'case_free_gate_b2' => '🔍 Гібридний пошук (BM25 + вектор) у вашій справі',
'case_free_gate_b3' => '🧠 Усі інструменти можуть посилатися на вашу справу',
'case_free_gate_b4' => '🇪🇺 Усе зберігається в ЄС (Німеччина/Фінляндія/Норвегія)',
'case_free_gate_cta' => 'Переглянути плани від NOK 129/міс',
'sec_analyses' => 'Збережені аналізи',
'analyses_lede' => 'Усі результати з Кореспонденції, Адвоката, BVJ, Глибокого аналізу, Розбіжностей і Хронології збираються тут — готові до повторного відкриття, перезапуску чи експорту.',
'no_analyses' => 'Поки що немає збережених аналізів. Запустіть інструмент, щоб зберегти перший.',
'used_case_badge' => 'Моя справа',
'pinned_title' => 'Закріплено',
'pin' => 'Закріпити',
'unpin' => 'Відкріпити',
'open_analysis' => 'Відкрити',
'delete_analysis' => 'Видалити',
'confirm_delete_analysis' => 'Видалити цей аналіз назавжди?',
'pin_failed' => 'Закріпити не вдалося.',
'sec_usage' => 'Нещодавнє використання (останні 25)',
'usage_col_tool' => 'Інструмент / подія',
'usage_col_credits' => 'Кредити',
'usage_col_when' => 'Коли',
'no_usage' => 'Використання ще не зареєстровано.',
'sec_mcp' => 'Розробники та MCP',
'mcp_desc' => 'Підключайте Claude Desktop, Claude Code, Cursor або будь-який MCP-клієнт до повного набору інструментів.',
'mcp_token_lbl' => 'API-токен',
'mcp_no_token' => 'Немає активного токена',
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
'mcp_remote_lbl' => 'Віддалений HTTP — Cursor, Zed, Windsurf',
'mcp_copy' => 'Копіювати',
'mcp_manage' => 'Керувати токенами →',
'mcp_full_setup' => 'Повна документація та управління токенами →',
'mcp_locked' => 'MCP-токени потребують Plus або Pro — оновіться для підключення AI-клієнтів.',
'mcp_loading' => 'Завантаження…',
],
'pl' => [
'page_title' => 'Moje konto',
'page_lede' => 'Plan, profil, magazyn sprawy, analizy, użycie i dostęp MCP — wszystko w jednym miejscu.',
'signed_in_as' => 'Zalogowany jako',
'jump_plan' => 'Plan',
'jump_profile' => 'Profil',
'jump_workspace' => 'Obszar roboczy',
'jump_case' => 'Moja sprawa',
'jump_analyses' => 'Zapisane analizy',
'jump_usage' => 'Użycie',
'jump_mcp' => 'MCP',
'trial_badge' => 'Próba — %d dni pozostało',
'sec_plan' => 'Plan i kredyty',
'current_plan' => 'Bieżący plan',
'renews_on' => 'Odnawia się',
'no_active_sub' => 'Brak aktywnej subskrypcji.',
'see_plans' => 'Zobacz wszystkie plany',
'available_credits' => 'Dostępne kredyty',
'monthly' => 'miesięcznych',
'prepaid' => 'przedpłaconych',
'manage_subscription'=> 'Zarządzaj subskrypcją',
'upgrade_plan' => 'Ulepsz plan',
'top_up_credits' => 'Doładuj kredyty',
'payment_confirmed' => 'Płatność potwierdzona. Jeśli właśnie wykupiono subskrypcję, aktualizacja konta może potrwać kilka sekund.',
'bonus_survey_title' => 'Kredyty bonusowe (ankieta)',
'bonus_survey_done' => '✓ Otrzymałeś już 25 kredytów bonusowych za wypełnienie ankiety.',
'bonus_survey_pitch' => 'Odpowiedz na 5 krótkich pytań o swoje potrzeby i otrzymaj 25 kredytów bonusowych — nigdy nie wygasają.',
'take_survey_btn' => 'Wypełnij ankietę',
'portal_loading' => 'Łączenie…',
'portal_error' => 'Nie można otworzyć portalu.',
'network_error' => 'Błąd sieci: ',
'sec_profile' => 'Dane profilu',
'profile_optional' => 'Opcjonalne',
'f_display_name' => 'Nazwa wyświetlana',
'f_phone' => 'Telefon',
'f_address_1' => 'Adres linia 1',
'f_address_2' => 'Adres linia 2',
'f_postal' => 'Kod pocztowy',
'f_city' => 'Miasto',
'f_region' => 'Region',
'f_country' => 'Kraj',
'save_profile' => 'Zapisz profil',
'profile_saving' => 'Zapisywanie…',
'profile_saved' => 'Zapisano',
'profile_error_default' => 'Nie udało się zapisać profilu',
'sec_workspace' => 'Dostęp do obszaru roboczego',
'w_email' => 'Email',
'w_seats' => 'Miejsca',
'w_role' => 'Rola',
'w_owner' => 'Właściciel',
'w_plan_storage' => 'Magazyn planu',
'w_seats_single' => 'Obszar roboczy jednoosobowy',
'w_upgrade_for_storage' => 'Ulepsz, aby uzyskać magazyn Mojej sprawy',
'sec_case' => 'Moja sprawa',
'case_lede' => 'Prześlij dokumenty swojej sprawy raz. Wszystkie narzędzia mogą się do nich odwoływać, gdy zaznaczysz „Użyj mojej sprawy jako kontekstu".',
'case_storage' => 'Magazyn',
'case_documents_count' => '%d dokumentów',
'case_drop_pdf' => 'Upuść pliki PDF tutaj lub kliknij, aby przeglądać',
'case_hint' => 'Maks. 25 MB na plik · OCR działa automatycznie · wszystko przechowywane zaszyfrowane w UE',
'case_no_docs' => 'Brak dokumentów. Prześlij swój pierwszy PDF powyżej.',
'case_uploaded_ok' => 'Przesłano: %s. OCR rozpoczyna się automatycznie.',
'case_over_25mb' => '%s przekracza 25 MB — najpierw podziel plik.',
'case_unknown_err' => 'Nieznany błąd',
'case_delete_failed' => 'Usuwanie nie powiodło się.',
'case_pages' => 'stron',
'case_uploaded_label' => 'przesłano',
'case_delete' => 'Usuń',
'case_confirm_delete' => 'Usunąć ten dokument na zawsze?',
'case_ocr_pending' => 'W kolejce',
'case_ocr_running' => 'OCR w toku',
'case_ocr_ready' => 'Gotowe',
'case_ocr_failed' => 'Nieudane',
'case_free_gate_title' => 'Moja sprawa — zbuduj własną sprawę',
'case_free_gate_pitch' => 'Prześlij dokumenty swojej sprawy raz i pozwól <strong>wszystkim</strong> narzędziom pracować na Twoim prywatnym korpusie.',
'case_free_gate_b1' => '📄 Prywatny bank dokumentów z OCR',
'case_free_gate_b2' => '🔍 Wyszukiwanie hybrydowe (BM25 + wektor) w Twojej sprawie',
'case_free_gate_b3' => '🧠 Wszystkie narzędzia mogą odwoływać się do Twojej sprawy',
'case_free_gate_b4' => '🇪🇺 Wszystko przechowywane w UE (Niemcy/Finlandia/Norwegia)',
'case_free_gate_cta' => 'Zobacz plany od NOK 129/mies',
'sec_analyses' => 'Zapisane analizy',
'analyses_lede' => 'Wszystkie wyniki z Korespondencji, Adwokata, BVJ, Głębokiej analizy, Rozbieżności i Osi czasu zbierają się tutaj — gotowe do ponownego otwarcia, uruchomienia lub eksportu.',
'no_analyses' => 'Brak zapisanych analiz. Uruchom narzędzie, aby zapisać pierwszą.',
'used_case_badge' => 'Moja sprawa',
'pinned_title' => 'Przypięte',
'pin' => 'Przypnij',
'unpin' => 'Odepnij',
'open_analysis' => 'Otwórz',
'delete_analysis' => 'Usuń',
'confirm_delete_analysis' => 'Usunąć tę analizę na zawsze?',
'pin_failed' => 'Przypinanie nie powiodło się.',
'sec_usage' => 'Ostatnie użycie (ostatnie 25)',
'usage_col_tool' => 'Narzędzie / zdarzenie',
'usage_col_credits' => 'Kredyty',
'usage_col_when' => 'Kiedy',
'no_usage' => 'Nie zarejestrowano jeszcze użycia.',
'sec_mcp' => 'Deweloperzy i MCP',
'mcp_desc' => 'Podłącz Claude Desktop, Claude Code, Cursor lub dowolnego klienta MCP do pełnego zestawu narzędzi.',
'mcp_token_lbl' => 'Token API',
'mcp_no_token' => 'Brak aktywnego tokenu',
'mcp_stdio_lbl' => 'Claude Desktop / Claude Code (stdio)',
'mcp_remote_lbl' => 'Zdalny HTTP — Cursor, Zed, Windsurf',
'mcp_copy' => 'Kopiuj',
'mcp_manage' => 'Zarządzaj tokenami →',
'mcp_full_setup' => 'Pełna dokumentacja i zarządzanie tokenami →',
'mcp_locked' => 'Tokeny MCP wymagają Plus lub Pro — zaktualizuj, aby połączyć klientów AI.',
'mcp_loading' => 'Ładowanie…',
],
];
$l = $L[$uiLang] ?? $L['en'];
$seatsLabel = $seatLimit > 1 ? '1 / ' . $seatLimit : $l['w_seats_single'];
$dateLocale = match ($uiLang) {
'no' => 'j. F Y',
default => 'j M Y',
};
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($l['page_title']) ?> — Do Better Norge</title>
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#00205B">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="assets/css/tools.css">
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
<link rel="stylesheet" href="assets/css/dashboard.css">
<style>
.acct-shell { max-width: 1100px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
.acct-header { margin: 0 0 1.5rem; }
.acct-header h1 { font-family: 'Crimson Pro', serif; margin: 0 0 .25rem; color: #00205B; font-size: 2.1rem; }
.acct-header .lede { color: #6b7280; margin: 0 0 .5rem; font-size: 1rem; }
.acct-header .meta { color: #6b7280; font-size: .9rem; }
.acct-header .meta strong { color: #1f2937; font-weight: 600; }
.acct-subnav { position: sticky; top: 0; z-index: 20; background: #f5f3f0; padding: .75rem 1.25rem; margin: 0 -1.25rem 1.5rem; border-bottom: 1px solid #e5e7eb; overflow-x: auto; white-space: nowrap; }
.acct-subnav a { display: inline-block; padding: .4rem .9rem; margin-right: .4rem; border-radius: 999px; background: #fff; border: 1px solid #e5e7eb; color: #374151; font-size: .88rem; font-weight: 600; text-decoration: none; transition: background .12s, border-color .12s; }
.acct-subnav a:hover { background: #f8fafc; border-color: #cbd5e1; }
.acct-subnav a.is-current { background: #00205B; color: #fff; border-color: #00205B; }
.acct-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 1.5rem; margin: 0 0 1.5rem; scroll-margin-top: 4.5rem; }
.acct-section h2 { font-family: 'Crimson Pro', serif; margin: 0 0 1rem; font-size: 1.4rem; color: #00205B; display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.acct-section h2 .chip { font-family: 'IBM Plex Sans', sans-serif; font-size: .72rem; font-weight: 600; padding: 3px 10px; border-radius: 999px; background: #f3f4f6; color: #374151; text-transform: uppercase; letter-spacing: .04em; }
.acct-section .acct-lede { color: #6b7280; margin: -.5rem 0 1rem; font-size: .95rem; }
.acct-plan-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
@media (max-width: 720px) { .acct-plan-grid { grid-template-columns: 1fr; } }
.acct-plan-cell { padding: 1.25rem; background: #f8fafc; border-radius: 10px; }
.acct-plan-cell h3 { margin: 0 0 .5rem; font-size: .85rem; color: #6b7280; text-transform: uppercase; letter-spacing: .05em; }
.acct-tier-pill { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: .85rem; font-weight: 700; }
.acct-trial-pill { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: .82rem; font-weight: 600; background: #fef3c7; color: #92400e; margin-left: .35rem; }
.acct-credits-big { font-size: 2.4rem; font-weight: 700; color: #00205B; margin: .25rem 0; line-height: 1; }
.acct-credits-break { color: #6b7280; font-size: .9rem; margin: 0; }
.acct-actions { display: flex; flex-wrap: wrap; gap: .65rem; margin: 1rem 0 0; }
.btn { padding: .65rem 1.2rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; font-size: .92rem; font-family: inherit; }
.btn-primary { background: #00205B; color: #fff; }
.btn-primary:hover { background: #001740; }
.btn-secondary { background: #f3f4f6; color: #1f2937; }
.btn-secondary:hover { background: #e5e7eb; }
.btn-accent { background: #c9a84c; color: #1f1300; }
.btn-accent:hover { background: #b89540; }
.acct-toast { padding: .75rem 1rem; border-radius: 8px; font-size: .92rem; margin: 0 0 1rem; background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
.acct-bonus { margin-top: 1.25rem; padding: 1.25rem; background: #fefce8; border: 1px solid #fef08a; border-radius: 10px; }
.acct-bonus h3 { margin: 0 0 .35rem; font-size: 1rem; color: #713f12; }
.acct-bonus p { margin: 0; color: #713f12; font-size: .9rem; }
.acct-bonus .acct-actions { margin-top: .75rem; }
.acct-profile-form { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 640px) { .acct-profile-form { grid-template-columns: 1fr; } }
.acct-profile-form .span-2 { grid-column: span 2; }
@media (max-width: 640px) { .acct-profile-form .span-2 { grid-column: span 1; } }
.acct-profile-form label { display: flex; flex-direction: column; font-size: .85rem; color: #374151; font-weight: 500; }
.acct-profile-form label > span { margin-bottom: .25rem; }
.acct-profile-form input { padding: .55rem .75rem; border: 1px solid #d1d5db; border-radius: 6px; font: inherit; font-size: .95rem; background: #fff; }
.acct-profile-form input:focus { outline: none; border-color: #00205B; box-shadow: 0 0 0 3px rgba(0,32,91,.12); }
.acct-profile-actions { display: flex; align-items: center; gap: 1rem; }
.acct-profile-status { color: #059669; font-size: .9rem; margin: 0; min-height: 1.2em; }
.acct-list { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem 2rem; }
@media (max-width: 640px) { .acct-list { grid-template-columns: 1fr; } }
.acct-list > div { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; padding-bottom: .6rem; border-bottom: 1px solid #f3f4f6; }
.acct-list span { color: #6b7280; font-size: .9rem; }
.acct-list strong { color: #1f2937; font-weight: 600; font-size: .95rem; text-align: right; }
.acct-case-status { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.25rem; }
@media (max-width: 640px) { .acct-case-status { grid-template-columns: 1fr; } }
.acct-storage-bar { background: #f3f4f6; height: 10px; border-radius: 999px; overflow: hidden; margin: .6rem 0 .35rem; }
.acct-storage-bar > div { background: linear-gradient(90deg, #00205B, #003478); height: 100%; }
.acct-upload { background: #f8fafc; border: 2px dashed #cbd5e1; border-radius: 12px; padding: 2rem 1.5rem; text-align: center; cursor: pointer; transition: all .15s; margin-bottom: 1.25rem; display: block; }
.acct-upload:hover, .acct-upload.is-drag { border-color: #00205B; background: #eff6ff; }
.acct-upload input { display: none; }
.acct-upload-icon { font-size: 2.2rem; line-height: 1; margin-bottom: .5rem; }
.acct-upload p { margin: .25rem 0; color: #374151; }
.acct-upload .hint { color: #6b7280; font-size: .85rem; }
.acct-flash { margin: 0 0 1rem; min-height: 1.2em; }
.acct-flash.is-ok { padding: .7rem 1rem; border-radius: 6px; background: #d1fae5; color: #065f46; font-size: .9rem; }
.acct-flash.is-err { padding: .7rem 1rem; border-radius: 6px; background: #fee2e2; color: #991b1b; font-size: .9rem; }
.acct-docs { border: 1px solid #e5e7eb; border-radius: 10px; overflow: hidden; }
.acct-doc { padding: .9rem 1.1rem; border-bottom: 1px solid #f3f4f6; display: flex; align-items: center; gap: 1rem; background: #fff; }
.acct-doc:last-child { border-bottom: none; }
.acct-doc-info { flex: 1; min-width: 0; }
.acct-doc-name { font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.acct-doc-meta { font-size: .85rem; color: #6b7280; }
.acct-status-pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: .78rem; font-weight: 600; }
.acct-status-pending { background: #fef3c7; color: #92400e; }
.acct-status-running { background: #ddd6fe; color: #5b21b6; }
.acct-status-ready { background: #d1fae5; color: #065f46; }
.acct-status-failed { background: #fee2e2; color: #991b1b; }
.acct-empty { padding: 1.5rem; text-align: center; color: #6b7280; background: #fff; }
.acct-result { display: flex; align-items: center; gap: 1rem; padding: .9rem 1.1rem; border-bottom: 1px solid #f3f4f6; background: #fff; }
.acct-result:last-child { border-bottom: none; }
.acct-result-title { font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.acct-result-title a { color: inherit; text-decoration: none; }
.acct-result-meta { font-size: .85rem; color: #6b7280; }
.acct-result-actions { display: flex; gap: .4rem; flex-shrink: 0; }
.btn-mini { background: #f3f4f6; border: none; padding: 5px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: .82rem; color: #1f2937; text-decoration: none; display: inline-block; }
.btn-mini:hover { background: #e5e7eb; }
.btn-mini-primary { background: #00205B; color: #fff; }
.btn-mini-primary:hover { background: #001740; }
.acct-usage-table { width: 100%; border-collapse: collapse; }
.acct-usage-table th, .acct-usage-table td { padding: 8px 12px; border-bottom: 1px solid #f3f4f6; text-align: left; font-size: .9rem; }
.acct-usage-table th { color: #6b7280; font-weight: 500; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; }
.acct-usage-table .credit-neg { color: #059669; font-weight: 600; }
.acct-usage-table .credit-pos { color: #b91c1c; }
.acct-mcp-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin: 0 0 1rem; padding: .7rem 1rem; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; }
.acct-mcp-row code { font-size: .85rem; color: #111827; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-mcp-locked { padding: .75rem 1rem; background: #fefce8; border: 1px solid #fef08a; border-radius: 8px; color: #713f12; font-size: .92rem; display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
.acct-mcp-pre { background: #101828; color: #f9fafb; padding: .85rem 3.5rem .85rem 1rem; border-radius: 8px; font-size: .78rem; overflow-x: auto; margin: 0; line-height: 1.7; white-space: pre; }
.acct-mcp-pre-wrap { position: relative; margin: 0 0 1rem; }
.acct-mcp-pre-wrap button { position: absolute; top: .5rem; right: .5rem; background: rgba(249,250,251,.12); border: 1px solid rgba(249,250,251,.2); color: #f9fafb; font-size: .72rem; padding: 3px 9px; border-radius: 5px; cursor: pointer; }
.acct-mcp-pre-wrap button:hover { background: rgba(249,250,251,.2); }
</style>
</head>
<body data-authenticated="true" class="lt-app">
<script>
window.DBN_TOOLS_AUTHENTICATED = true;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<?php include __DIR__ . '/includes/nav.php'; ?>
<div class="dbn-context-bar" role="note">
<span class="dbn-context-bar__tag"><?= htmlspecialchars(dbnToolsT('context_bar_tag', $uiLang)) ?></span>
<nav class="dbn-context-bar__links" aria-label="About">
<a href="/why-ours.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_why', $uiLang)) ?></a>
<a href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_pricing', $uiLang)) ?></a>
<a href="/mcp-tool.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_mcp', $uiLang)) ?></a>
<a href="/privacy.php<?= $langSuffix ?>"><?= htmlspecialchars(dbnToolsT('context_bar_privacy', $uiLang)) ?></a>
</nav>
</div>
<main id="appShell" class="app-shell acct-shell">
<header class="acct-header">
<h1><?= htmlspecialchars($l['page_title']) ?></h1>
<p class="lede"><?= htmlspecialchars($l['page_lede']) ?></p>
<?php if ($email !== ''): ?>
<p class="meta"><?= htmlspecialchars($l['signed_in_as']) ?>: <strong><?= htmlspecialchars($email) ?></strong></p>
<?php endif; ?>
</header>
<nav class="acct-subnav" aria-label="Account sections">
<a href="#plan"><?= htmlspecialchars($l['jump_plan']) ?></a>
<a href="#profile"><?= htmlspecialchars($l['jump_profile']) ?></a>
<a href="#workspace"><?= htmlspecialchars($l['jump_workspace']) ?></a>
<a href="#case"><?= htmlspecialchars($l['jump_case']) ?></a>
<?php if ($isPaidTier): ?>
<a href="#analyses"><?= htmlspecialchars($l['jump_analyses']) ?></a>
<?php endif; ?>
<a href="#usage"><?= htmlspecialchars($l['jump_usage']) ?></a>
<?php if ($isPaidTier): ?>
<a href="#mcp"><?= htmlspecialchars($l['jump_mcp']) ?></a>
<?php endif; ?>
</nav>
<?php if ($paidToast): ?>
<p class="acct-toast"><?= htmlspecialchars($l['payment_confirmed']) ?></p>
<?php endif; ?>
<!-- ── #plan ─────────────────────────────────────────────── -->
<section class="acct-section" id="plan">
<h2><?= htmlspecialchars($l['sec_plan']) ?></h2>
<div class="acct-plan-grid">
<div class="acct-plan-cell">
<h3><?= htmlspecialchars($l['current_plan']) ?></h3>
<p style="margin:.25rem 0;">
<span class="acct-tier-pill" style="background:<?= $tierLabel[1] ?>; color:<?= $tierLabel[2] ?>;"><?= htmlspecialchars($tierLabel[0]) ?></span>
<?php if (!empty($detail['trial_active'])): ?>
<span class="acct-trial-pill"><?= htmlspecialchars(sprintf($l['trial_badge'], (int)$detail['trial_days_remaining'])) ?></span>
<?php endif; ?>
</p>
<?php if (!empty($detail['subscription_period_end'])): ?>
<p class="acct-credits-break"><?= htmlspecialchars($l['renews_on']) ?>: <strong><?= htmlspecialchars(date($dateLocale, strtotime((string)$detail['subscription_period_end']))) ?></strong></p>
<?php elseif ($tier === 'free'): ?>
<p class="acct-credits-break"><?= htmlspecialchars($l['no_active_sub']) ?> <a href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['see_plans']) ?></a></p>
<?php endif; ?>
</div>
<div class="acct-plan-cell">
<h3><?= htmlspecialchars($l['available_credits']) ?></h3>
<p class="acct-credits-big"><?= number_format($effective, 0, ',', ' ') ?></p>
<p class="acct-credits-break">
<?= (int)$detail['balance'] ?> <?= htmlspecialchars($l['monthly']) ?>
· <?= (int)$detail['bonus_balance'] ?> <?= htmlspecialchars($l['prepaid']) ?>
</p>
</div>
</div>
<div class="acct-actions">
<?php if ($isPaidTier): ?>
<button type="button" id="portalBtn" class="btn btn-secondary"><?= htmlspecialchars($l['manage_subscription']) ?></button>
<?php else: ?>
<a class="btn btn-primary" href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['upgrade_plan']) ?> →</a>
<?php endif; ?>
<a class="btn btn-secondary" href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['see_plans']) ?></a>
<a class="btn btn-accent" href="/pricing.php<?= $langSuffix ?>#topup"><?= htmlspecialchars($l['top_up_credits']) ?> →</a>
</div>
<?php if ($isSso): ?>
<div class="acct-bonus">
<h3><?= htmlspecialchars($l['bonus_survey_title']) ?></h3>
<?php if (!empty($detail['survey_completed_at'])): ?>
<p style="color:#059669;"><?= htmlspecialchars($l['bonus_survey_done']) ?></p>
<?php else: ?>
<p><?= htmlspecialchars($l['bonus_survey_pitch']) ?></p>
<div class="acct-actions">
<a class="btn btn-primary" href="https://dobetternorge.no/survey.php"><?= htmlspecialchars($l['take_survey_btn']) ?></a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
<!-- ── #profile ──────────────────────────────────────────── -->
<section class="acct-section" id="profile">
<h2>
<?= htmlspecialchars($l['sec_profile']) ?>
<span class="chip"><?= htmlspecialchars($l['profile_optional']) ?></span>
</h2>
<form id="profileForm" class="acct-profile-form">
<label>
<span><?= htmlspecialchars($l['f_display_name']) ?></span>
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($profileVal('display_name')) ?>">
</label>
<label>
<span><?= htmlspecialchars($l['f_phone']) ?></span>
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($profileVal('phone')) ?>">
</label>
<label class="span-2">
<span><?= htmlspecialchars($l['f_address_1']) ?></span>
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($profileVal('address_line1')) ?>">
</label>
<label class="span-2">
<span><?= htmlspecialchars($l['f_address_2']) ?></span>
<input name="address_line2" type="text" maxlength="180" autocomplete="address-line2" value="<?= htmlspecialchars($profileVal('address_line2')) ?>">
</label>
<label>
<span><?= htmlspecialchars($l['f_postal']) ?></span>
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($profileVal('postal_code')) ?>">
</label>
<label>
<span><?= htmlspecialchars($l['f_city']) ?></span>
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($profileVal('city')) ?>">
</label>
<label>
<span><?= htmlspecialchars($l['f_region']) ?></span>
<input name="address_region" type="text" maxlength="100" autocomplete="address-level1" value="<?= htmlspecialchars($profileVal('address_region')) ?>">
</label>
<label>
<span><?= htmlspecialchars($l['f_country']) ?></span>
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($profileVal('country')) ?>">
</label>
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
<div class="acct-profile-actions span-2">
<button type="submit" class="btn btn-primary"><?= htmlspecialchars($l['save_profile']) ?></button>
<p id="profileStatus" class="acct-profile-status" role="status" aria-live="polite"></p>
</div>
</form>
</section>
<!-- ── #workspace ────────────────────────────────────────── -->
<section class="acct-section" id="workspace">
<h2>
<?= htmlspecialchars($l['sec_workspace']) ?>
<span class="chip"><?= htmlspecialchars($tierLabel[0]) ?></span>
</h2>
<div class="acct-list">
<div>
<span><?= htmlspecialchars($l['w_email']) ?></span>
<strong><?= htmlspecialchars($email) ?></strong>
</div>
<div>
<span><?= htmlspecialchars($l['w_seats']) ?></span>
<strong><?= htmlspecialchars($seatsLabel) ?></strong>
</div>
<div>
<span><?= htmlspecialchars($l['w_role']) ?></span>
<strong><?= htmlspecialchars($l['w_owner']) ?></strong>
</div>
<div>
<span><?= htmlspecialchars($l['w_plan_storage']) ?></span>
<strong><?= $isPaidTier ? htmlspecialchars($quotaMb . ' MB') : htmlspecialchars($l['w_upgrade_for_storage']) ?></strong>
</div>
</div>
</section>
<!-- ── #case ─────────────────────────────────────────────── -->
<section class="acct-section" id="case">
<h2><?= htmlspecialchars($l['sec_case']) ?></h2>
<?php if (!$isPaidTier): ?>
<p class="acct-lede"><?= $l['case_free_gate_pitch'] /* contains <strong> */ ?></p>
<ul style="margin:0 0 1rem; padding-left: 1.25rem; line-height: 1.9; color: #1f2937;">
<li><?= htmlspecialchars($l['case_free_gate_b1']) ?></li>
<li><?= htmlspecialchars($l['case_free_gate_b2']) ?></li>
<li><?= htmlspecialchars($l['case_free_gate_b3']) ?></li>
<li><?= htmlspecialchars($l['case_free_gate_b4']) ?></li>
</ul>
<div class="acct-actions">
<a class="btn btn-primary" href="/pricing.php<?= $langSuffix ?>"><?= htmlspecialchars($l['case_free_gate_cta']) ?></a>
</div>
<?php else: ?>
<p class="acct-lede"><?= htmlspecialchars($l['case_lede']) ?></p>
<div class="acct-case-status">
<div class="acct-plan-cell">
<h3 style="margin:0 0 .25rem; font-size:.82rem; color:#6b7280; text-transform:uppercase; letter-spacing:.05em;"><?= htmlspecialchars($l['case_storage']) ?></h3>
<p style="margin:0; font-size:1.4rem; font-weight:700; color:#00205B;"><?= $storageMb ?> MB <span style="color:#6b7280;font-size:.95rem;font-weight:400;">/ <?= $quotaMb ?> MB</span></p>
<div class="acct-storage-bar"><div style="width: <?= $storagePct ?>%;"></div></div>
<p style="margin:0; color:#6b7280; font-size:.85rem;"><?= htmlspecialchars(sprintf($l['case_documents_count'], count($docs))) ?></p>
</div>
<div class="acct-plan-cell">
<h3 style="margin:0 0 .25rem; font-size:.82rem; color:#6b7280; text-transform:uppercase; letter-spacing:.05em;"><?= htmlspecialchars($l['current_plan']) ?></h3>
<p style="margin:0; font-size:1.1rem; font-weight:700; color:#00205B;">
<?= htmlspecialchars($tierLabel[0]) ?>
<?php if (!empty($detail['trial_active'])): ?>
<span class="acct-trial-pill"><?= htmlspecialchars(sprintf($l['trial_badge'], (int)$detail['trial_days_remaining'])) ?></span>
<?php endif; ?>
</p>
</div>
</div>
<label for="acctFileInput" class="acct-upload" id="acctUploadZone">
<input id="acctFileInput" type="file" accept="application/pdf" multiple>
<div class="acct-upload-icon">📤</div>
<p><strong><?= htmlspecialchars($l['case_drop_pdf']) ?></strong></p>
<p class="hint"><?= htmlspecialchars($l['case_hint']) ?></p>
</label>
<div id="acctFlash" class="acct-flash" role="status" aria-live="polite"></div>
<div class="acct-docs">
<?php if (empty($docs)): ?>
<p class="acct-empty"><?= htmlspecialchars($l['case_no_docs']) ?></p>
<?php else: foreach ($docs as $d): ?>
<div class="acct-doc" data-doc-id="<?= (int)$d['id'] ?>">
<div style="font-size:1.6rem;line-height:1;">📄</div>
<div class="acct-doc-info">
<div class="acct-doc-name"><?= htmlspecialchars($d['filename']) ?></div>
<div class="acct-doc-meta">
<?= round((int)$d['size_bytes'] / 1024, 0) ?> KB
<?php if (!empty($d['page_count'])): ?> · <?= (int)$d['page_count'] ?> <?= htmlspecialchars($l['case_pages']) ?><?php endif; ?>
<?php if (!empty($d['doc_type'])): ?> · <?= htmlspecialchars($d['doc_type']) ?><?php endif; ?>
· <?= htmlspecialchars($l['case_uploaded_label']) ?> <?= htmlspecialchars(date($dateLocale, strtotime((string)$d['uploaded_at']))) ?>
</div>
</div>
<?php $ocrKey = 'case_ocr_' . $d['ocr_status']; ?>
<span class="acct-status-pill acct-status-<?= htmlspecialchars($d['ocr_status']) ?>">
<?= htmlspecialchars($l[$ocrKey] ?? $d['ocr_status']) ?>
</span>
<div>
<button type="button" class="btn-mini acct-delete" data-id="<?= (int)$d['id'] ?>"><?= htmlspecialchars($l['case_delete']) ?></button>
</div>
</div>
<?php endforeach; endif; ?>
</div>
<?php endif; ?>
</section>
<!-- ── #analyses (paid only) ─────────────────────────────── -->
<?php if ($isPaidTier): ?>
<section class="acct-section" id="analyses">
<h2><?= htmlspecialchars($l['sec_analyses']) ?></h2>
<p class="acct-lede"><?= htmlspecialchars($l['analyses_lede']) ?></p>
<div class="acct-docs">
<?php if (empty($results)): ?>
<p class="acct-empty"><?= htmlspecialchars($l['no_analyses']) ?></p>
<?php else: foreach ($results as $r): ?>
<div class="acct-result" data-result-id="<?= (int)$r['id'] ?>">
<div style="font-size:1.4rem;line-height:1;"><?= htmlspecialchars(CaseResults::toolIcon((string)$r['tool'])) ?></div>
<div style="flex:1; min-width:0;">
<div class="acct-result-title">
<a href="/case-result.php?id=<?= (int)$r['id'] ?>">
<?= htmlspecialchars((string)($r['title'] ?? CaseResults::toolLabel((string)$r['tool']))) ?>
</a>
<?php if (!empty($r['pinned'])): ?>
<span title="<?= htmlspecialchars($l['pinned_title']) ?>" style="color:#c9a84c;margin-left:.4rem;">★</span>
<?php endif; ?>
</div>
<div class="acct-result-meta">
<?= htmlspecialchars(CaseResults::toolLabel((string)$r['tool'])) ?>
· <?= htmlspecialchars(date($dateLocale . ' H:i', strtotime((string)$r['created_at']))) ?>
<?php if (!empty($r['used_case_context'])): ?>
· <span style="background:#dbeafe;color:#1e3a8a;padding:1px 8px;border-radius:999px;font-size:.75rem;font-weight:600;"><?= htmlspecialchars($l['used_case_badge']) ?></span>
<?php endif; ?>
</div>
</div>
<div class="acct-result-actions">
<button type="button" class="btn-mini acct-pin" data-id="<?= (int)$r['id'] ?>">
<?= htmlspecialchars(!empty($r['pinned']) ? $l['unpin'] : $l['pin']) ?>
</button>
<a class="btn-mini btn-mini-primary" href="/case-result.php?id=<?= (int)$r['id'] ?>"><?= htmlspecialchars($l['open_analysis']) ?></a>
<button type="button" class="btn-mini acct-result-delete" data-id="<?= (int)$r['id'] ?>"><?= htmlspecialchars($l['delete_analysis']) ?></button>
</div>
</div>
<?php endforeach; endif; ?>
</div>
</section>
<?php endif; ?>
<!-- ── #usage ────────────────────────────────────────────── -->
<section class="acct-section" id="usage">
<h2><?= htmlspecialchars($l['sec_usage']) ?></h2>
<?php if (empty($recent)): ?>
<p class="acct-empty" style="border:1px solid #f3f4f6; border-radius:8px;"><?= htmlspecialchars($l['no_usage']) ?></p>
<?php else: ?>
<table class="acct-usage-table">
<thead><tr>
<th><?= htmlspecialchars($l['usage_col_tool']) ?></th>
<th><?= htmlspecialchars($l['usage_col_credits']) ?></th>
<th><?= htmlspecialchars($l['usage_col_when']) ?></th>
</tr></thead>
<tbody>
<?php foreach ($recent as $r): $credits = (int)$r['credits_used']; ?>
<tr>
<td><?= htmlspecialchars($r['tool']) ?></td>
<td class="<?= $credits < 0 ? 'credit-neg' : 'credit-pos' ?>"><?= $credits < 0 ? '+' . abs($credits) : '' . $credits ?></td>
<td><?= htmlspecialchars((string)$r['created_at']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
<!-- ── #mcp (paid only) ──────────────────────────────────── -->
<?php if ($isPaidTier): ?>
<section class="acct-section" id="mcp">
<h2><?= htmlspecialchars($l['sec_mcp']) ?></h2>
<p class="acct-lede"><?= htmlspecialchars($l['mcp_desc']) ?></p>
<div class="acct-mcp-row">
<span style="font-weight:600; color:#374151; white-space:nowrap;"><?= htmlspecialchars($l['mcp_token_lbl']) ?>:</span>
<code id="acctMcpTokenPrefix"><?= htmlspecialchars($l['mcp_loading']) ?></code>
<a href="/mcp.php<?= $langSuffix ?>" style="font-size:.85rem; color:#00205B; font-weight:600; text-decoration:none; white-space:nowrap;"><?= htmlspecialchars($l['mcp_manage']) ?></a>
</div>
<p style="margin:1rem 0 .35rem; font-size:.85rem; font-weight:600; color:#374151;"><?= htmlspecialchars($l['mcp_stdio_lbl']) ?></p>
<div class="acct-mcp-pre-wrap">
<pre id="acctStdioBlock" class="acct-mcp-pre">claude mcp add dobetternorge -- npx -y @bluenotelogic/mcp dobetternorge-mcp --stdio
# Set token (add to ~/.bashrc or ~/.zshrc to persist):
export DBN_MCP_TOKEN=<span id="acctStdioToken" style="color:#86efac;">dbn_user_mcp_…</span></pre>
<button type="button" onclick="acctCopyBlock('acctStdioBlock', this)"><?= htmlspecialchars($l['mcp_copy']) ?></button>
</div>
<p style="margin:1rem 0 .35rem; font-size:.85rem; font-weight:600; color:#374151;"><?= htmlspecialchars($l['mcp_remote_lbl']) ?></p>
<div class="acct-mcp-pre-wrap">
<pre class="acct-mcp-pre">URL: https://mcp.dobetternorge.no/mcp
Authorization: Bearer <span id="acctRemoteToken" style="color:#86efac;">dbn_user_mcp_…</span></pre>
<button type="button" onclick="acctCopyRemote(this)"><?= htmlspecialchars($l['mcp_copy']) ?></button>
</div>
<p style="margin:1rem 0 0; font-size:.9rem;">
<a href="/mcp.php<?= $langSuffix ?>" style="color:#00205B; font-weight:600;"><?= htmlspecialchars($l['mcp_full_setup']) ?></a>
</p>
</section>
<?php elseif ($isSso): ?>
<section class="acct-section" id="mcp">
<h2><?= htmlspecialchars($l['sec_mcp']) ?></h2>
<div class="acct-mcp-locked">
<span><?= htmlspecialchars($l['mcp_locked']) ?></span>
<a href="/pricing.php<?= $langSuffix ?>" class="btn btn-primary"><?= htmlspecialchars($l['upgrade_plan']) ?> →</a>
</div>
</section>
<?php endif; ?>
</main>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<script>
window.ACCT_L = <?= json_encode($l, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
/* ── Sub-nav active highlight ──────────────────────────────────── */
(function () {
var links = document.querySelectorAll('.acct-subnav a');
var sections = Array.prototype.map.call(links, function (a) {
var id = a.getAttribute('href').slice(1);
return { link: a, el: document.getElementById(id) };
}).filter(function (s) { return s.el; });
function onScroll() {
var y = window.scrollY + 80;
var current = null;
sections.forEach(function (s) {
if (s.el.offsetTop <= y) current = s;
});
sections.forEach(function (s) {
s.link.classList.toggle('is-current', s === current);
});
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}());
/* ── Profile save ──────────────────────────────────────────────── */
(function () {
var form = document.getElementById('profileForm');
var status = document.getElementById('profileStatus');
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
var data = {};
Array.prototype.forEach.call(form.elements, function (el) {
if (!el.name) return;
data[el.name] = el.value || '';
});
data.dismiss_prompt = true;
if (status) { status.textContent = window.ACCT_L.profile_saving; status.style.color = '#059669'; }
fetch('/api/profile.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(data)
}).then(function (r) {
return r.json().then(function (j) {
if (!r.ok || !j.ok) {
throw new Error((j.error && j.error.message) || window.ACCT_L.profile_error_default);
}
if (status) { status.textContent = window.ACCT_L.profile_saved; status.style.color = '#059669'; }
});
}).catch(function (err) {
if (status) { status.textContent = err.message; status.style.color = '#b91c1c'; }
});
});
}());
/* ── Case upload / delete ──────────────────────────────────────── */
(function () {
var fileInput = document.getElementById('acctFileInput');
var dropZone = document.getElementById('acctUploadZone');
var flash = document.getElementById('acctFlash');
if (!fileInput || !dropZone) return;
function showFlash(msg, isError) {
flash.className = 'acct-flash ' + (isError ? 'is-err' : 'is-ok');
flash.textContent = msg;
}
function uploadFile(file) {
if (file.size > 25 * 1024 * 1024) {
showFlash(window.ACCT_L.case_over_25mb.replace('%s', file.name), true);
return;
}
var data = new FormData();
data.append('file', file);
fetch('/api/case/upload.php', { method: 'POST', body: data })
.then(function (r) { return r.json(); })
.then(function (json) {
if (json.ok) {
showFlash(window.ACCT_L.case_uploaded_ok.replace('%s', file.name), false);
setTimeout(function () { window.location.reload(); }, 1200);
} else {
showFlash(file.name + ': ' + ((json.error && json.error.message) || window.ACCT_L.case_unknown_err), true);
}
})
.catch(function (e) { showFlash(window.ACCT_L.network_error + e.message, true); });
}
fileInput.addEventListener('change', function () {
for (var i = 0; i < fileInput.files.length; i++) uploadFile(fileInput.files[i]);
});
['dragenter', 'dragover'].forEach(function (ev) {
dropZone.addEventListener(ev, function (e) { e.preventDefault(); dropZone.classList.add('is-drag'); });
});
['dragleave', 'drop'].forEach(function (ev) {
dropZone.addEventListener(ev, function (e) { e.preventDefault(); dropZone.classList.remove('is-drag'); });
});
dropZone.addEventListener('drop', function (e) {
for (var i = 0; i < e.dataTransfer.files.length; i++) {
var f = e.dataTransfer.files[i];
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) uploadFile(f);
}
});
document.querySelectorAll('.acct-delete').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!confirm(window.ACCT_L.case_confirm_delete)) return;
var id = btn.getAttribute('data-id');
fetch('/api/case/delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ doc_id: parseInt(id, 10) })
}).then(function (r) { return r.json(); })
.then(function (json) {
if (json.ok) window.location.reload();
else alert((json.error && json.error.message) || window.ACCT_L.case_delete_failed);
})
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); });
});
});
}());
/* ── Saved analyses: pin / delete ──────────────────────────────── */
(function () {
document.querySelectorAll('.acct-pin').forEach(function (btn) {
btn.addEventListener('click', function () {
var id = btn.getAttribute('data-id');
fetch('/api/case/result-action.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'pin', id: parseInt(id, 10) })
}).then(function (r) { return r.json(); })
.then(function (json) {
if (json.ok) window.location.reload();
else alert((json.error && json.error.message) || window.ACCT_L.pin_failed);
})
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); });
});
});
document.querySelectorAll('.acct-result-delete').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!confirm(window.ACCT_L.confirm_delete_analysis)) return;
var id = btn.getAttribute('data-id');
fetch('/api/case/result-action.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', id: parseInt(id, 10) })
}).then(function (r) { return r.json(); })
.then(function (json) {
if (json.ok) window.location.reload();
else alert((json.error && json.error.message) || window.ACCT_L.case_delete_failed);
})
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); });
});
});
}());
/* ── Stripe portal ─────────────────────────────────────────────── */
(function () {
var btn = document.getElementById('portalBtn');
if (!btn) return;
btn.addEventListener('click', function () {
btn.disabled = true;
var original = btn.textContent;
btn.textContent = window.ACCT_L.portal_loading;
fetch('/api/stripe-portal.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
}).then(function (r) { return r.json(); })
.then(function (data) {
if (data.ok && data.url) {
window.location.href = data.url;
} else {
alert((data.error && data.error.message) || window.ACCT_L.portal_error);
}
})
.catch(function (e) { alert(window.ACCT_L.network_error + e.message); })
.finally(function () { btn.disabled = false; btn.textContent = original; });
});
}());
<?php if ($isPaidTier): ?>
/* ── MCP token prefix fetch ────────────────────────────────────── */
(function () {
fetch('/api/mcp-tokens.php', { credentials: 'same-origin' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (!data || !data.ok) return;
var active = (data.tokens || []).filter(function (t) { return t.is_active; });
var prefixEl = document.getElementById('acctMcpTokenPrefix');
if (prefixEl) {
prefixEl.textContent = active.length > 0
? active[0].token_prefix + '…'
: window.ACCT_L.mcp_no_token;
}
if (active.length > 0) {
var p = active[0].token_prefix + '…';
var s = document.getElementById('acctStdioToken');
var rr = document.getElementById('acctRemoteToken');
if (s) s.textContent = p;
if (rr) rr.textContent = p;
}
})
.catch(function () {});
}());
window.acctCopyBlock = function (id, btn) {
var el = document.getElementById(id);
if (!el) return;
navigator.clipboard.writeText(el.innerText || el.textContent).then(function () {
var orig = btn.textContent; btn.textContent = '✓';
setTimeout(function () { btn.textContent = orig; }, 1500);
}).catch(function () {});
};
window.acctCopyRemote = function (btn) {
var tok = document.getElementById('acctRemoteToken');
var txt = 'URL: https://mcp.dobetternorge.no/mcp\nAuthorization: Bearer ' + (tok ? tok.textContent : 'dbn_user_mcp_…');
navigator.clipboard.writeText(txt).then(function () {
var orig = btn.textContent; btn.textContent = '✓';
setTimeout(function () { btn.textContent = orig; }, 1500);
}).catch(function () {});
};
<?php endif; ?>
</script>
</body>
</html>