---
phase: 05-cloud-storage-backends
plan: 07
type: execute
wave: 6
depends_on:
  - "05-06"
files_modified:
  - frontend/src/stores/cloudConnections.js
  - frontend/src/stores/__tests__/cloudConnections.test.js
  - frontend/src/api/client.js
  - frontend/src/views/SettingsView.vue
  - frontend/src/components/settings/SettingsPreferencesTab.vue
  - frontend/src/components/settings/SettingsAiTab.vue
  - frontend/src/components/settings/SettingsCloudTab.vue
  - frontend/src/components/cloud/CloudCredentialModal.vue
autonomous: true
requirements:
  - CLOUD-01
  - CLOUD-03
  - CLOUD-04
  - CLOUD-05
  - CLOUD-06

must_haves:
  truths:
    - "SettingsView has a 3-tab layout (Preferences, AI Configuration, Cloud Storage)"
    - "Cloud Storage tab shows all 4 providers with status badges (ACTIVE, REQUIRES_REAUTH, ERROR, not_connected)"
    - "Connect Google Drive / OneDrive triggers a redirect to the OAuth initiation endpoint"
    - "Connect Nextcloud / WebDAV opens CloudCredentialModal with server URL, username, and auth method toggle"
    - "Remove {provider} button disconnects the connection via DELETE /api/cloud/connections/{id}"
    - "OAuth redirect success/error handled in onMounted via ?cloud_connected= and ?cloud_error= query params"
    - "Success toast auto-dismisses in 5 seconds; error banner persists until dismissed"
    - "cloudConnectionsStore: connections, loading, error state; fetchConnections, disconnect, disconnectAll actions"
    - "Vitest unit tests for cloudConnections store (4 tests) and SettingsCloudTab mount test (2 tests) — per CLAUDE.md testing protocol (W4)"
  artifacts:
    - path: "frontend/src/stores/cloudConnections.js"
      provides: "Pinia store for cloud connections state"
      contains: "useCloudConnectionsStore"
    - path: "frontend/src/stores/__tests__/cloudConnections.test.js"
      provides: "Vitest unit tests for cloudConnections store (W4)"
      contains: "fetchConnections"
    - path: "frontend/src/api/client.js"
      provides: "Cloud API client functions"
      contains: "listCloudConnections"
    - path: "frontend/src/views/SettingsView.vue"
      provides: "3-tab settings view with OAuth callback handling"
      contains: "activeTab"
    - path: "frontend/src/components/settings/SettingsCloudTab.vue"
      provides: "Cloud provider card list with status badges and action buttons"
      contains: "CloudCredentialModal"
    - path: "frontend/src/components/cloud/CloudCredentialModal.vue"
      provides: "WebDAV/Nextcloud credential input modal"
      contains: "authMethod"
  key_links:
    - from: "frontend/src/components/settings/SettingsCloudTab.vue"
      to: "frontend/src/stores/cloudConnections.js"
      via: "useCloudConnectionsStore()"
      pattern: "useCloudConnectionsStore"
    - from: "frontend/src/views/SettingsView.vue"
      to: "frontend/src/stores/cloudConnections.js"
      via: "fetchConnections on tab switch to cloud"
      pattern: "fetchConnections"
---

<objective>
Build the frontend cloud storage UI: Pinia store, API client functions, SettingsView 3-tab conversion, SettingsCloudTab provider cards, and CloudCredentialModal.

Purpose: Complete the user-facing cloud storage management experience — connect, view status, and disconnect providers from SettingsView.
Output: cloudConnections.js store, API client additions, SettingsView tab conversion, 3 settings tab components, CloudCredentialModal.
</objective>

<execution_context>
@/Users/nik/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nik/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/05-cloud-storage-backends/05-CONTEXT.md
@.planning/phases/05-cloud-storage-backends/05-UI-SPEC.md
@.planning/phases/05-cloud-storage-backends/05-06-SUMMARY.md
</context>

<interfaces>
<!-- From frontend/src/api/client.js — existing API pattern -->
From frontend/src/api/client.js:
  async function request(path, options = {}) — handles auth headers, 401 retry
  All API functions call request(path, options) and return the JSON payload

