---
phase: 03-document-migration-multi-user-isolation
plan: 04
type: execute
wave: 4
depends_on:
  - 03-03
files_modified:
  - backend/config.py
  - backend/services/classifier.py
  - backend/services/storage.py
  - backend/tasks/document_tasks.py
  - backend/api/settings.py
  - backend/main.py
  - backend/tests/test_settings.py
  - frontend/src/views/SettingsView.vue
  - frontend/src/stores/settings.js
  - frontend/src/api/client.js
autonomous: true
requirements:
  - DOC-03
  - DOC-05

must_haves:
  truths:
    - "AI provider and model are resolved per document from users.ai_provider / users.ai_model via the Celery task (no flat-file read)"
    - "Falls back to settings.default_ai_provider / settings.default_ai_model env-var defaults when user.ai_provider is None"
    - "/api/settings endpoint no longer exists; backend/main.py does not register api.settings router"
    - "services.storage no longer exposes load_settings/save_settings/mask_api_key/settings_masked; settings.json is no longer read or written"
    - "classifier.classify_document accepts ai_provider and ai_model parameters; no longer reads global config from disk"
    - "SettingsView.vue displays an admin-managed message; the form is removed; stores/settings.js and api/client.js settings calls are removed"
  artifacts:
    - path: "backend/config.py"
      provides: "SYSTEM_PROMPT + DEFAULT_AI_PROVIDER + DEFAULT_AI_MODEL settings; SETTINGS_FILE / DEFAULT_SETTINGS / DEFAULT_SYSTEM_PROMPT removed"
      contains: "default_ai_provider"
    - path: "backend/services/classifier.py"
      provides: "classify_document accepts ai_provider, ai_model kwargs; _DEFAULT_SYSTEM_PROMPT module constant"
      contains: "ai_provider"
    - path: "backend/services/storage.py"
      provides: "load_settings/save_settings/mask_api_key/settings_masked removed"
      contains: "load_topics_for_user"
    - path: "backend/tasks/document_tasks.py"
      provides: "_run resolves user.ai_provider via session.get(User, doc.user_id) and passes to classifier"
      contains: "user.ai_provider"
    - path: "backend/main.py"
      provides: "api.settings router import + include_router removed"
      contains: "settings_router"
    - path: "backend/api/settings.py"
      provides: "File deleted"
    - path: "frontend/src/views/SettingsView.vue"
      provides: "Admin-managed placeholder card; form/store/api removed"
      contains: "managed by"
    - path: "frontend/src/stores/settings.js"
      provides: "File deleted or gutted to empty placeholder"
    - path: "frontend/src/api/client.js"
      provides: "getSettings/patchSettings/testProvider/getDefaultPrompt removed"
      contains: "getMyQuota"
  key_links:
    - from: "backend/tasks/document_tasks.py"
      to: "backend/db/models.py User"
      via: "user = await session.get(User, doc.user_id); ai_provider = user.ai_provider or settings.default_ai_provider"
      pattern: "user\\.ai_provider"
    - from: "backend/services/classifier.py"
      to: "backend/ai/__init__.py get_provider"
      via: "_settings = {'active_provider': ai_provider, 'providers': {ai_provider: {'model': ai_model}}}; provider = get_provider(_settings)"
      pattern: "active_provider.*ai_provider"
---

<objective>
Retire the flat-file settings system end-to-end (D-12, D-13) and wire per-user AI classification via DB lookup (D-14, D-15, DOC-03, DOC-05). Move system prompt + defaults to env vars in config.py. Delete /api/settings; refactor classifier to receive ai_provider/ai_model as parameters; update Celery task to look up doc.user_id → user.ai_provider/ai_model and pass through to classifier. Replace the frontend Settings view with an admin-managed placeholder; remove the settings store and client calls.

