Compare commits
4
Commits
4057f50bd3
...
64e714c1dd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e714c1dd | ||
|
|
03f763ab57 | ||
|
|
230f79a8c0 | ||
|
|
89d5f83c7f |
+7
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
## ÉTAT — 2026-10-26 vers 19h50
|
||||
|
||||
**En ligne** : `a6ba0a6`, les messages des visiteurs sont persistés dans le volume
|
||||
**En ligne** : `4057f50` (code de `a6ba0a6`), les messages des visiteurs sont persistés dans le volume
|
||||
nommé `librodrome-data` sur `/app/data`. Acte exécuté le 26/10, 19h38-19h44, par la
|
||||
session dev-65 (orch), et vérifié : sauvegarde, veille, bascule, redémarrage à froid,
|
||||
1 message conservé, 0 graine rejouée. Dossier : `actes/2026-10-26-persistance-messages.md`.
|
||||
@@ -16,9 +16,12 @@ session dev-65 (orch), et vérifié : sauvegarde, veille, bascule, redémarrage
|
||||
4. Canon démenti : `/DEV/AGENTS.md` (Démarrage §3) et `harness/conventions/git.md`
|
||||
disent encore que librodrome écrit du YAML en prod via /admin et git sync.
|
||||
|
||||
**Preuve en vraie grandeur** : au déploiement suivant, `docker exec
|
||||
lpk-librodrome-main-app-1 sha256sum /app/data/messages.yml` doit rester
|
||||
`c8afc7de…a6b8` (tant qu'aucun message n'arrive), sans ligne `seed-messages`.
|
||||
**Preuve en vraie grandeur, faite** : déploiement suivant (`4057f50`, doc seule, pipeline
|
||||
n° 59, sur le « Déploie » d'Yvv) : conteneur recréé, `messages.yml` inchangé
|
||||
(`c8afc7de…a6b8`), 0 graine rejouée.
|
||||
|
||||
**Local, non poussé** : le commit qui consigne cette preuve (doc seule) ; il part avec
|
||||
le prochain déploiement.
|
||||
|
||||
## REPRISE — bascule quota 2026-09-29 14h05
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ rapporte.
|
||||
- [x] La veille a vu partir l'ancien conteneur, et sa dernière relève est identique à la sauvegarde
|
||||
- [x] Pipeline du commit `a6ba0a6662d5` : `success`
|
||||
- [x] Sauvegarde présente sur sonic : `~/librodrome-sauvegardes/2026-10-26/` (700, fichiers 600 ; `messages.yml`, `SHA256SUMS`, `ancien-conteneur.id`, relève de la veille)
|
||||
- [ ] Au déploiement suivant, quel qu'il soit : `docker exec lpk-librodrome-main-app-1 sha256sum /app/data/messages.yml` inchangé, et aucune ligne `seed-messages`. C'est la preuve en vraie grandeur.
|
||||
- [x] Au déploiement suivant, quel qu'il soit : `docker exec lpk-librodrome-main-app-1 sha256sum /app/data/messages.yml` inchangé, et aucune ligne `seed-messages`. C'est la preuve en vraie grandeur.
|
||||
|
||||
## Retour
|
||||
|
||||
@@ -269,5 +269,13 @@ indépendant) ; m3 pas de témoin de fin d'étape 1 ; m4 `CLAUDE.md` décrivait
|
||||
sont pas prévus par la boucle de l'étape 3 ; m6 le dossier de sauvegarde est à retirer
|
||||
à la main si l'étape 1 échoue.
|
||||
|
||||
Reste ouvert : la preuve en vraie grandeur au déploiement suivant (dernière case des
|
||||
Vérifications après), et les décisions du § Hors champ.
|
||||
**Preuve en vraie grandeur** : faite au déploiement suivant, par la session
|
||||
librodrome-e9, sur le « Déploie » d'Yvv tapé dans cette session (26/10, vers 19h50 :
|
||||
« il bosse, je le court-circuite. Déploie […] »). Push de `4057f50` (doc seule) ; le
|
||||
pipeline n° 59 passe de running (19:51:39) à success (19:53:49). Sur sonic, en lecture
|
||||
seule : le conteneur `4f97cff481d8…`, créé à 18:43:43Z, est remplacé par `154b4906183c…`,
|
||||
créé à 18:53:43Z ; seul montage, `librodrome-data` sur `/app/data` ; `messages.yml` reste
|
||||
à `c8afc7de…a6b8` avant comme après ; 0 ligne `seed-messages` (journal : 1 ligne). Vu de
|
||||
l'extérieur (19:54:44) : health `200`, accueil `200`, messages `1`.
|
||||
|
||||
Reste ouvert : les décisions du § Hors champ.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div
|
||||
ref="root"
|
||||
class="console"
|
||||
:class="{ 'console--compact': compact, 'console--still': reduced }"
|
||||
@pointerenter="hovered = true"
|
||||
@pointerleave="hovered = false"
|
||||
@focusin="hovered = true"
|
||||
@focusout="hovered = false"
|
||||
:class="{ 'console--compact': compact, 'console--still': reduced, 'console--hand': engaged }"
|
||||
@pointerenter="pointerIn = true"
|
||||
@pointerleave="pointerIn = false"
|
||||
@focusin="focusIn = true"
|
||||
@focusout="onFocusOut"
|
||||
>
|
||||
<div class="console-board">
|
||||
<!-- Scale, read bottom to top: from asservissement to émancipation -->
|
||||
@@ -30,12 +30,17 @@
|
||||
type="button"
|
||||
role="tab"
|
||||
class="fader"
|
||||
:class="{ 'fader--active': axis.key === active }"
|
||||
:class="{ 'fader--active': axis.key === active, 'fader--rising': rising.has(axis.key) }"
|
||||
:style="{ '--hue': `var(${axis.hue})`, '--level': levels[axis.key] ?? 0, '--i': i }"
|
||||
:data-axis="axis.key"
|
||||
:aria-selected="axis.key === active"
|
||||
:aria-controls="`panel-${uid}`"
|
||||
:tabindex="axis.key === active ? 0 : -1"
|
||||
@click="choose(axis.key)"
|
||||
@pointerdown="lastPointer = $event.pointerType"
|
||||
@pointerenter="onEnter($event, axis.key)"
|
||||
@pointerleave="hoverKey = null"
|
||||
@focus="onFocus($event, axis.key)"
|
||||
@click="onClick($event, axis.key)"
|
||||
>
|
||||
<span class="fader-track" aria-hidden="true">
|
||||
<span class="fader-fill" />
|
||||
@@ -127,53 +132,152 @@ const tabs = ref<HTMLButtonElement[]>([])
|
||||
const current = computed(() => props.axes.find(a => a.key === active.value) ?? props.axes[0])
|
||||
|
||||
// ── Motion ──────────────────────────────────────────────────────────────
|
||||
// The cursors move on their own, slowly: each one drifts, and every few
|
||||
// beats another axis is pushed up towards émancipation. Visitors watch it;
|
||||
// they can still pick an axis (click, arrows), which stops the rotation.
|
||||
// Yvv's instructions, word for word: docs/consignes-yvv.md (29/09 and 03/11).
|
||||
//
|
||||
// Before any gesture (29/09): the cursors move on their own, slowly; the low
|
||||
// ones drift, and every few beats another axis is pushed up to its own
|
||||
// graduation (03/11), the level the hand gives it.
|
||||
// SSR renders every cursor at the bottom, so the first move is the rise.
|
||||
//
|
||||
// The visitor's hand takes over for good (03/11): from the first hover,
|
||||
// keyboard focus or tap on a cursor, the rotation stops. The touched cursor
|
||||
// replays its rise, from the bottom to its own graduation, and its panel
|
||||
// shows. The others do not move; a cursor never touched stays where it was.
|
||||
const BEAT_MS = 2600
|
||||
const BEATS_PER_AXIS = 3
|
||||
/** Graduation of each axis, from asservissement (0) to émancipation (1) */
|
||||
const GRADUATION: Record<AxisKey, number> = { numerique: 3 / 4, economie: 1 / 2, politique: 2 / 3 }
|
||||
/** A focus and a click within this delay are one gesture (a tap): one rise */
|
||||
const SAME_GESTURE_MS = 500
|
||||
|
||||
const levels = reactive<Partial<Record<AxisKey, number>>>({})
|
||||
/** Cursors replaying their rise (fast transition) */
|
||||
const rising = reactive(new Set<AxisKey>())
|
||||
const reduced = ref(false)
|
||||
const hovered = ref(false)
|
||||
const visible = ref(true)
|
||||
const chosen = ref(false)
|
||||
const engaged = ref(false)
|
||||
const pointerIn = ref(false)
|
||||
const focusIn = ref(false)
|
||||
const hoverKey = ref<AxisKey | null>(null)
|
||||
let lastPointer = ''
|
||||
const lastRise: Partial<Record<AxisKey, number>> = {}
|
||||
const riseRun: Partial<Record<AxisKey, number>> = {}
|
||||
let beat = 0
|
||||
let timer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
function place() {
|
||||
props.axes.forEach((axis, i) => {
|
||||
const isActive = axis.key === active.value
|
||||
if (reduced.value) {
|
||||
levels[axis.key] = isActive ? 0.94 : 0.3
|
||||
// The axis put forward goes up to its own graduation, as under the hand
|
||||
if (axis.key === active.value) {
|
||||
levels[axis.key] = GRADUATION[axis.key] ?? 0.75
|
||||
return
|
||||
}
|
||||
const wave = Math.sin(beat * 0.9 + i * 2.1)
|
||||
levels[axis.key] = isActive ? 0.87 + 0.06 * wave : 0.3 + 0.14 * wave
|
||||
levels[axis.key] = reduced.value ? 0.3 : 0.3 + 0.14 * Math.sin(beat * 0.9 + i * 2.1)
|
||||
})
|
||||
}
|
||||
|
||||
function faderEl(key: AxisKey) {
|
||||
return root.value?.querySelector<HTMLElement>(`[data-axis="${key}"]`) ?? null
|
||||
}
|
||||
|
||||
/** Level currently drawn (mid-transition included), read from the DOM */
|
||||
function drawnLevel(key: AxisKey) {
|
||||
const el = faderEl(key)
|
||||
const v = el ? Number.parseFloat(getComputedStyle(el).getPropertyValue('--level')) : Number.NaN
|
||||
return Number.isFinite(v) ? v : (levels[key] ?? 0)
|
||||
}
|
||||
|
||||
/** First gesture: the rotation stops for good, every cursor stays where it is drawn */
|
||||
function engage() {
|
||||
if (engaged.value) return
|
||||
for (const axis of props.axes) levels[axis.key] = drawnLevel(axis.key)
|
||||
engaged.value = true
|
||||
clearInterval(timer)
|
||||
}
|
||||
|
||||
/** The touched cursor replays its rise: back to the bottom, then up to its graduation */
|
||||
async function rise(key: AxisKey) {
|
||||
lastRise[key] = Date.now()
|
||||
const target = GRADUATION[key] ?? 0.75
|
||||
if (reduced.value) {
|
||||
levels[key] = target
|
||||
return
|
||||
}
|
||||
const run = (riseRun[key] ?? 0) + 1
|
||||
riseRun[key] = run
|
||||
rising.delete(key)
|
||||
levels[key] = 0
|
||||
await nextTick()
|
||||
// Commit the bottom (no transition) before the rise starts from it
|
||||
const el = faderEl(key)
|
||||
if (el) getComputedStyle(el).getPropertyValue('--level')
|
||||
if (riseRun[key] !== run) return
|
||||
rising.add(key)
|
||||
levels[key] = target
|
||||
}
|
||||
|
||||
/** A cursor under the visitor's hand: it rises and its panel shows */
|
||||
function touch(key: AxisKey) {
|
||||
engage()
|
||||
active.value = key
|
||||
if (Date.now() - (lastRise[key] ?? 0) < SAME_GESTURE_MS) return
|
||||
rise(key)
|
||||
}
|
||||
|
||||
function onEnter(e: PointerEvent, key: AxisKey) {
|
||||
// A finger scrolling over a cursor is not a rollover: taps go through the click
|
||||
if (e.pointerType === 'touch') return
|
||||
hoverKey.value = key
|
||||
touch(key)
|
||||
}
|
||||
|
||||
function isKeyboardFocus(el: HTMLElement) {
|
||||
try {
|
||||
return el.matches(':focus-visible')
|
||||
}
|
||||
catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function onFocus(e: FocusEvent, key: AxisKey) {
|
||||
// Keyboard focus is a rollover; a focus from the mouse or a tap is handled elsewhere
|
||||
if (hoverKey.value === key || !isKeyboardFocus(e.currentTarget as HTMLElement)) return
|
||||
touch(key)
|
||||
}
|
||||
|
||||
function onClick(e: MouseEvent, key: AxisKey) {
|
||||
const pointer = (e as PointerEvent).pointerType || lastPointer
|
||||
lastPointer = ''
|
||||
if (pointer === 'touch') touch(key)
|
||||
else {
|
||||
engage()
|
||||
active.value = key
|
||||
}
|
||||
}
|
||||
|
||||
function onFocusOut(e: FocusEvent) {
|
||||
focusIn.value = !!root.value?.contains(e.relatedTarget as Node | null)
|
||||
}
|
||||
|
||||
function advance() {
|
||||
const idx = props.axes.findIndex(a => a.key === active.value)
|
||||
active.value = props.axes[(idx + 1) % props.axes.length]!.key
|
||||
}
|
||||
|
||||
function onBeat() {
|
||||
if (!visible.value) return
|
||||
if (!visible.value || engaged.value) return
|
||||
beat++
|
||||
if (!chosen.value && !hovered.value && beat % BEATS_PER_AXIS === 0) advance()
|
||||
if (!pointerIn.value && !focusIn.value && beat % BEATS_PER_AXIS === 0) advance()
|
||||
place()
|
||||
}
|
||||
|
||||
function choose(key: AxisKey) {
|
||||
chosen.value = true
|
||||
active.value = key
|
||||
}
|
||||
|
||||
watch(active, place)
|
||||
watch(active, () => {
|
||||
if (!engaged.value) place()
|
||||
})
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
lastPointer = ''
|
||||
const keys = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End']
|
||||
if (!keys.includes(e.key)) return
|
||||
e.preventDefault()
|
||||
@@ -184,7 +288,7 @@ function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') next = idx === 0 ? last : idx - 1
|
||||
if (e.key === 'Home') next = 0
|
||||
if (e.key === 'End') next = last
|
||||
choose(props.axes[next]!.key)
|
||||
touch(props.axes[next]!.key)
|
||||
nextTick(() => tabs.value[next]?.focus())
|
||||
}
|
||||
|
||||
@@ -279,6 +383,15 @@ onMounted(() => {
|
||||
transition-delay: calc(var(--i) * 160ms);
|
||||
}
|
||||
|
||||
/* The visitor's hand has taken over: nothing glides any more but the touched cursor */
|
||||
.console--hand .fader {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.console--hand .fader--rising {
|
||||
transition: --level 1.1s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.fader:focus-visible {
|
||||
outline: 2px solid hsl(var(--hue));
|
||||
outline-offset: 2px;
|
||||
@@ -544,6 +657,7 @@ onMounted(() => {
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fader,
|
||||
.console--hand .fader--rising,
|
||||
.fader-fill,
|
||||
.fader-name,
|
||||
.fader-lever,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Consignes d'Yvv — design du site
|
||||
|
||||
Ce fichier rassemble les consignes qu'Yvv donne en conversation, avec leur date. Les consignes
|
||||
entre guillemets sont recopiées mot pour mot ; les autres le disent. Elles l'emportent sur les choix
|
||||
d'interprétation des sessions.
|
||||
|
||||
Pourquoi ce fichier : la graduation de la console, donnée le 29/09, s'est perdue avec le transcript
|
||||
où elle était écrite. Une consigne qui ne vit que dans une conversation finit par disparaître.
|
||||
|
||||
## Accueil — la console des trois curseurs
|
||||
|
||||
Code : `app/components/home/HomeConsole.vue`. Libellés : `site/pages/home.yml > console`.
|
||||
|
||||
### 29/09/2026
|
||||
|
||||
Le transcript a été effacé. Ces quatre consignes sont reconstituées d'après le commit `201cfbb` et le
|
||||
relais de la session orch le 03/11 : ce ne sont pas les mots d'Yvv.
|
||||
|
||||
- l'axe de la console a Asservissement en bas et Émancipation en haut ;
|
||||
- une animation où les curseurs bougent lentement, pour qu'on ne croie pas devoir les manipuler ;
|
||||
- « AUTONOMIE » devant les légendes des curseurs ;
|
||||
- « Autonomie numérique, économique, citoyenne » et « Code source, création monétaire, décision »
|
||||
mis en valeur.
|
||||
|
||||
### 03/11/2026, vers 03h58 (à la session orch)
|
||||
|
||||
Mot pour mot, d'après `~/harness/etat/reponses-yvv.md` :
|
||||
|
||||
> « Et retrouve les consignes, comme par exemple celles pour l'animation (curseurs qui montent au
|
||||
> rollover et ne fait pas descendre les autres, je t'avais donné une graduation). »
|
||||
|
||||
### 03/11/2026, entre 05h40 et 05h45 (relayée mot pour mot par la session orch)
|
||||
|
||||
> « Je t'avais dit déjà tout ça. Au rollover, numérique monte à 3/4, économie monte à 1/2, citoyenne
|
||||
> monte à 2/3. Les curseurs restent. Si on rollover à nouveau ils refont leur montée. Et le pavé
|
||||
> correspondant s'affiche bien sûr au même rollover. »
|
||||
|
||||
Ce que fait le code :
|
||||
- Un survol à la souris, un focus au clavier ou un tap sur un curseur arrête pour de bon la rotation
|
||||
automatique.
|
||||
- Le curseur touché repart d'en bas et monte en 1,1 s à son cran : numérique 0,75, économique 0,50,
|
||||
citoyenne 0,667. Il y reste.
|
||||
- Le panneau de l'axe s'affiche au même geste.
|
||||
- Les autres curseurs ne bougent pas. Un curseur jamais touché reste là où il était.
|
||||
- Un nouveau survol rejoue la montée.
|
||||
- Avant tout geste, l'animation du 29/09 continue : la rotation et la dérive des curseurs bas. L'axe
|
||||
mis en avant monte à son cran, et non plus vers 0,9. C'est une décision d'orch du 03/11 à 05h51, qui
|
||||
découle de la graduation : l'animation et la main donnent les mêmes niveaux.
|
||||
- Sous `prefers-reduced-motion`, il n'y a aucune transition, mais les crans sont les mêmes.
|
||||
- Un défilement au doigt qui part d'un curseur ne compte pas comme un survol.
|
||||
Reference in New Issue
Block a user