<!-- From frontend/src/stores/folders.js — Pinia store pattern -->
defineStore('folders', () => {
  const folders = ref([])
  const loading = ref(false)
  const error = ref(null)
  async function fetchFolders() { loading.value=true; ... }
  return { folders, loading, error, fetchFolders, ... }
})

<!-- From frontend/src/views/SettingsView.vue — current content to preserve -->
Current SettingsView: flat layout with AI config section + Document Preferences section (pdf_open_mode radios)
After conversion: 3-tab layout; "preferences" tab has pdf_open_mode; "ai" tab has AI config text; "cloud" tab is new

<!-- From 05-UI-SPEC.md — exact component specs -->
Tab strip: copy AdminView pattern verbatim (px-4 py-2 text-sm font-semibold border-b-2)
Provider rows: divide-y divide-gray-100 inside bg-white border border-gray-200 rounded-xl p-6
Status badge: bg-green-100 text-green-700 (ACTIVE), bg-yellow-100 text-yellow-800 (REQUIRES_REAUTH),
  bg-red-100 text-red-700 (ERROR), bg-gray-100 text-gray-600 (not_connected)
Action buttons per status: per UI-SPEC table
OAuth success toast: fixed top-4 right-4 z-50; auto-dismiss 5000ms
Error banner: mb-6 inline inside cloud tab content; persistent

<!-- From 05-UI-SPEC.md — CloudCredentialModal -->
Overlay: fixed inset-0 bg-gray-900 bg-opacity-40 z-40 flex items-center justify-center p-4
Panel: bg-white rounded-xl shadow-xl w-full max-w-md p-6
Fields: Server URL, Username, auth method radio (app_password default), Password/App password
Cancel label: "Keep current settings"
Save button label: "Connect {providerLabel}"
</interfaces>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create cloudConnections Pinia store, API client additions, and Vitest tests</name>
  <files>frontend/src/stores/cloudConnections.js, frontend/src/stores/__tests__/cloudConnections.test.js, frontend/src/api/client.js</files>
  <read_first>
    - frontend/src/stores/folders.js — Pinia store structure (defineStore composition API pattern)
    - frontend/src/api/client.js — existing API function patterns, request() helper
    - frontend/src/stores/auth.js — how stores handle loading/error state
    - .planning/phases/05-cloud-storage-backends/05-UI-SPEC.md — cloudConnections store state contract
  </read_first>
  <behavior>
    - frontend/src/stores/cloudConnections.js exports useCloudConnectionsStore with: connections (ref []), loading (ref false), error (ref null); fetchConnections(), disconnect(id), disconnectAll() actions
    - fetchConnections calls GET /api/cloud/connections; sets connections from response.items
    - disconnect(id) calls DELETE /api/cloud/connections/{id}; removes connection from connections array
    - disconnectAll() calls disconnect(id) for each connection serially; clears connections on completion
    - frontend/src/api/client.js gains: listCloudConnections(), disconnectCloud(id), connectWebDav(provider, serverUrl, username, password), updateDefaultStorage(backend)
    - API functions follow existing pattern: return request(...) or request(...).then(r => r)
    - GET /api/cloud/oauth/initiate/{provider} is a redirect — frontend navigates via window.location.href (not a fetch call); no API client function needed for OAuth initiation
  </behavior>
  <action>
    Create frontend/src/stores/cloudConnections.js following the folders.js defineStore composition pattern:
      import { defineStore } from 'pinia'
      import { ref } from 'vue'
      import * as api from '../api/client.js'

      export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
        const connections = ref([])
        const loading = ref(false)
        const error = ref(null)

        async function fetchConnections() {
          loading.value = true; error.value = null
          try {
            const data = await api.listCloudConnections()
            connections.value = data.items ?? []
          } catch (e) { error.value = e.message || 'Failed to load cloud connections' }
          finally { loading.value = false }
        }

        async function disconnect(id) {
          try {
            await api.disconnectCloud(id)
            connections.value = connections.value.filter(c => c.id !== id)
          } catch (e) { throw e }
        }

        async function disconnectAll() {
          const ids = connections.value.map(c => c.id)
          for (const id of ids) await disconnect(id)
          connections.value = []
        }

        return { connections, loading, error, fetchConnections, disconnect, disconnectAll }
      })

    Append to frontend/src/api/client.js (add after the existing adminListAuditLog function):

      // ── Cloud Storage ─────────────────────────────────────────────────────────
      export function listCloudConnections() {
        return request('/api/cloud/connections')
      }
      export function disconnectCloud(id) {
        return request(`/api/cloud/connections/${id}`, { method: 'DELETE' })
      }
      export function connectWebDav(provider, serverUrl, username, password) {
        return request('/api/cloud/connections/webdav', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ provider, server_url: serverUrl, username, password }),
        })
      }
      export function updateDefaultStorage(backend) {
        return request('/api/users/me/default-storage', {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ backend }),
        })
      }

    Create frontend/src/stores/__tests__/cloudConnections.test.js (W4 — Vitest unit tests per CLAUDE.md):
    Tests must mock api/client.js functions (no real HTTP calls).

    import { setActivePinia, createPinia } from 'pinia'
    import { describe, it, expect, vi, beforeEach } from 'vitest'
    import { useCloudConnectionsStore } from '../cloudConnections.js'
    import * as api from '../../api/client.js'

    beforeEach(() => { setActivePinia(createPinia()) })

    describe('useCloudConnectionsStore', () => {
      it('fetchConnections sets connections from API response', async () => {
        vi.spyOn(api, 'listCloudConnections').mockResolvedValue({ items: [{id:'1',provider:'google_drive',status:'ACTIVE'}] })
        const store = useCloudConnectionsStore()
        await store.fetchConnections()
        expect(store.connections).toHaveLength(1)
        expect(store.connections[0].provider).toBe('google_drive')
        expect(store.loading).toBe(false)
      })

      it('fetchConnections sets error on API failure', async () => {
        vi.spyOn(api, 'listCloudConnections').mockRejectedValue(new Error('Network error'))
        const store = useCloudConnectionsStore()
        await store.fetchConnections()
        expect(store.error).toBeTruthy()
        expect(store.connections).toHaveLength(0)
      })

      it('disconnect removes connection from state after API call', async () => {
        vi.spyOn(api, 'disconnectCloud').mockResolvedValue(undefined)
        const store = useCloudConnectionsStore()
        store.connections = [{ id: 'conn-1', provider: 'google_drive', status: 'ACTIVE' }]
        await store.disconnect('conn-1')
        expect(store.connections).toHaveLength(0)
        expect(api.disconnectCloud).toHaveBeenCalledWith('conn-1')
      })

      it('disconnectAll clears all connections', async () => {
        vi.spyOn(api, 'disconnectCloud').mockResolvedValue(undefined)
        const store = useCloudConnectionsStore()
        store.connections = [
          { id: 'a', provider: 'google_drive', status: 'ACTIVE' },
          { id: 'b', provider: 'onedrive', status: 'ACTIVE' },
        ]
        await store.disconnectAll()
        expect(store.connections).toHaveLength(0)
      })
    })
  </action>
  <verify>
    <automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e "
