Add disclaimer banner for shared deployments (HuggingFace)
Feature-flagged via DEPLOYMENT_MODE env var and /api/health endpoint. Disabled by default (self-hosted), enabled when deploymentMode=huggingface.
This commit is contained in:
parent
76e8c55b7c
commit
ee57b04f84
7 changed files with 106 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ from dataclasses import dataclass, field
|
|||
class Settings:
|
||||
app_version: str = "dev"
|
||||
conversion_engine: str = "local" # "local" or "remote"
|
||||
deployment_mode: str = "self-hosted" # "self-hosted" or "huggingface"
|
||||
docling_serve_url: str = "http://localhost:5001"
|
||||
docling_serve_api_key: str | None = None
|
||||
conversion_timeout: int = 600
|
||||
|
|
@ -27,6 +28,7 @@ class Settings:
|
|||
return cls(
|
||||
app_version=os.environ.get("APP_VERSION", "dev"),
|
||||
conversion_engine=os.environ.get("CONVERSION_ENGINE", "local"),
|
||||
deployment_mode=os.environ.get("DEPLOYMENT_MODE", "self-hosted"),
|
||||
docling_serve_url=os.environ.get("DOCLING_SERVE_URL", "http://localhost:5001"),
|
||||
docling_serve_api_key=os.environ.get("DOCLING_SERVE_API_KEY"),
|
||||
conversion_timeout=int(os.environ.get("CONVERSION_TIMEOUT", "600")),
|
||||
|
|
|
|||
|
|
@ -117,5 +117,6 @@ async def health() -> dict[str, str]:
|
|||
"status": status,
|
||||
"version": settings.app_version,
|
||||
"engine": settings.conversion_engine,
|
||||
"deploymentMode": settings.deployment_mode,
|
||||
"database": db_status,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class TestSettingsDefaults:
|
|||
s = Settings()
|
||||
assert s.app_version == "dev"
|
||||
assert s.conversion_engine == "local"
|
||||
assert s.deployment_mode == "self-hosted"
|
||||
assert s.docling_serve_url == "http://localhost:5001"
|
||||
assert s.docling_serve_api_key is None
|
||||
assert s.conversion_timeout == 600
|
||||
|
|
@ -30,6 +31,7 @@ class TestSettingsFromEnv:
|
|||
def test_reads_env_vars(self, monkeypatch):
|
||||
monkeypatch.setenv("APP_VERSION", "1.2.3")
|
||||
monkeypatch.setenv("CONVERSION_ENGINE", "remote")
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "huggingface")
|
||||
monkeypatch.setenv("DOCLING_SERVE_URL", "http://serve:9000")
|
||||
monkeypatch.setenv("DOCLING_SERVE_API_KEY", "secret-key")
|
||||
monkeypatch.setenv("CONVERSION_TIMEOUT", "120")
|
||||
|
|
@ -41,6 +43,7 @@ class TestSettingsFromEnv:
|
|||
|
||||
assert s.app_version == "1.2.3"
|
||||
assert s.conversion_engine == "remote"
|
||||
assert s.deployment_mode == "huggingface"
|
||||
assert s.docling_serve_url == "http://serve:9000"
|
||||
assert s.docling_serve_api_key == "secret-key"
|
||||
assert s.conversion_timeout == 120
|
||||
|
|
@ -53,6 +56,7 @@ class TestSettingsFromEnv:
|
|||
for key in (
|
||||
"APP_VERSION",
|
||||
"CONVERSION_ENGINE",
|
||||
"DEPLOYMENT_MODE",
|
||||
"DOCLING_SERVE_URL",
|
||||
"DOCLING_SERVE_API_KEY",
|
||||
"CONVERSION_TIMEOUT",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@
|
|||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="showDisclaimer" class="disclaimer-banner" role="alert">
|
||||
{{ t('disclaimer.banner') }}
|
||||
<button class="disclaimer-close" @click="dismissDisclaimer" aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="app-body">
|
||||
<AppSidebar :open="sidebarOpen" />
|
||||
<main class="main">
|
||||
|
|
@ -39,11 +46,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { RouterView, useRouter } from 'vue-router'
|
||||
import { AppSidebar } from '../shared/ui/index'
|
||||
import { useSettingsStore } from '../features/settings/store'
|
||||
import { useDocumentStore } from '../features/document/store'
|
||||
import { useFeatureFlag } from '../features/feature-flags'
|
||||
import { useI18n } from '../shared/i18n'
|
||||
|
||||
useSettingsStore()
|
||||
|
|
@ -52,6 +60,13 @@ const router = useRouter()
|
|||
const documentStore = useDocumentStore()
|
||||
|
||||
const sidebarOpen = ref(true)
|
||||
const disclaimerEnabled = useFeatureFlag('disclaimer')
|
||||
const disclaimerDismissed = ref(false)
|
||||
const showDisclaimer = computed(() => disclaimerEnabled.value && !disclaimerDismissed.value)
|
||||
|
||||
function dismissDisclaimer() {
|
||||
disclaimerDismissed.value = true
|
||||
}
|
||||
|
||||
function newAnalysis() {
|
||||
documentStore.selectedId = null
|
||||
|
|
@ -143,6 +158,36 @@ body {
|
|||
height: 100vh;
|
||||
}
|
||||
|
||||
.disclaimer-banner {
|
||||
background: #f59e0b;
|
||||
color: #1a1a1d;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding: 8px 40px 8px 16px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.disclaimer-close {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #1a1a1d;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.disclaimer-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: var(--topbar-height);
|
||||
min-height: var(--topbar-height);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ describe('useFeatureFlagStore', () => {
|
|||
const store = useFeatureFlagStore()
|
||||
expect(store.loaded).toBe(false)
|
||||
expect(store.isEnabled('chunking')).toBe(false)
|
||||
expect(store.isEnabled('disclaimer')).toBe(false)
|
||||
})
|
||||
|
||||
it('enables chunking when engine is local', async () => {
|
||||
|
|
@ -36,6 +37,37 @@ describe('useFeatureFlagStore', () => {
|
|||
expect(store.isEnabled('chunking')).toBe(false)
|
||||
})
|
||||
|
||||
it('enables disclaimer when deploymentMode is huggingface', async () => {
|
||||
mockApiFetch.mockResolvedValue({
|
||||
status: 'ok',
|
||||
engine: 'local',
|
||||
deploymentMode: 'huggingface',
|
||||
})
|
||||
const store = useFeatureFlagStore()
|
||||
await store.load()
|
||||
expect(store.deploymentMode).toBe('huggingface')
|
||||
expect(store.isEnabled('disclaimer')).toBe(true)
|
||||
})
|
||||
|
||||
it('disables disclaimer when deploymentMode is self-hosted', async () => {
|
||||
mockApiFetch.mockResolvedValue({
|
||||
status: 'ok',
|
||||
engine: 'local',
|
||||
deploymentMode: 'self-hosted',
|
||||
})
|
||||
const store = useFeatureFlagStore()
|
||||
await store.load()
|
||||
expect(store.isEnabled('disclaimer')).toBe(false)
|
||||
})
|
||||
|
||||
it('defaults deploymentMode to self-hosted when missing', async () => {
|
||||
mockApiFetch.mockResolvedValue({ status: 'ok', engine: 'local' })
|
||||
const store = useFeatureFlagStore()
|
||||
await store.load()
|
||||
expect(store.deploymentMode).toBe('self-hosted')
|
||||
expect(store.isEnabled('disclaimer')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles health endpoint failure gracefully', async () => {
|
||||
mockApiFetch.mockRejectedValue(new Error('Network error'))
|
||||
const store = useFeatureFlagStore()
|
||||
|
|
@ -43,5 +75,6 @@ describe('useFeatureFlagStore', () => {
|
|||
expect(store.loaded).toBe(true)
|
||||
expect(store.error).toBe('Network error')
|
||||
expect(store.isEnabled('chunking')).toBe(false)
|
||||
expect(store.isEnabled('disclaimer')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ import { ref, computed } from 'vue'
|
|||
import { apiFetch } from '../../shared/api/http'
|
||||
|
||||
type ConversionEngine = 'local' | 'remote'
|
||||
type DeploymentMode = 'self-hosted' | 'huggingface'
|
||||
|
||||
interface HealthResponse {
|
||||
status: string
|
||||
engine: ConversionEngine
|
||||
deploymentMode?: DeploymentMode
|
||||
}
|
||||
|
||||
export type FeatureFlag = 'chunking'
|
||||
export type FeatureFlag = 'chunking' | 'disclaimer'
|
||||
|
||||
interface FeatureFlagDef {
|
||||
description: string
|
||||
|
|
@ -18,6 +20,7 @@ interface FeatureFlagDef {
|
|||
|
||||
interface FeatureFlagContext {
|
||||
engine: ConversionEngine | null
|
||||
deploymentMode: DeploymentMode | null
|
||||
}
|
||||
|
||||
const featureRegistry: Record<FeatureFlag, FeatureFlagDef> = {
|
||||
|
|
@ -25,15 +28,21 @@ const featureRegistry: Record<FeatureFlag, FeatureFlagDef> = {
|
|||
description: 'Document chunking for RAG preparation',
|
||||
isEnabled: (ctx) => ctx.engine === 'local',
|
||||
},
|
||||
disclaimer: {
|
||||
description: 'Show shared-instance disclaimer banner',
|
||||
isEnabled: (ctx) => ctx.deploymentMode === 'huggingface',
|
||||
},
|
||||
}
|
||||
|
||||
export const useFeatureFlagStore = defineStore('feature-flags', () => {
|
||||
const engine = ref<ConversionEngine | null>(null)
|
||||
const deploymentMode = ref<DeploymentMode | null>(null)
|
||||
const loaded = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const context = computed<FeatureFlagContext>(() => ({
|
||||
engine: engine.value,
|
||||
deploymentMode: deploymentMode.value,
|
||||
}))
|
||||
|
||||
function isEnabled(flag: FeatureFlag): boolean {
|
||||
|
|
@ -46,6 +55,7 @@ export const useFeatureFlagStore = defineStore('feature-flags', () => {
|
|||
try {
|
||||
const data = await apiFetch<HealthResponse>('/api/health')
|
||||
engine.value = data.engine
|
||||
deploymentMode.value = data.deploymentMode ?? 'self-hosted'
|
||||
loaded.value = true
|
||||
error.value = null
|
||||
} catch (e) {
|
||||
|
|
@ -54,5 +64,5 @@ export const useFeatureFlagStore = defineStore('feature-flags', () => {
|
|||
}
|
||||
}
|
||||
|
||||
return { engine, loaded, error, isEnabled, load }
|
||||
return { engine, deploymentMode, loaded, error, isEnabled, load }
|
||||
})
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ const messages: Messages = {
|
|||
'settings.themeDark': 'Sombre',
|
||||
'settings.themeLight': 'Clair',
|
||||
'settings.language': 'Langue',
|
||||
|
||||
// Disclaimer
|
||||
'disclaimer.banner':
|
||||
'Instance de d\u00e9monstration \u2014 les documents upload\u00e9s sont partag\u00e9s et temporaires. Ne pas envoyer de fichiers confidentiels.',
|
||||
},
|
||||
en: {
|
||||
'nav.home': 'Home',
|
||||
|
|
@ -242,6 +246,10 @@ const messages: Messages = {
|
|||
'settings.themeDark': 'Dark',
|
||||
'settings.themeLight': 'Light',
|
||||
'settings.language': 'Language',
|
||||
|
||||
// Disclaimer
|
||||
'disclaimer.banner':
|
||||
'Demo instance \u2014 uploaded documents are shared and temporary. Do not upload confidential files.',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue