diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4fd89a..f8adf73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,14 @@
## Unreleased — 0.1.6
-Development opened after the stable v0.1.5 release. No 0.1.6 feature tranche
-is selected by this version-opening commit.
+Development opened after the stable v0.1.5 release. The Web refinement tranche
+adds persistent French/English settings, denser shared surfaces, ISO-week
+Dashboard activity, factual exercise-count overview charts, measured-timeline
+muscle-group drill-down, normalized measurement summaries, one shared refined
+BODY ZONES silhouette, and a compact Program header. A new Equipment workspace
+lists factual references and transactionally absorbs a custom duplicate into a
+canonical identity while retaining history/MAX context and recording a causal
+deletion marker; immutable embedded drafts cause an explicit conflict.
## 0.1.5 — 2026-09-23
diff --git a/docs/current_state.md b/docs/current_state.md
index 188bc06..72cf9f6 100644
--- a/docs/current_state.md
+++ b/docs/current_state.md
@@ -159,7 +159,7 @@ parallel implementation of its rules.
| Sleep tonight readiness delta | `TRAINLOG_SLEEP_TONIGHT_CLOSEOUT_V1=PASS`: Meson 111/111, import checks 6/6, Android `testDebugUnitTest` plus `assembleDebug`, Web 191/191 plus typecheck/build, exact V2 quantity replay, private vc47 schema-v32 migration, fresh CYCPLUS BPM and non-mutating Android/Web visual review. |
| 0.1.5 validation | `TRAINLOG_0_1_5_VALIDATION_V1=PASS`: native 111/111; Android unit-test task and APK build; Web 191/191 plus typecheck/build; JSON/import, C-format and diff checks pass. Private vc47 preserves real data through schema v32, renders pre-bed medication quantity controls, reconnects the CYCPLUS and presents the multi-night Web Sleep agenda. |
| Tomorrow session readiness | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: the private vc46 is clean on schema v31 with fresh CYCPLUS BPM and the next Program session is unambiguous; one combined regression proves two ordered occurrences loaded from the synchronized Program, Program provenance, factual timeline/HR ownership, finalization, full generation, desktop import, replay and correlated ACK |
-| Current operational cursor | 0.1.6 development is open after immutable stable v0.1.5; no new feature tranche is selected by the version-opening commit. |
+| Current operational cursor | 0.1.6 Web UI refinement implemented; validation and private deployment review are in progress after immutable stable v0.1.5. |
| Complete synchronization gap contract | Frozen dependency contract; operational USB/Drive slices required by v0.1.2 are delivered |
| Isolated synchronization test environment | `TRAINLOG_SYNC_TEST_ENV_V1=PASS/FROZEN` |
| Synchronization characterization | `TRAINLOG_SYNC_CHARACTERIZATION_V1=PASS/FROZEN` |
@@ -306,6 +306,22 @@ the result. The Web aggregate now exposes the distinct factual snapshot through
## Local Web
+The 0.1.6 refinement adds a persistent French/English preference and a dedicated
+Settings route without moving the global synchronization action. Dashboard
+activity is grouped by calendar month/ISO week, its progression tile is now a
+global factual synthesis, and Analyse progresses from primary BODY ZONE to
+exercise to individual detail. Exercise work time is derived exclusively from
+`session_exercise_timeline` start/end facts; missing timelines remain unknown.
+Measurements first expose unit-safe relative change cards before real-unit
+detail. Dashboard and Analyse reuse the same human-proportioned BODY ZONES SVG.
+
+The Equipment route projects supplied and custom definitions with exercise,
+history, preparation and Program reference counts. Its guarded merge accepts a
+custom duplicate only, transfers relational references in one SQLite IMMEDIATE
+transaction, preserves occurrence/MAX facts indirectly through occurrence
+identity, and writes the existing causal deletion boundary. An immutable
+execution-draft JSON reference rejects the operation rather than being edited.
+
`TRAINLOG_WEB_V1` architecture, API independence, local-network boundary,
browser shell, Dashboard, layout ownership, build/runtime separation, and
security invariants are canonical. The CLI/local HTTP infrastructure and
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 5e507a9..5d15478 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -64,7 +64,7 @@ The heart-rate path remains independent from the frozen Bluetooth Trainlog
synchronization transport.
```text
-CURRENT_OPERATIONAL_CURSOR=0.1.6 development open; next feature tranche not selected.
+CURRENT_OPERATIONAL_CURSOR=0.1.6 Web UI refinement implemented; validation and private deployment review pending.
```
Sleep Diary V1 stays frozen/readable while active capable peers require
diff --git a/docs/tests.md b/docs/tests.md
index 80ee778..1015eb3 100644
--- a/docs/tests.md
+++ b/docs/tests.md
@@ -1,5 +1,18 @@
# Tests and validation
+## Web UI refinement 0.1.6
+
+Web regressions cover calendar-week activity grouping, global rather than
+single-exercise Dashboard progression, exercises-per-date overview data,
+measured-only group drill-down, normalized measurement selection, the shared
+BODY ZONES figure, persistent language/settings navigation, multi-night Sleep
+selection invariants, and Equipment response validation. Native equipment
+coverage exercises a synthetic custom duplicate and proves that a transactional
+merge repoints completed history to the retained canonical identity without
+touching real user equipment. Strict C compilation, the full Meson inventory,
+Web typecheck/build/Vitest, JSON/import validation, changed-C formatting and
+diff checks remain the closeout gates.
+
Sleep Diary coverage includes schema creation/migration, midnight and DST
offsets, revision conflict, logical deletion/reopen, companion replay and
rollback, Web periods/deep links, Android persistence, and vector PDF
diff --git a/web/src/app/App.test.tsx b/web/src/app/App.test.tsx
index 6c4a38d..2cc1e8e 100644
--- a/web/src/app/App.test.tsx
+++ b/web/src/app/App.test.tsx
@@ -163,7 +163,7 @@ describe('shell Trainlog', () => {
it('expose les cinq destinations dans une navigation clavier sémantique', () => {
mockHealth()
render()
- expect(screen.getAllByRole('link').filter((link) => link.closest('nav'))).toHaveLength(5)
+ expect(screen.getAllByRole('link').filter((link) => link.closest('nav'))).toHaveLength(6)
expect(screen.getByRole('link', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'page')
})
})
diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx
index 148c178..db9fb07 100644
--- a/web/src/app/App.tsx
+++ b/web/src/app/App.tsx
@@ -10,10 +10,13 @@ import { ProgramsCalendarPage } from '../routes/ProgramsCalendarPage'
import { SessionsPage } from '../routes/SessionsPage'
import { ExercisesPage } from '../routes/ExercisesPage'
import { AnalysisPage } from '../routes/AnalysisPage'
+import { SettingsPage } from '../routes/SettingsPage'
+import { EquipmentPage } from '../routes/EquipmentPage'
import type { RouteId } from './routes'
import { useRoute } from './useRoute'
import { DatePreferencesProvider } from '../presentation/DatePreferences'
import { fetchAnalysis, type AnalysisSnapshot } from '../api/analysis'
+import { LanguagePreferencesProvider, useLanguagePreferences } from '../presentation/LanguagePreferences'
async function fetchDashboardAnalysis(signal?: AbortSignal): Promise {
const [windowed, allMeasurements] = await Promise.all([
@@ -23,11 +26,12 @@ async function fetchDashboardAnalysis(signal?: AbortSignal): Promise, [string, string, string]> = {
+const placeholderContent: Record, [string, string, string]> = {
sessions: ['Séances', 'ORGANISER', 'Les séances préparées et terminées seront présentées sans confondre plan et réalisé.'],
}
function AppContent() {
+ const { language } = useLanguagePreferences()
const [route, navigate] = useRoute()
const [health, setHealth] = useState(null)
const [healthPending, setHealthPending] = useState(true)
@@ -105,13 +109,15 @@ function AppContent() {
route.id === 'sessions' ? :
route.id === 'programs' ? :
route.id === 'exercises' ? :
+ route.id === 'equipment' ? :
+ route.id === 'settings' ? :
return (