---
phase: 05-cloud-storage-backends
plan: 07
type: execute
wave: 6
depends_on:
- "05-06"
files_modified:
- frontend/src/stores/cloudConnections.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"
artifacts:
- path: "frontend/src/stores/cloudConnections.js"
provides: "Pinia store for cloud connections state"
contains: "useCloudConnectionsStore"
- 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"
---
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.
@/Users/nik/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nik/.claude/get-shit-done/templates/summary.md
@.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
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
defineStore('folders', () => {
const folders = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchFolders() { loading.value=true; ... }
return { folders, loading, error, fetchFolders, ... }
})
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
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
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}"
Task 1: Create cloudConnections Pinia store and API client additions
frontend/src/stores/cloudConnections.js, frontend/src/api/client.js
- 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
- 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
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 }),
})
}
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);
});
"
- 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
- No modifications to existing API functions (folders, auth, etc.)
cloudConnections.js store created; 4 new API functions appended to client.js; existing API functions untouched
Task 2: Convert SettingsView to 3-tab layout and create all settings + cloud components
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
- 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
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
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 `