const fs = require('fs');
const store = fs.readFileSync('src/stores/cloudConnections.js', 'utf8');
['useCloudConnectionsStore','fetchConnections','disconnect','disconnectAll','connections','loading','error'].forEach(name => {
  if (!store.includes(name)) throw new Error('Missing: ' + name);
  console.log('OK: ' + name);
});
const api = fs.readFileSync('src/api/client.js', 'utf8');
['listCloudConnections','disconnectCloud','connectWebDav','updateDefaultStorage'].forEach(name => {
  if (!api.includes(name)) throw new Error('Missing from api/client.js: ' + name);
  console.log('OK api: ' + name);
});
if (!fs.existsSync('src/stores/__tests__/cloudConnections.test.js')) throw new Error('Missing Vitest file');
console.log('OK: Vitest test file exists');
" && npm run test -- src/stores/__tests__/cloudConnections.test.js 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - frontend/src/stores/cloudConnections.js exists with useCloudConnectionsStore
    - Store exports: connections (ref), loading (ref), error (ref), fetchConnections, disconnect, disconnectAll
    - frontend/src/api/client.js contains listCloudConnections, disconnectCloud, connectWebDav, updateDefaultStorage
    - frontend/src/stores/__tests__/cloudConnections.test.js exists with 4 Vitest tests (W4 — CLAUDE.md requirement)
    - All 4 Vitest tests pass: fetchConnections, fetchConnections error path, disconnect, disconnectAll
    - No modifications to existing API functions (folders, auth, etc.)
  </acceptance_criteria>
  <done>cloudConnections.js store created; 4 new API functions appended to client.js; 4 Vitest unit tests passing; existing API functions untouched</done>
