diff --git a/client/src/components/Settings/useIntegrationGates.ts b/client/src/components/Settings/useIntegrationGates.ts index ac6a67226..99f02a43e 100644 --- a/client/src/components/Settings/useIntegrationGates.ts +++ b/client/src/components/Settings/useIntegrationGates.ts @@ -4,9 +4,11 @@ import { useAuthStore } from '../../store/authStore'; /** * Which integrations the settings shells offer: the addon flags behind each - * integration section, whether any of them is on (the Integrations tab only shows - * then), and whether this is a managed install. With `reloadAddons` the addon list - * is fetched again on mount, as the integrations tab on both shells does. + * integration section, whether any of them is on, and whether this is a managed + * install. The desktop Integrations tab shows regardless, since its API keys need + * no addon; the mobile one has no API keys and only shows when `hasIntegrations`. + * With `reloadAddons` the addon list is fetched again on mount, as the + * integrations tab on both shells does. */ export function useIntegrationGates({ reloadAddons }: { reloadAddons: boolean }) { const { isEnabled: addonEnabled, loadAddons } = useAddonStore(); diff --git a/client/src/pages/SettingsPage.test.tsx b/client/src/pages/SettingsPage.test.tsx index d1efd7942..32753916f 100644 --- a/client/src/pages/SettingsPage.test.tsx +++ b/client/src/pages/SettingsPage.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import { resetAllStores, seedStore } from '../../tests/helpers/store'; import { buildUser } from '../../tests/helpers/factories'; import { useAuthStore } from '../store/authStore'; +import { useAddonStore } from '../store/addonStore'; import SettingsPage from './SettingsPage'; // Mock heavy settings sub-tabs to focus on page-level concerns @@ -152,4 +153,27 @@ describe('SettingsPage', () => { }); }); }); + + describe('FE-PAGE-SETTINGS-007: Integrations tab without any integration addon', () => { + // API keys live in the Integrations tab and need no addon, so the tab has to + // be there on an instance where MCP, AirTrail, AI Parsing, Dawarich and the + // photo providers are all switched off. + it('still offers the Integrations tab and opens it', async () => { + seedStore(useAddonStore, { isEnabled: () => false, loadAddons: vi.fn().mockResolvedValue(undefined) }); + const user = userEvent.setup(); + render(); + + await user.click(await screen.findByRole('button', { name: /integrations/i })); + + expect(await screen.findByTestId('integrations-tab')).toBeInTheDocument(); + }); + + it('keeps it on a managed install too', async () => { + seedStore(useAddonStore, { isEnabled: () => false, loadAddons: vi.fn().mockResolvedValue(undefined) }); + seedStore(useAuthStore, { managed: true }); + render(, { initialEntries: ['/settings?tab=integrations'] }); + + expect(await screen.findByTestId('integrations-tab')).toBeInTheDocument(); + }); + }); }); diff --git a/client/src/pages/SettingsPage.tsx b/client/src/pages/SettingsPage.tsx index 464cf190d..397aafc60 100644 --- a/client/src/pages/SettingsPage.tsx +++ b/client/src/pages/SettingsPage.tsx @@ -26,7 +26,7 @@ export default function SettingsPage(): React.ReactElement { function SettingsPageDesktop(): React.ReactElement { const { t } = useTranslation() // Page = wiring container: addon/version loading + active-tab state in the hook. - const { hasIntegrations, hasPlugins, appVersion, activeTab, setActiveTab, managed } = useSettings() + const { hasPlugins, appVersion, activeTab, setActiveTab, managed } = useSettings() // Every tab is its own help screen; a tab without one falls back to the settings overview. const helpId = getHelpContext(`settings-${activeTab}`) ? `settings-${activeTab}` : 'settings' @@ -35,9 +35,9 @@ function SettingsPageDesktop(): React.ReactElement { { id: 'appearance', label: t('settings.tabs.appearance'), icon: Paintbrush }, { id: 'map', label: t('settings.tabs.map'), icon: Map }, { id: 'notifications', label: t('settings.tabs.notifications'), icon: Bell }, - ...(hasIntegrations - ? [{ id: 'integrations', label: t('settings.tabs.integrations'), icon: Plug }] - : []), + // Always there: API keys sit in this tab and need no addon. The addon backed + // sections inside it gate themselves. + { id: 'integrations', label: t('settings.tabs.integrations'), icon: Plug }, ...(hasPlugins ? [{ id: 'plugins', label: t('settings.tabs.plugins'), icon: Blocks }] : []), @@ -86,7 +86,7 @@ function SettingsPageDesktop(): React.ReactElement { {activeTab === 'appearance' && } {activeTab === 'map' && } {activeTab === 'notifications' && } - {activeTab === 'integrations' && hasIntegrations && } + {activeTab === 'integrations' && } {activeTab === 'plugins' && hasPlugins && } {activeTab === 'offline' && } {activeTab === 'account' && } diff --git a/wiki/Public-API.md b/wiki/Public-API.md index d9def12b5..55bd9dd9b 100644 --- a/wiki/Public-API.md +++ b/wiki/Public-API.md @@ -17,7 +17,7 @@ It cannot write anything. An integration that reads your itinerary needs no abil ## Getting a key -1. Open **Settings → Integrations** +1. Open **Settings → Integrations** in the desktop layout. The tab is always there and needs no addon: an API key works with MCP and every other integration switched off. The phone layout has no API Keys section yet, so create the key on a wider screen. 2. Under **API Keys**, click **Create key** and give it a name you will recognise later (the name is only for you — "Dawarich", "home assistant", "laptop script") 3. Choose **what the key may read.** Everything is selected by default. Anything you switch off is refused for that key. 4. **Copy the key immediately.** It is shown once. TREK stores only a hash of it, so it cannot be shown again — if you lose it, delete the key and make a new one. diff --git a/wiki/User-Settings.md b/wiki/User-Settings.md index 93d2ee23d..49c72eb2c 100644 --- a/wiki/User-Settings.md +++ b/wiki/User-Settings.md @@ -20,7 +20,7 @@ If your account requires MFA setup, TREK redirects you directly to the **Account | Appearance | Color mode, color scheme / accent, readability (transparency, reduce motion, density, text size), and which widgets appear on your dashboard | Always | | Map | Map provider (Leaflet, Mapbox GL, or MapLibre GL), tile presets, map style and Mapbox token, 3D buildings, high-quality mode | Always | | Notifications | Email, webhook, ntfy, push and in-app notification preferences, plus switching push on or off for this device (every device is switched on separately) | Always | -| Integrations | Photo providers (Immich, Synology, etc.), the AirTrail connection, the Dawarich connection, your own AI parsing model, public API keys, and MCP OAuth clients / API tokens | Only when a photo provider (Immich or Synology Photos), MCP, AirTrail, AI Parsing or Dawarich is enabled | +| Integrations | Photo providers (Immich, Synology, etc.), the AirTrail connection, the Dawarich connection, your own AI parsing model, public API keys, and MCP OAuth clients / API tokens | Always on desktop, because API keys need no addon. In the phone layout only when a photo provider (Immich or Synology Photos), MCP, AirTrail, AI Parsing or Dawarich is enabled | | Plugins | Per-user settings for installed plugins | Only when at least one plugin is installed | | Offline | Cached trips, pending changes, re-sync and clear cache | Always | | Account | Username, email, password, MFA (TOTP + backup codes), passkeys, avatar, delete account | Always | @@ -100,13 +100,13 @@ If your account was linked via SSO, an **SSO** badge appears next to your role a ## Integrations tab -The Integrations tab is only visible when a **photo provider** (Immich or Synology Photos), **MCP**, **AirTrail**, **AI Parsing** or **Dawarich** is enabled. Each section then appears with its own addon, so the tab holds only what your instance actually runs: +On desktop the Integrations tab is always there, because **API Keys** need no addon. Every other section appears with its own addon, so the tab holds only what your instance actually runs. The phone layout has no API Keys section and shows the tab only when a **photo provider** (Immich or Synology Photos), **MCP**, **AirTrail**, **AI Parsing** or **Dawarich** is enabled: - **Photo Providers** (only when a photo provider is enabled) — Configure Immich, Synology Photos, and other photo integrations. One card appears per enabled provider. - **AirTrail** (only when the AirTrail addon is enabled) — Instance URL and API key for your self-hosted AirTrail, plus a self-signed-certificate switch and a **Write changes back to AirTrail** toggle (off by default: AirTrail is the source of truth and TREK only reads from it). **Test connection** reports how many flights it can see. The key is stored encrypted and never prefilled — leaving the field blank keeps the stored one. - **Dawarich** (only when the Dawarich addon is enabled): the **Instance address** and **API key** of your own Dawarich, a self-signed-certificate switch, **Test connection** and **Disconnect**. The key is stored encrypted and never handed back. See [Dawarich](Dawarich). - **AI parsing** (only when the AI Parsing addon is enabled): the provider (OpenAI or Anthropic), model and API key used to extract bookings from your uploaded files, plus **Model reads images**. Turn that on for a model that reads images: a photo of a ticket can then be imported as a booking, and a receipt scanned in Costs. This is the per-user fallback: it only takes effect when your administrator has not configured a model for the whole instance. A local Ollama endpoint is not offered here; that is set up once, instance-wide, in the admin settings (#1772). See [AI-Booking-Import](AI-Booking-Import#photos) and [Budget-Tracking](Budget-Tracking#scanning-a-receipt). -- **API Keys** (whenever the tab is shown): read-only keys for the public API, so other software can read your trips. **Create key** names the key and lets you narrow **what this key may read** to single areas; the key is shown once. See [Public-API](Public-API). +- **API Keys** (always, desktop layout only): read-only keys for the public API, so other software can read your trips. **Create key** names the key and lets you narrow **what this key may read** to single areas; the key is shown once. See [Public-API](Public-API). - **MCP section** (only when MCP addon is enabled): - Shows the MCP server endpoint URL. - **OAuth Clients** sub-tab — Create and manage OAuth 2.1 clients (with redirect URIs and scopes). Quick-fill presets are provided for Claude.ai, Claude Desktop, Cursor, VS Code, Windsurf, and Zed. Active OAuth sessions can be viewed and revoked here.