1
0
forked from LPK/sejeteralo

Compare commits

...

4 Commits

Author SHA1 Message Date
aya 28d04a93f3 empty commit 2026-07-24 23:28:14 +02:00
Yvv 3ad89ab744 build(deps): flip @yvv/nuxt-base vers la dép git distante (v0.1.0)
- extends: ['../../yvv-nuxt-base'] → ['@yvv/nuxt-base']
- package.json : dép git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0
- résout au commit taggé b1c6a2f (identique au layer local)
- install --legacy-peer-deps (conflit préexistant @pinia/nuxt 0.9 / pinia 3)
- build npm vert, prod :3099 → 200, smoke palettes 12/12 (6 palettes × mobile/desktop, .palette-dark sur nuit/ocean)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:49:16 +02:00
Yvv 8bfa5f98ef feat: adoption layer yvv-nuxt-base — palettes déléguées à useMood
- extends '../../yvv-nuxt-base' (frontend en sous-dossier)
- app/composables/usePalette.ts : moods avec vars précalculées
  (--color-primary-dark dérivé + variables SVG), délégation à useMood
  (darkClass 'palette-dark', syncColorMode false, storageKey 'sej-palette')
  et synchro de useState('theme-dark') lu par la page graphiques
- DisplaySettings.vue : palette inline (~90 lignes) remplacée par le composable ;
  font-size/densité inchangés
- README : note layer partagé

Vérifié Playwright : 6 palettes × light/dark × mobile/desktop (bodyBg suit
--color-bg, .palette-dark ne bascule que sur nuit/ocean) ; build npm vert ;
prod :3099 → 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:49:16 +02:00
syoul b24f226b35 fix prod : Eau2018.xls hors du volume /app + pin Trivy
Le volume backend-data monté sur /app masquait Eau2018.xls copié dans
l'image à /app/Eau2018.xls — d'où le FileNotFoundError au step seed CI.

- Dockerfile : copie Eau2018.xls dans /opt/ (hors mount)
- seed.py : résolution multi-chemin avec /opt en priorité (Docker)
- .woodpecker.yml : trivy:latest -> trivy:0.70.0 (pin reproductibilité)

Note : si le seed replante avec la même erreur après ce commit, c'est
que le volume backend-data en prod a aussi un seed.py figé (shadowing
de /app entier). Fix de fond à venir : déplacer le mount sur /app/data.
2026-04-26 22:57:39 +02:00
9 changed files with 159 additions and 95 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ steps:
# NOTE: volumes + pas de from_secret : compatible # NOTE: volumes + pas de from_secret : compatible
- name: sbom-scan - name: sbom-scan
image: aquasec/trivy:latest image: aquasec/trivy:0.70.0
volumes: volumes:
- /home/syoul/trivy-cache:/root/.cache/trivy - /home/syoul/trivy-cache:/root/.cache/trivy
commands: commands:
+7
View File
@@ -87,6 +87,13 @@ pinia@^3.0.2 # State management
typescript@^5.8.2 # Typage statique typescript@^5.8.2 # Typage statique
``` ```
Le frontend étend le layer partagé **`../../yvv-nuxt-base`** (`extends` dans
`nuxt.config.ts`) : le composable `useMood` y centralise l'application DOM des
palettes (variables CSS + classe `.palette-dark`), la persistance localStorage et
le light/dark. Les palettes propres à SejeteralO (6 ambiances + variables SVG des
graphiques) sont définies côté projet dans `app/composables/usePalette.ts`, qui
délègue à `useMood` et synchronise l'état partagé `useState('theme-dark')`.
## Base de donnees ## Base de donnees
**SQLite** en dev/production legere, configurable via `DATABASE_URL`. **SQLite** en dev/production legere, configurable via `DATABASE_URL`.
+6 -3
View File
@@ -17,9 +17,12 @@ from app.services.auth_service import hash_password
from app.services.import_service import generate_auth_code from app.services.import_service import generate_auth_code
from app.engine.pricing import HouseholdData, compute_p0 from app.engine.pricing import HouseholdData, compute_p0
XLS_PATH = os.path.join(os.path.dirname(__file__), "Eau2018.xls") _XLS_CANDIDATES = [
if not os.path.exists(XLS_PATH): "/opt/Eau2018.xls", # image Docker (hors volume monté sur /app)
XLS_PATH = os.path.join(os.path.dirname(__file__), "..", "Eau2018.xls") os.path.join(os.path.dirname(__file__), "Eau2018.xls"),
os.path.join(os.path.dirname(__file__), "..", "Eau2018.xls"), # dev local depuis backend/
]
XLS_PATH = next((p for p in _XLS_CANDIDATES if os.path.exists(p)), _XLS_CANDIDATES[-1])
# Codes fixes — identiques dans le dev hint frontend # Codes fixes — identiques dans le dev hint frontend
+2 -1
View File
@@ -11,7 +11,7 @@ COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ . COPY backend/ .
COPY Eau2018.xls /app/Eau2018.xls COPY Eau2018.xls /opt/Eau2018.xls
# Production # Production
FROM base AS production FROM base AS production
@@ -22,6 +22,7 @@ COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3
COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn
COPY --from=build /usr/local/bin/alembic /usr/local/bin/alembic COPY --from=build /usr/local/bin/alembic /usr/local/bin/alembic
COPY --from=build /app /app COPY --from=build /app /app
COPY --from=build /opt/Eau2018.xls /opt/Eau2018.xls
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1 CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1
+4 -90
View File
@@ -61,8 +61,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// Global shared dark mode state (accessible from any component via useState) // Palette : identité + application DOM/persistance/dark déléguées à useMood.
const isDark = useState('theme-dark', () => false) const { paletteList, currentPalette, setPalette, initPalette } = usePalette()
const selectorRef = ref<HTMLElement>() const selectorRef = ref<HTMLElement>()
const isOpen = ref(false) const isOpen = ref(false)
@@ -101,92 +101,6 @@ function setDensity(d: string) {
} }
} }
// ── Palette definitions ──
interface Palette {
name: string; label: string; dark: boolean
primary: string; accent: string
bg: string; surface: string; text: string; textMuted: string; border: string
// SVG-specific
svgPlotBg: string; svgGrid: string; svgLegendBg: string; svgText: string; svgTextLight: string
}
const paletteList: Palette[] = [
// ── Light palettes ──
{
name: 'eau', label: 'Eau', dark: false,
primary: '#2563eb', accent: '#059669',
bg: '#f8fafc', surface: '#ffffff', text: '#1e293b', textMuted: '#64748b', border: '#e2e8f0',
svgPlotBg: '#f8fafc', svgGrid: '#e2e8f0', svgLegendBg: 'white', svgText: '#334155', svgTextLight: '#64748b',
},
{
name: 'terre', label: 'Terre', dark: false,
primary: '#92400e', accent: '#d97706',
bg: '#fefbf6', surface: '#ffffff', text: '#1c1917', textMuted: '#78716c', border: '#e7e5e4',
svgPlotBg: '#faf8f5', svgGrid: '#e7e5e4', svgLegendBg: 'white', svgText: '#44403c', svgTextLight: '#78716c',
},
{
name: 'foret', label: 'Foret', dark: false,
primary: '#166534', accent: '#65a30d',
bg: '#f7fdf9', surface: '#ffffff', text: '#14532d', textMuted: '#6b7c6e', border: '#d1e7d6',
svgPlotBg: '#f5fbf7', svgGrid: '#d1e7d6', svgLegendBg: 'white', svgText: '#2d4a35', svgTextLight: '#6b7c6e',
},
{
name: 'ardoise', label: 'Ardoise', dark: false,
primary: '#475569', accent: '#64748b',
bg: '#f8fafc', surface: '#ffffff', text: '#1e293b', textMuted: '#64748b', border: '#e2e8f0',
svgPlotBg: '#f8fafc', svgGrid: '#e2e8f0', svgLegendBg: 'white', svgText: '#334155', svgTextLight: '#64748b',
},
// ── Dark palettes ──
{
name: 'nuit', label: 'Nuit', dark: true,
primary: '#60a5fa', accent: '#34d399',
bg: '#0f172a', surface: '#1e293b', text: '#f1f5f9', textMuted: '#94a3b8', border: '#334155',
svgPlotBg: '#1e293b', svgGrid: '#334155', svgLegendBg: '#1e293b', svgText: '#cbd5e1', svgTextLight: '#94a3b8',
},
{
name: 'ocean', label: 'Ocean', dark: true,
primary: '#38bdf8', accent: '#2dd4bf',
bg: '#0c1222', surface: '#162032', text: '#e2e8f0', textMuted: '#7dd3fc', border: '#1e3a5f',
svgPlotBg: '#162032', svgGrid: '#1e3a5f', svgLegendBg: '#162032', svgText: '#bae6fd', svgTextLight: '#7dd3fc',
},
]
const currentPalette = ref('eau')
function setPalette(name: string) {
currentPalette.value = name
const p = paletteList.find(x => x.name === name)
if (!p || !import.meta.client) return
localStorage.setItem('sej-palette', name)
const root = document.documentElement
const s = root.style
s.setProperty('--color-primary', p.primary)
s.setProperty('--color-primary-dark', darken(p.primary))
s.setProperty('--color-secondary', p.accent)
s.setProperty('--color-bg', p.bg)
s.setProperty('--color-surface', p.surface)
s.setProperty('--color-text', p.text)
s.setProperty('--color-text-muted', p.textMuted)
s.setProperty('--color-border', p.border)
// SVG theme vars
s.setProperty('--svg-plot-bg', p.svgPlotBg)
s.setProperty('--svg-grid', p.svgGrid)
s.setProperty('--svg-legend-bg', p.svgLegendBg)
s.setProperty('--svg-text', p.svgText)
s.setProperty('--svg-text-light', p.svgTextLight)
root.classList.toggle('palette-dark', p.dark)
isDark.value = p.dark
}
function darken(hex: string): string {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
const f = 0.82
return `#${Math.round(r * f).toString(16).padStart(2, '0')}${Math.round(g * f).toString(16).padStart(2, '0')}${Math.round(b * f).toString(16).padStart(2, '0')}`
}
// ── Init from localStorage ── // ── Init from localStorage ──
onMounted(() => { onMounted(() => {
if (!import.meta.client) return if (!import.meta.client) return
@@ -194,8 +108,8 @@ onMounted(() => {
if (savedFont) setFontSize(savedFont) if (savedFont) setFontSize(savedFont)
const savedDensity = localStorage.getItem('sej-density') const savedDensity = localStorage.getItem('sej-density')
if (savedDensity) setDensity(savedDensity) if (savedDensity) setDensity(savedDensity)
const savedPalette = localStorage.getItem('sej-palette') // Palette : restauration + application déléguées au composable partagé.
if (savedPalette) setPalette(savedPalette) initPalette()
}) })
// ── Click outside to close ── // ── Click outside to close ──
+131
View File
@@ -0,0 +1,131 @@
import type { MoodDef } from '#imports'
/**
* Palettes SejeteralO — identité couleur du projet.
* L'application DOM (variables CSS + classe .palette-dark), la persistance et
* le light/dark sont délégués au mécanisme partagé `useMood` (@yvv/nuxt-base).
* Chaque palette injecte AUSSI ses variables SVG (thème des graphiques) et un
* `--color-primary-dark` dérivé — d'où le précalcul en `vars`.
*/
export interface Palette {
name: string
label: string
dark: boolean
primary: string
accent: string
bg: string
surface: string
text: string
textMuted: string
border: string
// SVG-specific (thème des graphiques)
svgPlotBg: string
svgGrid: string
svgLegendBg: string
svgText: string
svgTextLight: string
}
export const paletteList: Palette[] = [
// ── Light palettes ──
{
name: 'eau', label: 'Eau', dark: false,
primary: '#2563eb', accent: '#059669',
bg: '#f8fafc', surface: '#ffffff', text: '#1e293b', textMuted: '#64748b', border: '#e2e8f0',
svgPlotBg: '#f8fafc', svgGrid: '#e2e8f0', svgLegendBg: 'white', svgText: '#334155', svgTextLight: '#64748b',
},
{
name: 'terre', label: 'Terre', dark: false,
primary: '#92400e', accent: '#d97706',
bg: '#fefbf6', surface: '#ffffff', text: '#1c1917', textMuted: '#78716c', border: '#e7e5e4',
svgPlotBg: '#faf8f5', svgGrid: '#e7e5e4', svgLegendBg: 'white', svgText: '#44403c', svgTextLight: '#78716c',
},
{
name: 'foret', label: 'Foret', dark: false,
primary: '#166534', accent: '#65a30d',
bg: '#f7fdf9', surface: '#ffffff', text: '#14532d', textMuted: '#6b7c6e', border: '#d1e7d6',
svgPlotBg: '#f5fbf7', svgGrid: '#d1e7d6', svgLegendBg: 'white', svgText: '#2d4a35', svgTextLight: '#6b7c6e',
},
{
name: 'ardoise', label: 'Ardoise', dark: false,
primary: '#475569', accent: '#64748b',
bg: '#f8fafc', surface: '#ffffff', text: '#1e293b', textMuted: '#64748b', border: '#e2e8f0',
svgPlotBg: '#f8fafc', svgGrid: '#e2e8f0', svgLegendBg: 'white', svgText: '#334155', svgTextLight: '#64748b',
},
// ── Dark palettes ──
{
name: 'nuit', label: 'Nuit', dark: true,
primary: '#60a5fa', accent: '#34d399',
bg: '#0f172a', surface: '#1e293b', text: '#f1f5f9', textMuted: '#94a3b8', border: '#334155',
svgPlotBg: '#1e293b', svgGrid: '#334155', svgLegendBg: '#1e293b', svgText: '#cbd5e1', svgTextLight: '#94a3b8',
},
{
name: 'ocean', label: 'Ocean', dark: true,
primary: '#38bdf8', accent: '#2dd4bf',
bg: '#0c1222', surface: '#162032', text: '#e2e8f0', textMuted: '#7dd3fc', border: '#1e3a5f',
svgPlotBg: '#162032', svgGrid: '#1e3a5f', svgLegendBg: '#162032', svgText: '#bae6fd', svgTextLight: '#7dd3fc',
},
]
/** Assombrit une couleur hex (facteur 0.82) — variante `--color-primary-dark`. */
function darken(hex: string): string {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
const f = 0.82
return `#${Math.round(r * f).toString(16).padStart(2, '0')}${Math.round(g * f).toString(16).padStart(2, '0')}${Math.round(b * f).toString(16).padStart(2, '0')}`
}
const paletteMoods: MoodDef[] = paletteList.map(p => ({
id: p.name,
label: p.label,
isDark: p.dark,
vars: {
'--color-primary': p.primary,
'--color-primary-dark': darken(p.primary),
'--color-secondary': p.accent,
'--color-bg': p.bg,
'--color-surface': p.surface,
'--color-text': p.text,
'--color-text-muted': p.textMuted,
'--color-border': p.border,
// Thème SVG (graphiques)
'--svg-plot-bg': p.svgPlotBg,
'--svg-grid': p.svgGrid,
'--svg-legend-bg': p.svgLegendBg,
'--svg-text': p.svgText,
'--svg-text-light': p.svgTextLight,
},
}))
export function usePalette() {
const { currentMood, setMood, initMood } = useMood(paletteMoods, {
storageKey: 'sej-palette',
defaultId: 'eau',
syncColorMode: false, // pas de @nuxtjs/color-mode
lightDarkClasses: false, // le CSS ne stylise que .palette-dark
darkClass: 'palette-dark',
})
// État dark partagé, lu par la page graphiques (commune/[slug]) pour le thème SVG.
const isDark = useState('theme-dark', () => false)
function syncDark(id: string) {
const m = paletteMoods.find(x => x.id === id)
if (m) isDark.value = m.isDark
}
const currentPalette = computed(() => currentMood.value || 'eau')
function setPalette(name: string) {
setMood(name)
syncDark(name)
}
function initPalette() {
initMood()
syncDark(currentMood.value || 'eau')
}
return { paletteList, currentPalette, setPalette, initPalette, isDark }
}
+2
View File
@@ -1,4 +1,6 @@
export default defineNuxtConfig({ export default defineNuxtConfig({
extends: ['@yvv/nuxt-base'],
compatibilityDate: '2025-01-01', compatibilityDate: '2025-01-01',
future: { compatibilityVersion: 4 }, future: { compatibilityVersion: 4 },
+5
View File
@@ -8,6 +8,7 @@
"name": "sejeteralo-frontend", "name": "sejeteralo-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
"nuxt": "^4.3.1", "nuxt": "^4.3.1",
"vue": "^3.5.28", "vue": "^3.5.28",
"vue-router": "^5.0.3" "vue-router": "^5.0.3"
@@ -3832,6 +3833,10 @@
"integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@yvv/nuxt-base": {
"version": "0.1.0",
"resolved": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#b1c6a2f28052564d505dd05b363169cb97fb81b3"
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
+1
View File
@@ -9,6 +9,7 @@
"generate": "nuxt generate" "generate": "nuxt generate"
}, },
"dependencies": { "dependencies": {
"@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0",
"nuxt": "^4.3.1", "nuxt": "^4.3.1",
"vue": "^3.5.28", "vue": "^3.5.28",
"vue-router": "^5.0.3" "vue-router": "^5.0.3"