</task>

<task type="auto">
  <name>Task 2: Convert SettingsView to 3-tab layout and create all settings + cloud components</name>
  <files>
    frontend/src/views/SettingsView.vue,
    frontend/src/components/settings/SettingsPreferencesTab.vue,
    frontend/src/components/settings/SettingsAiTab.vue,
    frontend/src/components/settings/SettingsCloudTab.vue,
    frontend/src/components/settings/__tests__/SettingsCloudTab.test.js,
    frontend/src/components/cloud/CloudCredentialModal.vue
  </files>
  <read_first>
    - frontend/src/views/SettingsView.vue — current content (preferences + AI sections) to preserve
    - frontend/src/views/AdminView.vue — tab strip pattern to copy verbatim
    - .planning/phases/05-cloud-storage-backends/05-UI-SPEC.md — all Surface 1-4 specs (tab structure, provider rows, status badges, action buttons, modal)
    - frontend/src/stores/cloudConnections.js — useCloudConnectionsStore (Task 1)
    - frontend/src/api/client.js — connectWebDav function
    - frontend/src/components/ui/ — check for ConfirmBlock component for disconnect confirmation
  </read_first>
  <behavior>
    SettingsView.vue:
    - Converts to 3-tab layout: tabs = [{id:'preferences', label:'Preferences'}, {id:'ai', label:'AI Configuration'}, {id:'cloud', label:'Cloud Storage'}]
    - activeTab defaults to 'preferences'
    - onMounted: reads window.location.search for ?cloud_connected={provider} and ?cloud_error={message}; if found: sets activeTab='cloud'; clears query params via router.replace({path:'/settings'})
    - oauthSuccessProvider ref: null; auto-clears after 5000ms via setTimeout
    - oauthError ref: null; dismissed via X button
    - Renders SettingsPreferencesTab, SettingsAiTab, SettingsCloudTab as tab content
    - OAuth success toast: fixed top-4 right-4 z-50 (per UI-SPEC Surface 3 exact markup)
    - Error banner: inline above section card when oauthError is set (per UI-SPEC Surface 3 exact markup)

    SettingsPreferencesTab.vue:
    - Extracted from current SettingsView: the pdf_open_mode radio section
    - Maintains existing pdfOpenMode ref, watch, onMounted behavior
    - Template: bg-white border border-gray-200 rounded-xl p-6 wrapper; same radios, save feedback text

    SettingsAiTab.vue:
    - Extracted from current SettingsView: the "AI configuration" section
    - Template: bg-white border border-gray-200 rounded-xl p-6; same copy ("AI provider and model are managed by your administrator.")

    SettingsCloudTab.vue:
    - Imports useCloudConnectionsStore; calls fetchConnections() in onMounted
    - PROVIDERS constant: [{key:'google_drive', label:'Google Drive', iconColor:'text-blue-500'}, {key:'onedrive', label:'OneDrive', iconColor:'text-sky-500'}, {key:'nextcloud', label:'Nextcloud', iconColor:'text-orange-500'}, {key:'webdav', label:'WebDAV server', iconColor:'text-gray-500'}]
    - For each provider: renders row with icon + provider name + StatusBadge + action buttons
    - Status badge: inline pill span with classes per UI-SPEC status badge table
    - Action buttons per status: exact labels from UI-SPEC Copywriting Contract
    - "Connect {provider}" for OAuth providers: window.location.href = `/api/cloud/oauth/initiate/${provider.key}`
    - "Connect {provider}" for WebDAV/Nextcloud: opens CloudCredentialModal with showModal=true, activePro=provider
    - "Remove {provider}": calls store.disconnect(connection.id) with inline ConfirmBlock confirm pattern
    - "Reconnect {provider}": same as "Connect {provider}"
    - REQUIRES_REAUTH inline banner: per UI-SPEC Surface 2 exact markup (bg-yellow-50 border border-yellow-200)
    - "Disconnect all cloud storage" link at bottom: only when any connection ACTIVE or ERROR
    - Disconnect all ConfirmBlock: message, confirm label "Disconnect all", cancel "Keep all connected"

    CloudCredentialModal.vue:
    - Props: show (Boolean), provider (Object: {key, label}) 
    - Emits: close, connected
    - Fields: serverUrl, username, authMethod (ref 'app_password'), password
    - On submit: calls api.connectWebDav(provider.key, serverUrl, username, password); emits 'connected'; closes
    - On error: shows connectError message (per UI-SPEC Surface 4)
    - Cancel label: "Keep current settings"
    - Save label: "Connect {provider.label}"
    - Escape key and overlay click close the modal (unless saving=true)
    - All Tailwind classes and layout per UI-SPEC Surface 4 exact specifications
  </behavior>
  <action>
    Create directories: frontend/src/components/settings/ and frontend/src/components/cloud/ if they don't exist.

    1. Create frontend/src/components/settings/SettingsPreferencesTab.vue:
       Extract the pdf_open_mode section from current SettingsView.vue.
       Keep the `<script setup>` with pdfOpenMode ref, watch, onMounted, api imports.
       Wrap template in a section div with same bg-white border classes.

    2. Create frontend/src/components/settings/SettingsAiTab.vue:
       Extract the AI configuration section from current SettingsView.vue.
       Static content (no script logic needed).

    3. Create frontend/src/components/settings/SettingsCloudTab.vue:
       Full provider list component per UI-SPEC Surface 2 specification.
       Use useCloudConnectionsStore for connections data.
       PROVIDERS array defined as a local constant.
       statusBadgeClasses(status) computed helper mapping status to Tailwind classes.
       connectionFor(providerKey) computed returning the matching connection or null.
       All action button logic per behavior spec above.

    4. Create frontend/src/components/cloud/CloudCredentialModal.vue:
       Full modal per UI-SPEC Surface 4 specification.
       Teleport to body or fixed positioning.
       @keydown.escape.window handler to close modal.
       Overlay click handler to close (when not saving).

    5. Rewrite frontend/src/views/SettingsView.vue:
       New 3-tab layout. Import and render the 3 tab components.
       Read AdminView.vue tab strip implementation and copy the pattern verbatim.
       Add oauthSuccessProvider and oauthError state + toast/banner markup per UI-SPEC Surface 3.
       Preserve: p-8 max-w-3xl mx-auto wrapper, h2 heading, description paragraph.

    Check existing components: look for ConfirmBlock in frontend/src/components/ui/ — if present, use it for disconnect confirmation dialogs. If not present, implement inline confirmation pattern.

    6. Create frontend/src/components/settings/__tests__/SettingsCloudTab.test.js (W4 — CLAUDE.md requires tests for new components):
       import { mount } from '@vue/test-utils'
       import { createTestingPinia } from '@pinia/testing'
       import { describe, it, expect } from 'vitest'
       import SettingsCloudTab from '../SettingsCloudTab.vue'

       describe('SettingsCloudTab', () => {
         it('renders all 4 provider rows', () => {
           const wrapper = mount(SettingsCloudTab, {
             global: {
               plugins: [createTestingPinia({ createSpy: vi.fn })],
             },
           })
           expect(wrapper.text()).toContain('Google Drive')
           expect(wrapper.text()).toContain('OneDrive')
           expect(wrapper.text()).toContain('Nextcloud')
           expect(wrapper.text()).toContain('WebDAV')
         })

         it('shows "Not connected" state when no connections active', () => {
           const wrapper = mount(SettingsCloudTab, {
             global: {
               plugins: [createTestingPinia({
                 createSpy: vi.fn,
                 initialState: { cloudConnections: { connections: [], loading: false, error: null } },
               })],
             },
           })
           // All providers have Connect buttons when no connections exist
           const connectButtons = wrapper.findAll('button')
           expect(connectButtons.length).toBeGreaterThan(0)
         })
       })
  </action>
  <verify>
    <automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e "
