chore(version): update version and changelog for release 0.8.134
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s

- Bumped APP_VERSION to 0.8.134 and updated the changelog to reflect recent changes.
- Continued Frontend Phase 4 with the introduction of the `frontend/src/api/planning.js` module for training planning.
- Updated architecture documentation to include details on the new `planning.js` API and its integration with the existing client structure.
- Enhanced `utils/api.js` to re-export the new planning module, streamlining API access.
This commit is contained in:
Lars 2026-05-14 21:39:45 +02:00
parent 8f5af49a6f
commit 8175e239b4
5 changed files with 188 additions and 200 deletions

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information # Shinkan Jinkendo Version Information
APP_VERSION = "0.8.133" APP_VERSION = "0.8.134"
BUILD_DATE = "2026-05-12" BUILD_DATE = "2026-05-12"
DB_SCHEMA_VERSION = "20260514062" DB_SCHEMA_VERSION = "20260514062"
@ -36,6 +36,13 @@ MODULE_VERSIONS = {
} }
CHANGELOG = [ CHANGELOG = [
{
"version": "0.8.134",
"date": "2026-05-14",
"changes": [
"Frontend Phase 4 Welle 2: frontend/src/api/planning.js (Trainingsplanung); utils/api.js re-exportiert Modul, api-Objekt spread planning.",
],
},
{ {
"version": "0.8.133", "version": "0.8.133",
"date": "2026-05-14", "date": "2026-05-14",

View File

@ -9,7 +9,8 @@ Dieses Bündel ist die **Leitlinie für die große Refaktorierung** nach dem MVP
| [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md) | Zielarchitektur (Frontend, API, Daten), Qualitätsziele, Einbindung neuer Features | | [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md) | Zielarchitektur (Frontend, API, Daten), Qualitätsziele, Einbindung neuer Features |
| [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md) | Erfasste Architekturschuld, Reihenfolge und Massnahmen zur Behebung | | [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md) | Erfasste Architekturschuld, Reihenfolge und Massnahmen zur Behebung |
| [UMSETZUNGSPLAN_ROADMAP.md](./UMSETZUNGSPLAN_ROADMAP.md) | Phasen, Meilensteine, Abnahmekriterien, Aufwandsschwerpunkte | | [UMSETZUNGSPLAN_ROADMAP.md](./UMSETZUNGSPLAN_ROADMAP.md) | Phasen, Meilensteine, Abnahmekriterien, Aufwandsschwerpunkte |
| [`frontend/src/api/client.js`](../../frontend/src/api/client.js) | Phase 4: zentraler HTTP-Client (`request`); `utils/api.js` bleibt Facade | | [`frontend/src/api/client.js`](../../frontend/src/api/client.js) | Phase 4: zentraler HTTP-Client (`request`, `ACTIVE_CLUB_STORAGE_KEY`) |
| [`frontend/src/api/planning.js`](../../frontend/src/api/planning.js) | Phase 4: Trainingsplanung (Einheiten, Vorlagen, Module, Rahmen, KPIs) |
| [BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md) | Phase 0: Bundle-, API- und Last-Baseline (Messvorlagen, Vergleich nach Phase 2) | | [BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md) | Phase 0: Bundle-, API- und Last-Baseline (Messvorlagen, Vergleich nach Phase 2) |
| [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md) | **Verbindliche** Shinkan-spezifische Regeln (Ergänzung zu den globalen Rules) | | [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md) | **Verbindliche** Shinkan-spezifische Regeln (Ergänzung zu den globalen Rules) |

View File

@ -8,7 +8,7 @@
- **Phase 2:** **abgeschlossen** (2026-05-14) — Indizes 058062, Keyset `/api/exercises` + `/api/training-units`, **`/api/dashboard/kpis`** inkl. `training_home`, EXPLAIN-Vorlagen **`scripts/load/explain-readpaths.sql`**. - **Phase 2:** **abgeschlossen** (2026-05-14) — Indizes 058062, Keyset `/api/exercises` + `/api/training-units`, **`/api/dashboard/kpis`** inkl. `training_home`, EXPLAIN-Vorlagen **`scripts/load/explain-readpaths.sql`**.
- **Offen Phase 1:** Inbox optional **TTL** / nur bei sichtbarem Widget. - **Offen Phase 1:** Inbox optional **TTL** / nur bei sichtbarem Widget.
- **Phase 3 (abgeschlossen 2026-05-14):** Übungsliste modularisiert; Trainingsplanung/Übungsformular: **Page-Dateien unter Soft-Limit** — Implementierung in `TrainingPlanningPageRoot.jsx`, `ExerciseFormPageRoot.jsx`, `ExercisesListPageRoot.jsx`; `pages/*.jsx` nur Re-Export. Playwright **Tests 910**. - **Phase 3 (abgeschlossen 2026-05-14):** Übungsliste modularisiert; Trainingsplanung/Übungsformular: **Page-Dateien unter Soft-Limit** — Implementierung in `TrainingPlanningPageRoot.jsx`, `ExerciseFormPageRoot.jsx`, `ExercisesListPageRoot.jsx`; `pages/*.jsx` nur Re-Export. Playwright **Tests 910**.
- **Phase 4 (gestartet 2026-05-14):** API **Welle 1**`frontend/src/api/client.js` (`request`); `utils/api.js` bleibt Facade. - **Phase 4 (fortlaufend 2026-05-14):** API **Welle 1** `client.js`; **Welle 2** `planning.js`; `utils/api.js` bleibt Facade (`export *`, `api`-Objekt `...planning`).
**Ziel:** Nach MVP eine **nachhaltige** Architektur für Wachstum, **Performance** (Server + schwache Clients) und **sichere Feature-Erweiterung**. **Ziel:** Nach MVP eine **nachhaltige** Architektur für Wachstum, **Performance** (Server + schwache Clients) und **sichere Feature-Erweiterung**.
**Leitdokumente:** [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md), [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md), [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md). **Leitdokumente:** [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md), [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md), [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md).
@ -93,13 +93,14 @@
## Phase 4 API-Client Modularisierung ## Phase 4 API-Client Modularisierung
**Status:** **gestartet** (2026-05-14) — Welle 1: **`frontend/src/api/client.js`** (`request`, `ACTIVE_CLUB_STORAGE_KEY`); **`utils/api.js`** bleibt vollständige Facade (bestehende Importe unverändert). **Status:** **fortlaufend** (2026-05-14) — Welle 1: **`client.js`**; Welle 2: **`planning.js`**; **`utils/api.js`** bleibt vollständige Facade.
**Fokus:** Wartbarkeit für viele neue Features. **Fokus:** Wartbarkeit für viele neue Features.
| Task | Bezug | Status | | Task | Bezug | Status |
|------|-------|--------| |------|-------|--------|
| `frontend/src/api/client.js` — zentraler HTTP-Client | A2 | erledigt (Welle 1) | | `frontend/src/api/client.js` — zentraler HTTP-Client | A2 | erledigt (Welle 1) |
| `frontend/src/api/planning.js` — Trainingsplanung (Einheiten, Vorlagen, Module, Rahmen, Dashboard-KPIs) | A2 | erledigt (Welle 2) |
| Domänen-Module unter `frontend/src/api/` + schrittweise Entlastung von `api.js` | A2 | offen | | Domänen-Module unter `frontend/src/api/` + schrittweise Entlastung von `api.js` | A2 | offen |
| Neue Endpoints primär in Domänen-Dateien | S3 | offen | | Neue Endpoints primär in Domänen-Dateien | S3 | offen |

View File

@ -0,0 +1,171 @@
/**
* Trainingsplanung: Einheiten, Vorlagen, Module, Rahmenprogramme, Dashboard-KPIs.
* Facade: weiterhin `utils/api.js` (default + Named Exports).
*/
import { request } from './client.js'
/** Query-Parameter wie GET /api/training-units. */
export async function listTrainingUnits(filters = {}) {
const q = new URLSearchParams()
if (filters.group_id != null && filters.group_id !== '') {
q.set('group_id', String(filters.group_id))
}
if (filters.club_id != null && filters.club_id !== '') {
q.set('club_id', String(filters.club_id))
}
if (filters.start_date) q.set('start_date', filters.start_date)
if (filters.end_date) q.set('end_date', filters.end_date)
if (filters.status) q.set('status', filters.status)
if (filters.debrief_pending === true) q.set('debrief_pending', 'true')
if (filters.assigned_to_me === true) q.set('assigned_to_me', 'true')
if (filters.sort) q.set('sort', String(filters.sort))
if (filters.limit != null && filters.limit !== '') q.set('limit', String(filters.limit))
if (filters.cursor_planned_date) q.set('cursor_planned_date', String(filters.cursor_planned_date))
if (filters.cursor_planned_time != null && filters.cursor_planned_time !== '') {
q.set('cursor_planned_time', String(filters.cursor_planned_time))
}
if (filters.cursor_id != null && filters.cursor_id !== '') q.set('cursor_id', String(filters.cursor_id))
const qs = q.toString()
return request(`/api/training-units${qs ? `?${qs}` : ''}`)
}
/** Dashboard Kurzüberblick: Entwürfe / meine Übungen / YTD abgeschlossene Einheiten (ein Roundtrip). */
export async function getDashboardKpis() {
return request('/api/dashboard/kpis')
}
/** Dashboard: Übungen in geplanten Einheiten, die für den Verein noch auf Sichtbarkeit „Verein“ gehören. */
export async function getTrainingExerciseClubVisibilityQueue(filters = {}) {
const q = new URLSearchParams()
if (filters.start_date) q.set('start_date', String(filters.start_date))
if (filters.end_date) q.set('end_date', String(filters.end_date))
if (filters.assigned_to_me === false) q.set('assigned_to_me', 'false')
if (filters.limit_units != null && filters.limit_units !== '') {
q.set('limit_units', String(filters.limit_units))
}
const qs = q.toString()
return request(`/api/training-units/exercises-club-visibility-queue${qs ? `?${qs}` : ''}`)
}
export async function getTrainingUnit(id) {
return request(`/api/training-units/${id}`)
}
export async function createTrainingUnit(data) {
return request('/api/training-units', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function updateTrainingUnit(id, data) {
return request(`/api/training-units/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteTrainingUnit(id) {
return request(`/api/training-units/${id}`, { method: 'DELETE' })
}
export async function quickCreateTrainingUnit(data) {
return request('/api/training-units/quick-create', {
method: 'POST',
body: JSON.stringify(data),
})
}
/** Rahmen-Slot → geplante Einheit (tiefe Kopie, origin_framework_slot_id). */
export async function createTrainingUnitFromFrameworkSlot(data) {
return request('/api/training-units/from-framework-slot', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function listTrainingPlanTemplates() {
return request('/api/training-plan-templates')
}
export async function getTrainingPlanTemplate(id) {
return request(`/api/training-plan-templates/${id}`)
}
export async function createTrainingPlanTemplate(data) {
return request('/api/training-plan-templates', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function updateTrainingPlanTemplate(id, data) {
return request(`/api/training-plan-templates/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteTrainingPlanTemplate(id) {
return request(`/api/training-plan-templates/${id}`, { method: 'DELETE' })
}
export async function listTrainingModules() {
return request('/api/training-modules')
}
export async function getTrainingModule(id) {
return request(`/api/training-modules/${id}`)
}
export async function createTrainingModule(data) {
return request('/api/training-modules', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function updateTrainingModule(id, data) {
return request(`/api/training-modules/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteTrainingModule(id) {
return request(`/api/training-modules/${id}`, { method: 'DELETE' })
}
/** Kopiert Modul-Inhalte ans Ende eines Abschnitts (section_order_index 0-basiert). */
export async function applyTrainingModuleToTrainingUnit(unitId, data) {
return request(`/api/training-units/${unitId}/apply-training-module`, {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function listTrainingFrameworkPrograms() {
return request('/api/training-framework-programs')
}
export async function getTrainingFrameworkProgram(id) {
return request(`/api/training-framework-programs/${id}`)
}
export async function createTrainingFrameworkProgram(data) {
return request('/api/training-framework-programs', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function updateTrainingFrameworkProgram(id, data) {
return request(`/api/training-framework-programs/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteTrainingFrameworkProgram(id) {
return request(`/api/training-framework-programs/${id}`, { method: 'DELETE' })
}

View File

@ -7,8 +7,10 @@
import { stripHtmlToText } from './htmlUtils' import { stripHtmlToText } from './htmlUtils'
import { normalizeAdvanceMode, parseComboRepSeriesCountUi } from './combinationMethodProfileUi' import { normalizeAdvanceMode, parseComboRepSeriesCountUi } from './combinationMethodProfileUi'
import { request, ACTIVE_CLUB_STORAGE_KEY } from '../api/client.js' import { request, ACTIVE_CLUB_STORAGE_KEY } from '../api/client.js'
import * as planning from '../api/planning.js'
export { ACTIVE_CLUB_STORAGE_KEY } export { ACTIVE_CLUB_STORAGE_KEY }
export * from '../api/planning.js'
// ============================================================================ // ============================================================================
// Auth // Auth
@ -1253,176 +1255,6 @@ export async function deleteTrainerContext(id) {
return request(`/api/trainer-contexts/${id}`, { method: 'DELETE' }) return request(`/api/trainer-contexts/${id}`, { method: 'DELETE' })
} }
// ============================================================================
// Training Planning
// ============================================================================
/** Query-Parameter wie GET /api/training-units. */
export async function listTrainingUnits(filters = {}) {
const q = new URLSearchParams()
if (filters.group_id != null && filters.group_id !== '') {
q.set('group_id', String(filters.group_id))
}
if (filters.club_id != null && filters.club_id !== '') {
q.set('club_id', String(filters.club_id))
}
if (filters.start_date) q.set('start_date', filters.start_date)
if (filters.end_date) q.set('end_date', filters.end_date)
if (filters.status) q.set('status', filters.status)
if (filters.debrief_pending === true) q.set('debrief_pending', 'true')
if (filters.assigned_to_me === true) q.set('assigned_to_me', 'true')
if (filters.sort) q.set('sort', String(filters.sort))
if (filters.limit != null && filters.limit !== '') q.set('limit', String(filters.limit))
if (filters.cursor_planned_date) q.set('cursor_planned_date', String(filters.cursor_planned_date))
if (filters.cursor_planned_time != null && filters.cursor_planned_time !== '') {
q.set('cursor_planned_time', String(filters.cursor_planned_time))
}
if (filters.cursor_id != null && filters.cursor_id !== '') q.set('cursor_id', String(filters.cursor_id))
const qs = q.toString()
return request(`/api/training-units${qs ? `?${qs}` : ''}`)
}
/** Dashboard Kurzüberblick: Entwürfe / meine Übungen / YTD abgeschlossene Einheiten (ein Roundtrip). */
export async function getDashboardKpis() {
return request('/api/dashboard/kpis')
}
/** Dashboard: Übungen in geplanten Einheiten, die für den Verein noch auf Sichtbarkeit „Verein“ gehören. */
export async function getTrainingExerciseClubVisibilityQueue(filters = {}) {
const q = new URLSearchParams()
if (filters.start_date) q.set('start_date', String(filters.start_date))
if (filters.end_date) q.set('end_date', String(filters.end_date))
if (filters.assigned_to_me === false) q.set('assigned_to_me', 'false')
if (filters.limit_units != null && filters.limit_units !== '') {
q.set('limit_units', String(filters.limit_units))
}
const qs = q.toString()
return request(`/api/training-units/exercises-club-visibility-queue${qs ? `?${qs}` : ''}`)
}
export async function getTrainingUnit(id) {
return request(`/api/training-units/${id}`)
}
export async function createTrainingUnit(data) {
return request('/api/training-units', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function updateTrainingUnit(id, data) {
return request(`/api/training-units/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
}
export async function deleteTrainingUnit(id) {
return request(`/api/training-units/${id}`, { method: 'DELETE' })
}
export async function quickCreateTrainingUnit(data) {
return request('/api/training-units/quick-create', {
method: 'POST',
body: JSON.stringify(data)
})
}
/** Rahmen-Slot → geplante Einheit (tiefe Kopie, origin_framework_slot_id). */
export async function createTrainingUnitFromFrameworkSlot(data) {
return request('/api/training-units/from-framework-slot', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function listTrainingPlanTemplates() {
return request('/api/training-plan-templates')
}
export async function getTrainingPlanTemplate(id) {
return request(`/api/training-plan-templates/${id}`)
}
export async function createTrainingPlanTemplate(data) {
return request('/api/training-plan-templates', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function updateTrainingPlanTemplate(id, data) {
return request(`/api/training-plan-templates/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
}
export async function deleteTrainingPlanTemplate(id) {
return request(`/api/training-plan-templates/${id}`, { method: 'DELETE' })
}
export async function listTrainingModules() {
return request('/api/training-modules')
}
export async function getTrainingModule(id) {
return request(`/api/training-modules/${id}`)
}
export async function createTrainingModule(data) {
return request('/api/training-modules', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function updateTrainingModule(id, data) {
return request(`/api/training-modules/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteTrainingModule(id) {
return request(`/api/training-modules/${id}`, { method: 'DELETE' })
}
/** Kopiert Modul-Inhalte ans Ende eines Abschnitts (section_order_index 0-basiert). */
export async function applyTrainingModuleToTrainingUnit(unitId, data) {
return request(`/api/training-units/${unitId}/apply-training-module`, {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function listTrainingFrameworkPrograms() {
return request('/api/training-framework-programs')
}
export async function getTrainingFrameworkProgram(id) {
return request(`/api/training-framework-programs/${id}`)
}
export async function createTrainingFrameworkProgram(data) {
return request('/api/training-framework-programs', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function updateTrainingFrameworkProgram(id, data) {
return request(`/api/training-framework-programs/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function deleteTrainingFrameworkProgram(id) {
return request(`/api/training-framework-programs/${id}`, { method: 'DELETE' })
}
// ============================================================================ // ============================================================================
// Version & Health // Version & Health
// ============================================================================ // ============================================================================
@ -1534,32 +1366,8 @@ export const api = {
createExerciseProgressionSequence, createExerciseProgressionSequence,
deleteExerciseProgressionEdgesBatch, deleteExerciseProgressionEdgesBatch,
// Training Planning // Training Planning → frontend/src/api/planning.js
listTrainingUnits, ...planning,
getDashboardKpis,
getTrainingExerciseClubVisibilityQueue,
getTrainingUnit,
createTrainingUnit,
updateTrainingUnit,
deleteTrainingUnit,
quickCreateTrainingUnit,
createTrainingUnitFromFrameworkSlot,
listTrainingPlanTemplates,
getTrainingPlanTemplate,
createTrainingPlanTemplate,
updateTrainingPlanTemplate,
deleteTrainingPlanTemplate,
listTrainingModules,
getTrainingModule,
createTrainingModule,
updateTrainingModule,
deleteTrainingModule,
applyTrainingModuleToTrainingUnit,
listTrainingFrameworkPrograms,
getTrainingFrameworkProgram,
createTrainingFrameworkProgram,
updateTrainingFrameworkProgram,
deleteTrainingFrameworkProgram,
// Catalogs // Catalogs
listFocusAreas, listFocusAreas,