Purpose: Phase 3 SC5 (each user's AI classification uses the provider/model assigned to that user by the admin) cannot pass while load_settings() exists. This is the final backend plan for Phase 3.
Output: 10 file modifications (3 backend code, 1 backend route removal, 1 backend test update, 1 backend main.py, 3 frontend code, 1 backend config). After this plan, the entire Phase 3 backend is multi-user-isolated and admin-controlled.
</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/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-document-migration-multi-user-isolation/03-CONTEXT.md
@.planning/phases/03-document-migration-multi-user-isolation/03-RESEARCH.md
@.planning/phases/03-document-migration-multi-user-isolation/03-PATTERNS.md
@.planning/phases/03-document-migration-multi-user-isolation/03-VALIDATION.md
@.planning/phases/03-document-migration-multi-user-isolation/03-03-SUMMARY.md
@CLAUDE.md

@backend/config.py
@backend/services/classifier.py
@backend/services/storage.py
@backend/tasks/document_tasks.py
@backend/api/settings.py
@backend/main.py
@backend/ai/__init__.py
@backend/tests/test_settings.py
@frontend/src/views/SettingsView.vue
@frontend/src/stores/settings.js
@frontend/src/api/client.js

<interfaces>
<!-- Contracts the executor needs without re-reading the codebase. -->

From backend/db/models.py:
  User.ai_provider: Mapped[Optional[str]]   # nullable text — set by admin via PATCH /api/admin/users/{id}/ai-config
  User.ai_model: Mapped[Optional[str]]      # nullable text

From backend/ai/__init__.py (current — RESEARCH.md Finding 12 / A4):
  def get_provider(settings: dict) -> AIProvider
  # Accepts dict with keys: "active_provider": str, "providers": {<name>: {"model": str, "api_key"?: str, "base_url"?: str}}
  # Branches on active_provider for anthropic / openai / ollama / lmstudio

From backend/tasks/document_tasks.py (current):
  async def _run(document_id: str) -> dict
    — fetches Document by id
    — calls `topics = await classifier.classify_document(session, document_id)`  # no ai_provider/ai_model yet

From backend/services/classifier.py (post Plan 03-03):
  async def classify_document(session, doc_id, topic_names=None) -> list[str]
    — currently calls `settings = storage.load_settings(); provider = get_provider(settings)`
    — must change to `provider = get_provider({"active_provider": ai_provider, "providers": {ai_provider: {"model": ai_model}}})`

From backend/config.py (current):
  SETTINGS_FILE = Path(settings.data_dir) / "settings.json"   # to be removed
  DEFAULT_SYSTEM_PROMPT = "..."                                # to be removed (kept as module const in classifier.py)
  DEFAULT_SETTINGS = {...}                                     # to be removed
  class Settings(BaseSettings): ...                            # extend with new fields
</interfaces>
</context>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| user → /api/settings | Endpoint removed; previous attack surface eliminated |
| Celery task → users table | Task resolves AI config via doc.user_id → users row; no user-controlled provider/model input |
| admin → PATCH /api/admin/users/{id}/ai-config | Existing Phase 2 admin endpoint is the only write path to user.ai_provider / user.ai_model |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-17 | Elevation of Privilege | User changing their own AI provider | mitigate | /api/settings is removed (D-12); only PATCH /api/admin/users/{id}/ai-config (admin-only) writes user.ai_provider/ai_model |
| T-03-18 | Information Disclosure | settings.json flat file persisted to disk with API keys | mitigate | services.storage.load_settings/save_settings deleted; settings.json no longer read or written; API keys live in env vars only |
| T-03-19 | Tampering | Celery task accepting ai_provider in task signature | mitigate | Task signature unchanged (just document_id); ai_provider/ai_model resolved INSIDE the task via DB lookup so a malicious broker message cannot inject provider |
| T-03-20 | Information Disclosure | system_prompt env var in container logs | accept | SYSTEM_PROMPT is a static instruction string with no PII; documented as low-sensitivity |
| T-03-21 | Repudiation | Frontend SettingsView still attempts old API calls | mitigate | Remove getSettings/patchSettings/testProvider/getDefaultPrompt from api/client.js; gut stores/settings.js; SettingsView shows static message — no API calls |
| T-03-SC | Tampering | pip installs | mitigate | No new package installs |
</threat_model>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Backend — retire settings flat-file; route classifier through per-user AI config</name>
  <files>backend/config.py, backend/services/classifier.py, backend/services/storage.py, backend/tasks/document_tasks.py, backend/api/settings.py, backend/main.py, backend/tests/test_settings.py</files>
  <read_first>
    - backend/config.py — Settings class fields, SETTINGS_FILE definition, DEFAULT_SYSTEM_PROMPT string, DEFAULT_SETTINGS dict
    - backend/services/storage.py — load_settings / save_settings / mask_api_key / settings_masked function bodies and __all__ entries (lines ~411-473)
    - backend/services/classifier.py — current settings load + get_provider call sites (lines 33-35 and 67-69)
    - backend/tasks/document_tasks.py — _run async body, particularly the classifier.classify_document call (line ~80)
    - backend/api/settings.py — the entire file (to be deleted)
    - backend/main.py — settings_router import + include_router call site
    - backend/ai/__init__.py — get_provider factory signature (RESEARCH.md A4 — verify factory accepts {"active_provider", "providers"} dict)
    - backend/tests/test_settings.py — existing test file with active tests (to be reduced to test_settings_endpoint_removed)
    - .planning/phases/03-document-migration-multi-user-isolation/03-RESEARCH.md — Finding 8 (Celery refactor), Finding 11 (retirement scope), Finding 12 (get_provider factory)
    - .planning/phases/03-document-migration-multi-user-isolation/03-PATTERNS.md — classifier signature (lines ~452-510), config.py additions (lines ~743-752)
  </read_first>
  <behavior>
    - config.py removes `SETTINGS_FILE`, `DEFAULT_SYSTEM_PROMPT`, `DEFAULT_SETTINGS` module-level constants
    - config.py Settings class gains `system_prompt: str = ""`, `default_ai_provider: str = "ollama"`, `default_ai_model: str = "llama3.2"`
    - services/classifier.py defines a module-level `_DEFAULT_SYSTEM_PROMPT` constant (the hardcoded string from old config.DEFAULT_SYSTEM_PROMPT). classify_document signature gains `ai_provider: str | None = None, ai_model: str | None = None` keyword params; substitutes config defaults when None; constructs `_settings = {"active_provider": ai_provider, "providers": {ai_provider: {"model": ai_model}}}` and calls `get_provider(_settings)`. NEVER calls `storage.load_settings()`
    - suggest_topics_for_document gets the same treatment (also gains ai_provider/ai_model kwargs and same fallback)
    - services/storage.py: removes `load_settings`, `save_settings`, `mask_api_key`, `settings_masked`; removes these from `__all__`; removes `from config import DEFAULT_SETTINGS, SETTINGS_FILE` and any `import json`, `import copy` lines that are no longer used; adds `from config import settings as app_settings` if not already present (Plan 03-04 may not need it, but classifier does)
    - tasks/document_tasks.py: in `_run` after fetching Document, fetch `user = await session.get(User, doc.user_id)`; compute `ai_provider = (user.ai_provider if user else None) or settings.default_ai_provider`, same for model; pass into `classifier.classify_document(session, document_id, ai_provider=ai_provider, ai_model=ai_model)`. Add `from config import settings` and `from db.models import User` deferred imports inside `_run`
    - backend/api/settings.py: file deleted
    - backend/main.py: removes `from api.settings import router as settings_router` and `app.include_router(settings_router)`
    - backend/tests/test_settings.py: replaces all existing tests with the single `test_settings_endpoint_removed` (the xfail stub from Plan 03-01) — keep file path. Mark previous tests as xfail or simply delete them; the only required test is the 404 assertion
    - GET /api/settings returns 404 (route no longer exists)
    - The Plan 03-01 stub tests test_per_user_provider, test_celery_task_uses_user_provider, test_default_provider_fallback, test_settings_endpoint_removed transition from xfail → pass
  </behavior>
  <action>
    Modify `backend/config.py`:
    1. Remove `SETTINGS_FILE = Path(settings.data_dir) / "settings.json"` (line 60).
    2. Remove the `DEFAULT_SYSTEM_PROMPT = """..."""` block (lines 62-67).
    3. Remove the `DEFAULT_SETTINGS = {...}` dict (lines 69-91).
    4. Inside the `Settings` class (alongside existing fields), add:
       ```
       # AI classification defaults (Phase 3 — D-13, D-15)
       system_prompt: str = ""              # SYSTEM_PROMPT env var; hardcoded fallback lives in classifier.py
       default_ai_provider: str = "ollama"  # DEFAULT_AI_PROVIDER env var
       default_ai_model: str = "llama3.2"   # DEFAULT_AI_MODEL env var
       ```
    5. Verify no module-level imports of `Path` or anything else remain unused after the deletions (clean up `from pathlib import Path` if no other consumers).

    Modify `backend/services/classifier.py`:
    1. At module top, add `_DEFAULT_SYSTEM_PROMPT = """You are a document classification assistant..."""` with the verbatim string from the old `config.DEFAULT_SYSTEM_PROMPT`.
    2. Add imports `import uuid` and `from db.models import Document` and `from config import settings as app_settings`.
    3. Modify `classify_document` signature:
       ```
       async def classify_document(
           session: AsyncSession,
           doc_id: str,
           topic_names: list[str] | None = None,
           ai_provider: str | None = None,
           ai_model: str | None = None,
       ) -> list[str]:
       ```
       Replace `settings = storage.load_settings(); system_prompt = settings.get("system_prompt", ""); provider = get_provider(settings)` with:
       ```
       _ai_provider = ai_provider or app_settings.default_ai_provider
       _ai_model = ai_model or app_settings.default_ai_model
       system_prompt = app_settings.system_prompt or _DEFAULT_SYSTEM_PROMPT
       _settings = {
           "active_provider": _ai_provider,
           "providers": {_ai_provider: {"model": _ai_model}},
       }
       provider = get_provider(_settings)
       ```
    4. Modify `suggest_topics_for_document` with the same signature change (`ai_provider`, `ai_model` kwargs) and same `_settings` construction in place of `storage.load_settings()`.
    5. Ensure no remaining `storage.load_settings()` call exists anywhere in this file.

    Modify `backend/services/storage.py`:
    1. Remove the import `from config import DEFAULT_SETTINGS, SETTINGS_FILE` (line 36) — replace with nothing (config import is no longer needed in this module).
    2. Remove `import copy` and `import json` from top imports (no longer needed).
    3. Remove `load_settings()`, `save_settings()`, `mask_api_key()`, `settings_masked()` function definitions (lines ~416-449).
    4. Remove these from `__all__`: `"load_settings", "save_settings", "mask_api_key", "settings_masked"`.

    Modify `backend/tasks/document_tasks.py` `_run` function:
    1. After `doc = await session.get(Document, doc_uuid)` and the None check, insert:
       ```
       from db.models import User
       from config import settings as app_settings
       user = await session.get(User, doc.user_id) if doc.user_id else None
       ai_provider = (user.ai_provider if user else None) or app_settings.default_ai_provider
       ai_model = (user.ai_model if user else None) or app_settings.default_ai_model
       ```
    2. Replace `topics = await classifier.classify_document(session, document_id)` with `topics = await classifier.classify_document(session, document_id, ai_provider=ai_provider, ai_model=ai_model)`.

    Delete `backend/api/settings.py` entirely. Use the `rm` shell equivalent or write an empty stub — preferable: delete the file. (Executor: use `git rm backend/api/settings.py` or `os.remove` then commit.)

    Modify `backend/main.py`:
    1. Remove `from api.settings import router as settings_router` (line 18).
    2. Remove `app.include_router(settings_router)` (line 174).

    Modify `backend/tests/test_settings.py`:
    1. Delete all existing tests (they reference the removed endpoint and storage functions).
    2. Keep only the single test from Plan 03-01:
       ```
       async def test_settings_endpoint_removed(async_client):
           """D-12: /api/settings endpoint is removed in Phase 3."""
           resp = await async_client.get("/api/settings")
           assert resp.status_code == 404
       ```
       Remove the `@pytest.mark.xfail` marker — this test should now pass green.
  </action>
  <verify>
    <automated>cd backend && pytest tests/test_settings.py tests/test_classifier.py::test_per_user_provider tests/test_classifier.py::test_celery_task_uses_user_provider tests/test_classifier.py::test_default_provider_fallback -x -q 2>&1 | tail -30 && test ! -f backend/api/settings.py && echo "settings.py deleted" && ! grep -q "load_settings" backend/services/storage.py && echo "load_settings removed" && grep -c "default_ai_provider" backend/config.py && grep -c "_DEFAULT_SYSTEM_PROMPT" backend/services/classifier.py && grep -c "user.ai_provider" backend/tasks/document_tasks.py</automated>
  </verify>
  <done>
    `backend/api/settings.py` file does not exist. `backend/services/storage.py` contains no `load_settings`, `save_settings`, `mask_api_key`, `settings_masked` (grep returns 0). `backend/config.py` contains `default_ai_provider` and `default_ai_model`. `backend/services/classifier.py` contains `_DEFAULT_SYSTEM_PROMPT` and accepts `ai_provider` kwarg. `backend/tasks/document_tasks.py` contains `user.ai_provider`. test_settings_endpoint_removed and the 3 classifier tests pass.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Frontend — strip settings store/API and replace SettingsView with admin-managed placeholder</name>
  <files>frontend/src/views/SettingsView.vue, frontend/src/stores/settings.js, frontend/src/api/client.js</files>
  <read_first>
    - frontend/src/views/SettingsView.vue — current form structure; replace entirely with placeholder card
    - frontend/src/stores/settings.js — actions to remove (fetchSettings, save, testConnection, resetPrompt)
    - frontend/src/api/client.js — getSettings, patchSettings, testProvider, getDefaultPrompt functions (lines ~110-132)
    - frontend/src/router/index.js — verify /settings route still exists (do not remove — UI-SPEC Risk 6 says keep route with placeholder)
    - .planning/phases/03-document-migration-multi-user-isolation/03-UI-SPEC.md — Phase 3 spec inherits Phase 2 design system; no specific SettingsView spec — use minimal card matching existing card style
    - .planning/phases/03-document-migration-multi-user-isolation/03-RESEARCH.md — Finding 11 (frontend retirement scope), Risk 6 (UX regression mitigation)
  </read_first>
  <behavior>
    - frontend/src/views/SettingsView.vue is replaced with a static placeholder: a heading "AI Configuration" and a card explaining "AI configuration is managed by your admin." Add a secondary link or text directing users to contact their admin. No form, no API calls, no useSettingsStore import
    - frontend/src/stores/settings.js is reduced to an empty defineStore that exports no actions, OR deleted entirely. Plan: delete the file and update any imports (SettingsView no longer imports it; check if anything else imports it — grep). If other files import it, gut the file to a minimal noop store
    - frontend/src/api/client.js removes the four functions: getSettings, patchSettings, testProvider, getDefaultPrompt (lines ~110-132 in current file). Adds a new export getMyQuota() that hits GET /api/auth/me/quota (used by QuotaBar in Plan 03-05 — pre-emptively add here so Plan 03-05 is a pure UI plan)
    - Visiting /settings in the browser shows the admin-managed placeholder without any console errors
    - SettingsView.vue uses only the Phase 2 design system (existing classes; text-sm / text-xl / bg-white / border-gray-200 / rounded-xl) — UI-SPEC inheritance
  </behavior>
  <action>
    Rewrite `frontend/src/views/SettingsView.vue` with the placeholder content. Template structure:
    ```vue
    <template>
      <div class="p-8 max-w-3xl mx-auto">
        <h2 class="text-2xl font-semibold text-gray-900 mb-1">Settings</h2>
        <p class="text-sm text-gray-500 mb-8">Account-level options for your DocuVault workspace.</p>

        <section class="bg-white border border-gray-200 rounded-xl p-6">
          <h3 class="text-xl font-semibold text-gray-800 mb-2">AI configuration</h3>
          <p class="text-sm text-gray-600">
            AI provider and model are managed by your administrator. Contact your admin
            to request changes to which AI provider is used for your documents.
          </p>
        </section>
      </div>
    </template>

    <script setup>
    // SettingsView is a static placeholder after Phase 3 D-12 settings retirement.
    // No store usage, no API calls — AI config is admin-only via /api/admin/users/{id}/ai-config.
    </script>
    ```
    Remove any `<style>` blocks if present in the original file.

    Delete `frontend/src/stores/settings.js`. Before deleting, grep the frontend tree for imports of `'../stores/settings.js'` or `'@/stores/settings.js'`. If any consumer exists outside SettingsView.vue, instead gut the file to:
    ```js
    // stores/settings.js was retired in Phase 3 D-12 — kept as a minimal no-op to avoid breaking imports.
    // Remove this file in a future cleanup once all consumers are updated.
    import { defineStore } from 'pinia'
    export const useSettingsStore = defineStore('settings', () => {
      return {}
    })
    ```
    Otherwise delete the file.

    Modify `frontend/src/api/client.js`:
    1. Remove the four functions in the `// ── Settings ──` section: `getSettings`, `patchSettings`, `testProvider`, `getDefaultPrompt` (lines ~110-132). Remove the section header comment too.
    2. Add a new function for the quota endpoint (used by Plan 03-05's QuotaBar). Place it under a new `// ── Quota ──` section (or under `// ── Auth ──`):
       ```js
       export function getMyQuota() {
         return request('/api/auth/me/quota')
       }
       ```
    3. Add the upload-flow API helpers used by Plan 03-05:
       ```js
       export function getUploadUrl(filename, contentType) {
         return request('/api/documents/upload-url', {
           method: 'POST',
           headers: { 'Content-Type': 'application/json' },
           body: JSON.stringify({ filename, content_type: contentType }),
         })
       }

       export function confirmUpload(documentId) {
         return request(`/api/documents/${documentId}/confirm`, { method: 'POST' })
       }
       ```
       Place these under the `// ── Documents ──` section near the existing `uploadDocument` function. Leave `uploadDocument` in place for now (Plan 03-05 will remove it when the documents store no longer calls it).
  </action>
  <verify>
    <automated>cd frontend && grep -c "managed by your administrator" src/views/SettingsView.vue && ! grep -q "useSettingsStore" src/views/SettingsView.vue && ! grep -q "getSettings\|patchSettings\|testProvider\|getDefaultPrompt" src/api/client.js && grep -c "getMyQuota\|getUploadUrl\|confirmUpload" src/api/client.js</automated>
  </verify>
  <done>
    SettingsView.vue is the placeholder card (no form, no store). api/client.js has getMyQuota + getUploadUrl + confirmUpload exports and no getSettings/patchSettings/testProvider/getDefaultPrompt. stores/settings.js is deleted OR gutted to a no-op store. Visiting /settings in the dev server renders the card with no console errors (manual check during Plan 03-05 verification — not required here).
  </done>
</task>

</tasks>

<verification>
- Settings endpoint removed: `cd backend && pytest tests/test_settings.py::test_settings_endpoint_removed -x -q`
- Per-user AI classification: `cd backend && pytest tests/test_classifier.py::test_per_user_provider tests/test_classifier.py::test_celery_task_uses_user_provider tests/test_classifier.py::test_default_provider_fallback -x -q`
- No load_settings reference remains: `cd backend && grep -rn 'load_settings\|save_settings' backend --include='*.py' | grep -v tests/` returns no hits
- Frontend settings imports clean: `cd frontend && grep -rn 'getSettings\|patchSettings\|testProvider\|getDefaultPrompt' src/` returns no hits
- All Phase 3 tests still green: `cd backend && pytest -x -q`
</verification>

<success_criteria>
- backend/api/settings.py deleted
- backend/main.py no longer registers settings_router
- backend/services/storage.py no longer contains load_settings/save_settings/mask_api_key/settings_masked
- backend/services/classifier.py classify_document accepts ai_provider and ai_model kwargs; reads no flat file
- backend/tasks/document_tasks.py resolves user.ai_provider via DB lookup and passes to classifier
- backend/config.py exposes system_prompt, default_ai_provider, default_ai_model fields and no longer defines SETTINGS_FILE/DEFAULT_SETTINGS/DEFAULT_SYSTEM_PROMPT
- frontend/src/views/SettingsView.vue shows the admin-managed placeholder
- frontend/src/api/client.js exports getMyQuota, getUploadUrl, confirmUpload and no longer exports settings functions
- All Plan 03-01 stub tests for DOC-03 / DOC-05 / D-12 pass
</success_criteria>

<output>
Create `.planning/phases/03-document-migration-multi-user-isolation/03-04-SUMMARY.md` when done — document the final classifier signature and the env var defaults; note that Phase 4 will keep /settings as a placeholder until a dedicated storage settings page lands.
</output>