const fs = require('fs');
const files = [
  'src/views/SettingsView.vue',
  'src/components/settings/SettingsPreferencesTab.vue',
  'src/components/settings/SettingsAiTab.vue',
  'src/components/settings/SettingsCloudTab.vue',
  'src/components/settings/__tests__/SettingsCloudTab.test.js',
  'src/components/cloud/CloudCredentialModal.vue',
];
files.forEach(f => {
  if (!fs.existsSync(f)) throw new Error('Missing: ' + f);
  const content = fs.readFileSync(f, 'utf8');
  console.log('EXISTS OK: ' + f + ' (' + content.length + ' chars)');
});
const settings = fs.readFileSync('src/views/SettingsView.vue', 'utf8');
if (!settings.includes('activeTab')) throw new Error('SettingsView missing activeTab');
if (!settings.includes('SettingsPreferencesTab')) throw new Error('SettingsView missing tab component');
if (!settings.includes('SettingsCloudTab')) throw new Error('SettingsView missing CloudTab');
console.log('SettingsView tab conversion: OK');
const cloud = fs.readFileSync('src/components/settings/SettingsCloudTab.vue', 'utf8');
if (!cloud.includes('google_drive')) throw new Error('SettingsCloudTab missing google_drive provider');
if (!cloud.includes('CloudCredentialModal')) throw new Error('SettingsCloudTab missing CloudCredentialModal');
console.log('SettingsCloudTab providers and modal: OK');
" && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run test -- src/stores/__tests__/cloudConnections.test.js src/components/settings/__tests__/SettingsCloudTab.test.js 2>&1 | tail -10 && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - All 5 new/modified files exist
    - SettingsView.vue contains activeTab ref, SettingsPreferencesTab, SettingsAiTab, SettingsCloudTab imports and rendering
    - SettingsView.vue contains oauthSuccessProvider ref and success toast markup (fixed top-4 right-4)
    - SettingsView.vue contains oauthError ref and error banner markup
    - SettingsCloudTab.vue contains all 4 provider keys: google_drive, onedrive, nextcloud, webdav
    - SettingsCloudTab.vue uses useCloudConnectionsStore
    - CloudCredentialModal.vue contains authMethod ref and auth method radio group
    - `npm run build` (Vite build) exits 0 without errors
    - SettingsCloudTab.test.js exists with at least one mount test confirming all 4 providers render (W4 — CLAUDE.md requirement for new components)
  </acceptance_criteria>
  <done>5 files created/modified; 3-tab SettingsView with OAuth handling; SettingsCloudTab with 4 providers; CloudCredentialModal; SettingsCloudTab mount test; Vite build passes</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| browser → /api/cloud/oauth/initiate | window.location.href redirect — OAuth tokens never touch JavaScript |
| ?cloud_error= query param → display | URL-decoded error message displayed to user; must not execute as HTML |
| WebDAV credentials → POST /api/cloud/connections/webdav | Credentials sent over HTTPS only; Vue template auto-escaping prevents XSS |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-07-01 | Information Disclosure | OAuth tokens in browser JavaScript | mitigate | OAuth initiation uses window.location.href redirect — FastAPI handles code exchange; tokens never land in frontend (D-03) |
| T-05-07-02 | XSS | ?cloud_error= decoded and displayed | mitigate | Vue template auto-escaping ({{ oauthError }}) prevents HTML injection; no v-html used |
| T-05-07-03 | Information Disclosure | WebDAV password in component state | accept | Password lives in ref() only during modal interaction; cleared on close/submit; never persisted in localStorage |
| T-05-07-04 | Information Disclosure | connection.credentials_enc in store | mitigate | CloudConnectionOut from API never includes credentials_enc; store.connections holds only safe fields |
</threat_model>

<verification>
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5
</verification>

<success_criteria>
- cloudConnections.js store: connections, loading, error, fetchConnections, disconnect, disconnectAll
- client.js: listCloudConnections, disconnectCloud, connectWebDav, updateDefaultStorage added
- SettingsView.vue: 3-tab layout; OAuth success/error handling; tab strip matches AdminView pattern
- SettingsCloudTab.vue: all 4 providers; status badges; action buttons per status; REQUIRES_REAUTH banner; disconnect all
- CloudCredentialModal.vue: server URL + username + auth method toggle + password; correct cancel/save labels
- Vitest: cloudConnections.test.js (4 tests passing) and SettingsCloudTab.test.js (2 tests passing)
- Vite build exits 0
</success_criteria>

<output>
Create `.planning/phases/05-cloud-storage-backends/05-07-SUMMARY.md` when done
</output>
