Compare commits

..

237 Commits

Author SHA1 Message Date
90e8f51566 Update Catalog Prompt Slots Router and Access Layer Exemptions
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added documentation to the `catalog_prompt_slots.py` file to clarify its role as a global admin catalog requiring authentication and admin role, without tenant context.
- Updated the `check_access_layer_hints.py` script to include `catalog_prompt_slots.py` in the list of exempt routers, ensuring proper access control for admin functionalities.
2026-06-15 15:27:03 +02:00
7e5ef4561a Refactor Catalog Prompt Slot Management and Enhance Fallback Logic
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced a new function `_resolve_entry_slot_values` to streamline the merging of stored slot values with fallbacks, improving code clarity and maintainability.
- Updated `get_catalog_entry_slots` and `resolve_catalog_prompt_variables` functions to utilize the new fallback logic, enhancing the handling of catalog entries.
- Enhanced the `CatalogPromptSlotsEditor` component to display fallback information for slots, improving user experience in managing catalog prompts.
- Incremented version numbers and updated changelog to reflect the new features and improvements.
2026-06-15 15:18:00 +02:00
53f2b027cc Enhance Planning Catalog Context and Prompt Slot Management
Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m20s
- Introduced new catalog context handling in planning prompt functions, allowing for improved integration of planning variables.
- Added optional catalog context parameters in various functions to streamline the merging of planning prompt variables.
- Updated frontend components to include CatalogPromptSlotsEditor for managing prompt slots across different catalog types.
- Enhanced API utilities to support fetching and updating catalog prompt slots, improving backend functionality for catalog management.
- Incremented version numbers and updated changelog to reflect the new features and improvements.
2026-06-15 12:13:15 +02:00
9cee862c32 Implement Planning Prompt Enhancements and LLM Usage Tracking
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 49s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m26s
- Added new fields for goal query, user notes, max steps, and search query in the AiPromptPreviewBody to support planning prompts.
- Integrated planning prompt handling in the preview_ai_prompt function, allowing for distinct processing of planning and exercise prompts.
- Introduced LLM usage tracking in openrouter_chat_completion and planning_exercise_suggest functions to monitor AI call metrics.
- Updated frontend components to accommodate new input fields for planning prompts, enhancing user experience and functionality.
2026-06-15 07:50:49 +02:00
0b203489f7 Implement Graph Visibility Promotion Logic and Update UI Components
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 41s
Test Suite / playwright-tests (push) Successful in 1m21s
- Added a new function `_graph_promotion_transition` to determine the necessary exercise visibility changes during graph promotions.
- Updated the `list_visibility_promotion_candidates` endpoint to utilize the new promotion logic, ensuring accurate exercise visibility handling.
- Enhanced the frontend components to prompt users for exercise visibility adjustments based on graph visibility changes, improving user experience.
- Introduced tests for the new promotion logic to ensure correctness and reliability in visibility transitions.
2026-06-14 07:30:26 +02:00
1c67a50ce4 Enhance Exercise Progression Graph Panel with Governance Club Management
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 38s
Test Suite / playwright-tests (push) Successful in 1m12s
- Introduced functionality to manage governance clubs for superadmins, allowing for better club selection and organization within the Exercise Progression Graph Panel.
- Implemented state management for clubs, including sorting and filtering options, to improve user experience and accessibility.
- Enhanced the useEffect hook to fetch governance clubs dynamically, ensuring up-to-date club information is available for selection.
- Updated the club selection dropdown to categorize clubs into "My Clubs" and "Other Clubs," improving clarity and usability for users.
2026-06-14 07:19:35 +02:00
87d9fa9b65 Enhance Exercise Progression Graph Panel with Club Management Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 43s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added functionality to select and manage clubs within the Exercise Progression Graph Panel, allowing users to assign clubs to exercises.
- Introduced state management for club selection and manual entry, improving user experience for platform admins.
- Updated visibility handling to ensure proper governance and club association during exercise promotion.
- Enhanced error handling to provide clearer feedback when no club is selected, ensuring users are guided to make necessary selections.
2026-06-14 07:10:11 +02:00
4b9374765b Enhance Progression Graph Management with F15 Features and Evaluation Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s
- Updated `PROJECT_STATUS.md` to reflect the implementation of F15 features, including the unified slot review and handling of `findings_stale`.
- Enhanced `PROGRESSION_GRAPH_SLOT_EDITOR_SPEC.md` with detailed descriptions of new functionalities related to the match dialog and path quality assessments.
- Introduced new functions in `exercise_progression_graphs.py` to validate exercise visibility against progression graph settings, ensuring proper governance.
- Improved frontend components to support new governance parameters (visibility and club_id) in exercise creation workflows.
- Updated documentation in `HANDOVER.md` and `PLANNING_KI_ROADMAP.md` to outline the latest developments and validation results for the F15 features.
- Enhanced utility functions for exercise creation to incorporate governance settings, improving the overall user experience in the path builder and editor.
2026-06-14 06:44:12 +02:00
b629f192ac Refactor ProgressionSlotCard Key Prop for Improved Stability
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m39s
- Updated the key prop in the ProgressionSlotCard component to use a simpler index-based key, enhancing the stability of component rendering during updates.
- This change aims to prevent potential issues with component re-renders and improve overall performance in the ProgressionGraphEditor.
2026-06-13 17:33:36 +02:00
313d613b7c Enhance Path Quality Assessment and Slot Management Features
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added new functions for computing assignment quality scores and counting step assignment statistics, improving the evaluation of steps in path quality assessments.
- Updated existing methods to incorporate the new scoring logic, enhancing the robustness of path evaluations.
- Introduced UI components in the frontend to display detailed quality assessment results, including handling of split dimensions in path evaluations.
- Enhanced tests to cover new functionalities and ensure accuracy in quality scoring and slot management processes.
2026-06-13 17:13:35 +02:00
7265cd5a01 Add findings_stale field to GraphPlanningRoadmapArtifact and update ProgressionGraphEditor for state management
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m34s
- Introduced `findings_stale` field in `GraphPlanningRoadmapArtifact` to track the freshness of findings.
- Updated `ProgressionGraphEditor` to manage `findingsStale` state across various functions, ensuring accurate representation of evaluation status.
- Modified related utility functions and tests to accommodate the new state, enhancing overall functionality and user feedback in the progression graph management process.
2026-06-13 16:29:17 +02:00
5e5f4ca8d4 Enhance Progression Findings and Graph Editor with Evaluation Staleness Handling
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Failing after 2s
Test Suite / playwright-tests (push) Successful in 1m19s
- Added `evaluationStale` state to `ProgressionGraphEditor` and `ProgressionFindingsPanel` to track the freshness of evaluations.
- Updated UI to display a warning when evaluations are stale, prompting users to re-evaluate the graph.
- Modified loading and evaluation functions to manage the `evaluationStale` state effectively, ensuring accurate user feedback during the evaluation process.
- Improved user notifications regarding the need for re-evaluation after changes to the graph.
2026-06-13 16:23:04 +02:00
f0e581a9f5 Implement Off-Topic Slot Gap Specification and Unified Slot Review Enhancements
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m34s
- Introduced `_build_off_topic_slot_gap_spec` to generate specifications for off-topic slots, improving the handling of filled but thematically inappropriate slots.
- Added `_build_unified_slot_review_entry` to streamline the review process for slots, incorporating various parameters for better evaluation and suggestions.
- Enhanced existing logic in slot management to improve the robustness of path evaluations and user feedback.
- Added tests for the new off-topic slot gap specification to ensure functionality and correctness.
2026-06-13 12:43:59 +02:00
cd457e3ea0 Enhance Slot Evaluation and Scoring Mechanisms
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m26s
- Introduced new functions `_off_topic_semantic_scores_by_slot` and `_score_exercise_stage_fit_for_spec` to improve the evaluation of off-topic steps and exercise stage fit, enhancing the quality assessment process.
- Updated `_run_unified_slot_improvement_review` to incorporate off-topic scores and exercise stage fit scoring, refining the decision-making process for slot suggestions.
- Enhanced existing logic to streamline the handling of slot scores and improve the overall robustness of slot management in path evaluations.
2026-06-13 12:33:16 +02:00
e9bf5bd1a5 Enhance Path Evaluation and Slot Management Features
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `_parse_slot_refs_from_text` to extract and convert slot references from text, improving the handling of user input in path evaluations.
- Updated `_problematic_slots_from_path_qa` to utilize the new parsing function, enhancing the identification of problematic slots based on various hints and issues.
- Enhanced `ProgressionGraphEditor` and `ProgressionOptimizeCompareModal` to better display identified problem slots and their associated reasons, improving user feedback during evaluations.
- Added tests for new parsing functionality and its integration with existing slot management processes, ensuring robustness in slot reference handling.
2026-06-13 12:17:58 +02:00
3468b2066e Enhance Path QA and Progression Review Logic
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 41s
Test Suite / playwright-tests (push) Successful in 1m27s
- Introduced `_resolve_hint_major_index` to accurately map hints to major step indices, improving the handling of optimization hints in path evaluations.
- Added `_problematic_slots_from_path_qa` to identify and categorize problematic slots based on baseline QA, enhancing the quality assessment process.
- Updated `_slot_suggestion_accepted` to incorporate new parameters for slot problems and stage specifications, refining the decision-making process for slot suggestions.
- Enhanced `ProgressionGraphEditor` to improve user notifications regarding identified issues and suggestions, ensuring clearer communication of path evaluation results.
- Modified `buildProgressionComparePayload` and `buildUnifiedSlotReviewComparePayload` to support baseline evaluations, streamlining the comparison process for proposed paths.
2026-06-13 10:39:52 +02:00
a1e4ad66df Implement Quick Evaluation and Quality Scoring for Path QA
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m11s
- Added `_quick_evaluate_steps_qa` function to streamline path quality assessment without recursive API calls, enhancing performance for slot comparisons.
- Introduced `compute_deterministic_path_quality_score` to provide a heuristic quality score based on gaps and off-topic steps, improving evaluation accuracy.
- Updated `_run_unified_slot_improvement_review` to utilize the new quick evaluation method, optimizing the review process and integrating quality scoring.
- Enhanced `build_path_qa_summary` to include quality score calculations, ensuring comprehensive feedback on path evaluations.
- Refactored related functions for improved clarity and efficiency in handling path quality assessments.
2026-06-13 10:27:07 +02:00
85fccdd093 Enhance Progression Path Comparison and Slot Evaluation Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s
- Introduced new fields in `ProgressionPathSuggestRequest` for baseline evaluation and incremental scoring, improving the assessment of proposed paths.
- Implemented `_apply_slot_diff_to_steps` and `_score_incremental_slot_diffs` functions to manage slot differences and evaluate their impact on quality scores.
- Updated `ProgressionGraphEditor` to streamline the match comparison flow, integrating new evaluation parameters and improving user notifications.
- Enhanced `ProgressionOptimizeCompareModal` to better display proposed path suggestions, including pro/con evaluations and quality delta metrics.
- Refactored utility functions for clearer handling of slot differences and improved overall data management in the progression graph editor.
2026-06-13 10:11:10 +02:00
19bbcdaf50 Refactor Progression Comparison Logic and Enhance UI Components
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m19s
- Introduced new utility functions for comparing slot differences, including `compareDiffKind`, `annotateCompareDiffKinds`, and various filtering functions to streamline the comparison process.
- Updated `ProgressionGraphEditor` to utilize the new comparison logic, improving the handling of slot differences and user notifications.
- Enhanced `ProgressionOptimizeCompareModal` to better manage proposed path suggestions, including clearer messaging and improved selection handling for optional replacements.
- Adjusted frontend components to reflect changes in comparison logic, ensuring a more intuitive user experience in managing progression paths.
2026-06-13 09:02:15 +02:00
cec96ae473 Implement Progression Comparison Logic and Refactor Fetching Methods
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `buildProgressionComparePayload` to create a structured comparison response from baseline and proposed evaluation results, enhancing clarity in slot differences.
- Refactored `fetchMatchCompare` to `fetchFullMatch` for improved clarity and functionality in fetching progression paths.
- Updated `runMatchCompareFlow` to streamline the evaluation process, integrating baseline and match results for a comprehensive comparison.
- Enhanced utility functions for managing slot differences and gap fill offers, improving overall data handling in the progression graph editor.
- Adjusted frontend components to reflect these changes, ensuring a more intuitive user experience in managing progression paths.
2026-06-13 08:46:10 +02:00
53f1c7161f Refactor AI Gap Fill and Progression Path Evaluation Logic
Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Has been cancelled
- Removed the `try_suggest_ai_stage_step` function from `_enrich_roadmap_unfilled_gap_offers`, simplifying the gap fill offer generation process.
- Updated `_run_evaluate_only_path_qa` and `suggest_progression_path` to disable AI calls and proposals, enhancing control over evaluation parameters.
- Adjusted `ProgressionGraphEditor` to reflect changes in API requests, ensuring consistent handling of evaluation data.
- Added a new test to validate the behavior of proposed QA when no slot differences are present, improving test coverage for comparison logic.
2026-06-13 08:43:02 +02:00
89c6780294 Enhance AI Gap Fill Logic and Progression Path Handling
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Integrated `try_suggest_ai_stage_step` to suggest AI-generated gap fill steps based on user input, improving the automation of the planning process.
- Updated `_enrich_roadmap_unfilled_gap_offers` to conditionally include AI gap fill proposals, enhancing the offer generation logic.
- Implemented `_merge_gap_fill_offers_from_steps` to consolidate gap fill offers from various steps, ensuring a comprehensive list of available offers.
- Modified `ProgressionGraphEditor` to utilize the new merging logic for gap fill offers, improving the user experience in managing offers.
- Enhanced utility functions to streamline the collection and filtering of gap fill offers from API responses.
- Bumped version to reflect the new features and improvements.
2026-06-13 08:36:53 +02:00
3f130aa8ad Refactor Progression Path Evaluation and Comparison Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Updated `suggest_progression_path` to utilize `evaluate_steps` for improved validation, ensuring at least one evaluation step is provided.
- Modified frontend components to enhance user experience in the comparison process, including clearer messaging and improved dialog handling.
- Adjusted `ProgressionGraphEditor` to streamline the comparison flow and integrate new evaluation parameters.
- Enhanced `ProgressionOptimizeCompareModal` to reflect changes in comparison logic, allowing for better user interaction with proposed path suggestions.
- Bumped version to reflect the new features and improvements.
2026-06-13 08:17:59 +02:00
69ce3f6975 Enhance Rematch Suggestion Logic and Progression Path Evaluation
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced `_baseline_slot_accepts_rematch_suggestion` to filter out filled or invalid slots from rematch suggestions, improving the accuracy of rematch logic.
- Updated `_build_rematch_suggestion_diffs` to skip non-eligible baseline slots, streamlining the rematch suggestion process.
- Added `_evaluate_steps_for_compare_qa` to evaluate steps against the current state, enhancing the quality assessment during progression path suggestions.
- Modified `_build_progression_compare_response` to ensure proper handling of slot differences and quality scores, improving response clarity.
- Updated frontend components to reflect changes in rematch handling and evaluation logic.
- Bumped version to reflect the new features and improvements.
2026-06-13 08:02:44 +02:00
dccb065181 Enhance Slot Difference Annotation and Rematch Suggestion Logic
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `_annotate_slot_diffs` to mark trivial ID swaps in slot differences, improving clarity in comparison results.
- Added `_actionable_slot_diffs` to filter out non-actionable differences, streamlining the evaluation process.
- Implemented `_build_rematch_suggestion_diffs` to generate suggestions based on rematch logs, enhancing the path optimization workflow.
- Updated `_build_progression_compare_response` to incorporate actionable slot differences and rematch suggestions, improving the response structure.
- Enhanced frontend components to display rematch suggestions and handle trivial differences more effectively.
- Bumped version to reflect the new features and improvements.
2026-06-13 07:55:47 +02:00
e828a5da32 Enhance Progression Path Evaluation and Comparison Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 38s
Test Suite / playwright-tests (push) Successful in 1m23s
- Introduced `_steps_to_evaluate_payloads` to convert path steps into evaluation payloads for improved quality assessments.
- Updated `_build_progression_compare_response` to include a new `proposed_eval` parameter, allowing for fair quality assessment comparisons.
- Enhanced `ProgressionGraphEditor` to utilize the new pipeline quality assessment data.
- Modified `ProgressionOptimizeCompareModal` to display detailed comparison results, including handling of trivial slot differences and optimization hints.
- Bumped version to reflect the new features and improvements.
2026-06-13 07:44:01 +02:00
5bca5ef9eb Enhance Progression Path Evaluation and Optimization Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
- Updated `suggest_progression_path` to include additional evaluation parameters, allowing for more comprehensive path assessments.
- Introduced `PathQaPipelineDetails` component to display detailed quality assessment metrics, including rematch and refine logs, in the frontend.
- Enhanced `ProgressionGraphEditor` to manage proposed path evaluations and integrate quality assessment results into the draft workflow.
- Improved `ProgressionOptimizeCompareModal` to present optimization hints and quality tier information for proposed paths.
- Bumped version to reflect the new features and improvements.
2026-06-12 13:33:36 +02:00
5ed06002d9 Implement Comparison Logic for Progression Path Suggestions
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m18s
- Added `compare_with_assignments` flag to `ProgressionPathSuggestRequest` to enable comparison of proposed paths with existing slot assignments.
- Introduced `_assignment_preservation_active` function to determine if existing assignments should be preserved during path suggestions.
- Enhanced `suggest_progression_path` to handle comparison logic, including validation for minimum slot assignments required for comparison.
- Implemented `_build_progression_compare_response` to structure the response for comparison results, including slot differences and quality scores.
- Updated frontend components to support new comparison features, including handling of slot assignments and optimization comparisons.
- Bumped version to reflect the new features and improvements.
2026-06-12 13:22:04 +02:00
b8f65e04c5 Enhance Rematch Logic and Slot Filtering in Planning Path
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `filter_rematch_slot_indices` to exclude preserved slots from rematching, improving the accuracy of slot assignments.
- Added `_slot_priority_for_rematch` to prioritize existing slot assignments during rematching, enhancing the robustness of the rematch process.
- Updated `_run_roadmap_rematch_loop` to utilize the new filtering and prioritization logic, ensuring better handling of rematch scenarios.
- Enhanced tests in `test_planning_path_rematch.py` to validate the new filtering behavior and ensure correct exercise restoration when not rejected.
- Bumped version to reflect the new features and improvements.
2026-06-12 12:33:00 +02:00
f3710ac0a1 Enhance Planning Catalog Context Integration in Progression Path
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Updated `PROJECT_STATUS.md` to reflect the addition of the Planning AI Progression Graph and its context in the roadmap.
- Enhanced `DOMAIN_MODEL.md` with details on the new `planning_catalog_context` features, allowing trainers to manage curriculum stages and context.
- Added tests in `test_planning_catalog_context.py` to validate the separation of LLM highlights from fix hints during QA processes.
- Updated `HANDOVER.md` and `PLANNING_KI_ROADMAP.md` to reflect the latest app version and improvements in the planning context.
- Enhanced frontend components to support the new planning catalog context, including updates to `ExerciseProgressionPathBuilder` and `ProgressionGraphEditor`.
- Bumped version to 0.8.233 to reflect the new features and improvements.
2026-06-12 12:25:52 +02:00
6ab2f20f08 Enhance Progression Path Suggestion with Planning Catalog Context Integration
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m16s
- Introduced `planning_catalog_context` to `ProgressionPathSuggestRequest` for improved handling of catalog-related data during path suggestions.
- Implemented `_resolve_planning_catalog_context` to retrieve and validate the planning catalog context, enhancing the robustness of the suggestion process.
- Updated `_build_path_target_profile` to incorporate catalog context, enriching target profiles with relevant planning data.
- Enhanced frontend components in `ProgressionGraphEditor` to manage and display planning catalog context, including new selection options for focus areas, style directions, training types, and target groups.
- Added utility functions for parsing and transforming planning catalog context data for API interactions.
- Bumped version to 0.8.233 to reflect the new features and improvements.
2026-06-12 10:16:55 +02:00
a4e73c830f Implement Pruning of Filled Steps from Roadmap Unfilled
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 22s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `_prune_filled_from_roadmap_unfilled` function to remove steps with filled exercises from the unfilled roadmap, preventing outdated references.
- Updated `_run_roadmap_rematch_loop` to incorporate the new pruning logic, ensuring only relevant unfilled steps are retained during rematching.
- Added tests for the pruning function to validate its behavior with various step scenarios.
- Bumped version to 0.8.232 to reflect the new functionality and improvements.
2026-06-12 08:27:39 +02:00
63c99b0ec5 Enhance Roadmap Slot Matching and Gap Offer Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced `_roadmap_step_passes_post_match_gate` to validate steps after matching, ensuring only relevant steps proceed.
- Enhanced `_enrich_roadmap_unfilled_gap_offers` to generate AI gap offers for unfilled roadmap slots, improving exercise suggestions.
- Updated `suggest_progression_path` to incorporate new gap offer logic and streamline the handling of roadmap steps.
- Refined frontend logic in `applyMatchStepsToSlots` to better manage step assignments and improve clarity in slot handling.
- Bumped version to 0.8.231 to reflect the new features and improvements.
2026-06-12 08:05:56 +02:00
d448c3191f Enhance Stage Mismatch Handling and Roadmap Slot Purging
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced `_purge_stage_mismatch_roadmap_slots` to clear slots with persistent stage mismatches, improving the relevance of exercise suggestions.
- Updated `collect_gap_fill_specs` to handle stage mismatch issues more effectively, providing clearer rationale and title hints for off-topic exercises.
- Modified `_filter_learning_goal_candidate_ids` to enforce stricter filtering criteria, ensuring only relevant candidates are considered.
- Enhanced `rematch_roadmap_slots` to incorporate slot assignment history, preventing conflicts with previously assigned exercises.
- Bumped version to 0.8.230 to reflect the new features and improvements.
2026-06-12 07:57:19 +02:00
8a4be795f4 Implement Peer Learning Goals and Stage Fit Enhancements
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m33s
- Introduced `_peer_stage_learning_goals` to retrieve learning goals from peer stages, enhancing the ability to filter exercises based on cross-slot collisions.
- Added `_filter_learning_goal_candidate_ids` to refine candidate selection by incorporating peer learning goals and stage fit criteria, improving exercise relevance in suggestions.
- Enhanced `pick_best_path_hit` and `_match_roadmap_slot` to utilize peer learning goals for better exercise selection and to prevent conflicts with titles from other stages.
- Updated `stage_refinement_criteria_from_learning_goal` to provide clearer criteria for stage refinement based on learning goals.
- Bumped version to 0.8.229 to reflect the new features and improvements.
2026-06-12 07:40:26 +02:00
a49987408b Enhance Stage Specification Refinement and Rematch Logic
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Updated `max_rematch_rounds` in `ProgressionPathSuggestRequest` to allow for a maximum of 3 rounds, improving flexibility in rematch processes.
- Introduced `_track_rejected` function to track rejected exercises by major step index, enhancing the rematch logic to account for previously rejected exercises.
- Enhanced `_run_roadmap_rematch_loop` to utilize the new rejection tracking, ensuring better handling of off-topic steps during rematching.
- Improved `detect_off_topic_steps` to incorporate refined scoring and reasoning for stage fit, enhancing the accuracy of off-topic detection.
- Updated `refine_stage_spec_artifact` to merge stage exclusion phrases more effectively, improving the clarity of anti-pattern handling.
- Bumped version to 0.8.228 to reflect the new features and improvements.
2026-06-11 22:11:31 +02:00
f36a747efa Enhance Progression Path Suggestion with Stage Specification Refinement
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
- Introduced `auto_refine_stage_spec` to `ProgressionPathSuggestRequest`, enabling optional refinement of stage specifications during the rematch process.
- Updated `_run_roadmap_rematch_loop` to incorporate stage specification refinements, logging changes for better tracking of adjustments made during rematching.
- Enhanced `suggest_progression_path` to include refine logs in the output, providing clearer insights into the refinement process.
- Added utility functions for formatting refine log entries, improving the display of refinement actions in the frontend components.
- Updated frontend components to display refine logs, enhancing user feedback on stage specification adjustments during progression analysis.
- Bumped version to 0.8.227 to reflect the new features and improvements.
2026-06-11 21:43:45 +02:00
de9fdf3ac0 Enhance Progression Findings Panel with Rematch and Optimization Hints
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added support for displaying optimization hints and rematch logs in the Progression Findings Panel, improving user feedback on potential enhancements.
- Introduced new utility functions for formatting rematch log entries and resolving hint slot indices, enhancing clarity in displayed information.
- Updated the Progression Graph Editor to handle rematch actions and display relevant match summaries, ensuring comprehensive insights during progression analysis.
- Enhanced the utility functions to support the new features, ensuring robust handling of optimization actions and rematch logic.
2026-06-11 21:39:16 +02:00
df93da9a03 Enhance Gap Fill and Rematch Logic in Progression Path
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 39s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced `_step_neighbors_at_index` to safely retrieve neighboring steps without causing IndexErrors, improving robustness in gap fill specifications.
- Updated `collect_gap_fill_specs` to utilize the new neighbor retrieval function, ensuring safe access to adjacent steps during gap fill processing.
- Enhanced rematch logic in `_run_roadmap_rematch_loop` to incorporate `max_rematch_rounds`, allowing for controlled iterations during roadmap rematching.
- Improved handling of unfilled roadmap slots in `collect_rematch_slot_indices`, ensuring accurate identification of gaps in the progression path.
- Added tests to validate the new gap fill handling and rematch logic, ensuring reliability in path suggestion features.
2026-06-11 21:20:47 +02:00
de939481ba Enhance Gap Fill Offer Handling and Progression Path Logic
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m24s
- Updated `suggest_progression_path` to ensure unique gap fill offers are collected and added based on their IDs, improving the relevance of suggestions.
- Refined the logic for setting `slot_status` and handling `gap_offer` and `proposal_key` in steps, enhancing clarity in progression path management.
- Improved the `collectGapOffersFromApiResponse` function to consolidate gap offers from various sources, ensuring comprehensive offer retrieval.
- Enhanced the handling of unfilled slots in `applyMatchStepsToSlots`, ensuring proper assignment of proposals and gap offers.
- Added tests to validate the new logic for gap fill offers and slot assignments, ensuring robustness in path suggestion features.
2026-06-11 13:13:46 +02:00
6d130a7e09 Implement Learning Goal Candidate Retrieval and Roadmap Fallback Logic
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Added `_safe_tsquery_fragment` to sanitize learning goal input for SQL queries, improving query safety.
- Introduced `_fetch_learning_goal_library_candidate_ids` to retrieve exercise IDs matching learning goals, enhancing exercise relevance in roadmap suggestions.
- Enhanced `_match_roadmap_slot` to utilize learning goal candidates, improving the accuracy of supplemental exercise selection.
- Implemented `_pick_roadmap_rank_fallback` to provide a fallback mechanism for selecting the best exercise when strict matching fails, ensuring better exercise retrieval.
- Updated tests to validate the new learning goal retrieval and fallback logic, ensuring robustness in exercise selection processes.
2026-06-11 12:54:07 +02:00
b2fbf6b4af Refactor Roadmap Step Annotation and Slot Assignment Logic
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s
- Updated `_annotate_roadmap_step` to change the condition for setting `slot_status` based on `roadmap_match_source`, improving clarity in slot assignment handling.
- Removed the `_try_reconcile_slot_assignment` function to streamline the slot assignment process, as its logic is now integrated into the main flow.
- Enhanced `_match_roadmap_slot` to conditionally preserve slot assignments based on exercise ID, ensuring better handling of existing assignments.
- Improved the handling of semantic scores in `rank_visible_library_hits` to prioritize the best semantic fit, enhancing exercise retrieval accuracy.
- Added tests to validate the new logic for title equivalence and semantic scoring, ensuring robustness in exercise selection processes.
2026-06-11 12:45:53 +02:00
ca2adbd55e Enhance Exercise Retrieval and Path Handling Logic
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 48s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m23s
- Introduced new functions for handling exercise visibility and retrieval based on progression graph context, including `fetch_exercise_rows_by_ids_for_graph`.
- Updated `_load_supplemental_exercise_rows` to incorporate graph visibility rules, improving the accuracy of exercise retrieval.
- Enhanced `_run_path_step_retrieval` to utilize preloaded supplemental exercise rows, optimizing performance and clarity in path step processing.
- Added `exercise_title_equivalent_to_stage_goal` function to improve title matching against learning goals, enhancing exercise relevance.
- Updated tests to validate new retrieval logic and title equivalence functionality, ensuring robustness in exercise selection processes.
2026-06-11 12:33:02 +02:00
ad051c015f Enhance Progression Path Suggestion with Retrieval Boost and Slot Assignment Logic
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added `preserve_slot_assignments` and `retrieval_boost_exercise_ids` to `ProgressionPathSuggestRequest` for improved handling of exercise suggestions.
- Refactored `_supplemental_exercise_ids_from_body` to incorporate retrieval boost exercise IDs, ensuring they are prioritized over slot assignments.
- Updated `_build_steps_roadmap_first` to conditionally preserve slot assignments based on the new flag.
- Enhanced tests to validate the new retrieval boost logic and its integration with existing slot assignment handling.
2026-06-11 12:20:41 +02:00
b464047c3a Enhance Exercise Progression Graph Functionality and Visibility Logic
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
- Introduced new functions for handling exercise visibility in progression graphs, including `library_content_visibility_for_progression_graph_sql` to manage visibility based on graph context.
- Added `_supplemental_exercise_ids_from_body` to extract exercise IDs from request bodies, improving data handling in path suggestions.
- Implemented visibility promotion candidate retrieval in the API, allowing for the identification of private exercises that need visibility adjustments when promoting graph visibility.
- Enhanced existing SQL queries and retrieval functions to incorporate new visibility logic, ensuring accurate exercise visibility based on user roles and graph settings.
- Updated frontend components to support visibility promotion workflows, including user prompts for managing private exercises during graph visibility changes.
- Added tests to validate new visibility logic and ensure robustness in exercise retrieval and promotion processes.
2026-06-11 12:10:46 +02:00
7203c871fc Add Slot Assignments and Enhance Path Handling Logic
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced `slot_assignments` to `ProgressionPathSuggestRequest` for improved handling of existing slot assignments in path building.
- Implemented `_slot_assignments_by_major_index` and `_path_step_from_slot_assignment` functions to facilitate the integration of slot assignments into the path generation process.
- Updated `_build_steps_roadmap_first` to utilize slot assignments, enhancing the accuracy of path steps based on existing exercise slots.
- Enhanced `detect_path_gaps` to skip empty slots, preventing unnecessary errors during gap detection.
- Added tests to validate the new slot assignment handling and ensure robustness in path generation logic.
2026-06-11 12:02:04 +02:00
480890d0c6 Update Dockerfile and requirements for improved dependency management
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
- Added `tzdata` installation in the Dockerfile to support time zone handling in Linux environments.
- Increased `PIP_DEFAULT_TIMEOUT` and added retry logic for pip installations to enhance reliability during dependency installation.
- Updated `requirements.txt` to conditionally include `tzdata` for Windows platforms, ensuring compatibility across different operating systems.
2026-06-11 11:48:25 +02:00
8f1dad53ab Enhance Progression Path Suggestion Logic and UI Feedback
All checks were successful
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m18s
Deploy Development / deploy (push) Successful in 44s
- Updated `suggest_progression_path` to include AI-generated gap fill offers when exercises are missing, improving the relevance of suggested paths.
- Introduced a match summary to provide insights on library matches and gap fill offers, enhancing user feedback in the `ProgressionGraphEditor`.
- Refined the `pick_best_path_hit` function to ensure proper handling of roadmap stage matches based on primary topics.
- Added tests to validate the new gap fill offer logic and match summary functionality, ensuring robustness in path suggestion features.
2026-06-11 11:17:53 +02:00
044ce2ee60 Implement Primary Topic Resolution in Path Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced `resolve_path_primary_topic` function to enhance the determination of primary topics from goal queries and semantic briefs, improving exercise relevance.
- Updated `_match_roadmap_slot` and `detect_off_topic_steps` functions to utilize the new primary topic resolution logic, ensuring accurate topic identification.
- Enhanced tests to validate the functionality of primary topic resolution and its impact on exercise selection and off-topic detection.
- Improved handling of primary topics in the `ExerciseProgressionPathBuilder` and related components for better integration with the overall path-building process.
2026-06-11 11:06:38 +02:00
f63b09fc9c Refine Technique Path Scope Logic and Enhance Test Coverage
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m24s
- Updated the `exercise_passes_technique_path_scope` function to clarify the requirements for technique inclusion, ensuring that the primary technique must appear in the exercise text.
- Enhanced the logic to allow for relaxed matching based on parts of the primary topic, improving flexibility in exercise validation.
- Added new tests to validate the rejection of off-topic exercises, specifically addressing cases where only stage goals mention the primary technique.
- Improved the selection logic in `pick_best_path_hit` to ensure proper handling of roadmap stage matches.
2026-06-11 10:48:36 +02:00
713a344d17 Enhance Roadmap Step Handling and Off-Topic Logic
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 52s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 41s
Test Suite / playwright-tests (push) Successful in 1m51s
- Improved off-topic step handling by incorporating roadmap major step indices for better indexing and detection.
- Refactored `collect_gap_fill_specs` to streamline the insertion logic for off-topic steps, ensuring correct placement based on major step indices.
- Introduced `_normalize_roadmap_steps_coverage` function to standardize roadmap steps coverage, enhancing the handling of missing slots.
- Added `prune_stripped_after_rematch` function to clean up stripped off-topic steps after rematching, improving the overall rematching process.
- Updated tests to validate new rematching and off-topic handling features, ensuring robustness against edge cases.
- Incremented application version to reflect these updates.
2026-06-11 10:40:25 +02:00
1d94c2ebf1 Enhance Roadmap Slot Matching and Off-Topic Detection
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 36s
Test Suite / playwright-tests (push) Successful in 1m23s
- Introduced `auto_rematch_after_qa` parameter in `ProgressionPathSuggestRequest` to enable automatic rematching after quality assurance checks.
- Refactored roadmap slot matching logic to improve clarity and functionality, renaming `_build_steps_roadmap_first` to `_match_roadmap_slot`.
- Added `_with_roadmap_major_index` utility to streamline off-topic step detection by incorporating roadmap major step indices.
- Enhanced off-topic detection logic to utilize the new utility for improved clarity in identifying mismatches and exclusions.
- Incremented application version to reflect these updates.
2026-06-11 10:30:48 +02:00
a152218c45 Enhance Path QA and Stage Matching Logic
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced multistage path quality assurance (QA) functionality to improve exercise relevance and feedback through structured tiers and optimization hints.
- Updated stage specifications to include `start_state` and `target_state` for better contextualization in roadmap matching.
- Enhanced semantic brief construction with technique sibling exclusions to refine exercise selection based on primary topics.
- Improved path retrieval logic to incorporate new parameters for nuanced matching against learning goals.
- Incremented application version to reflect these updates.
2026-06-11 10:19:58 +02:00
4ef3f00e6b Enhance Planning Intent Context and Stage Specification Finalization
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `intent_context` and `semantic_brief` parameters in `try_llm_stage_specs` to improve context handling for stage specifications.
- Updated `build_goal_analysis` to extract explicit exclusions from goal queries, enhancing constraint management.
- Enhanced `roadmap_context_from_override` to enrich semantic briefs with path constraints and finalize stage specifications with intent context.
- Incremented application version to reflect these updates.
2026-06-11 08:47:26 +02:00
3c12363b8f Enhance Path Exclusion Logic and Semantic Brief Enrichment
Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Has been cancelled
- Introduced `resolve_path_anti_patterns` to improve handling of path exclusions based on explicit negations and semantic briefs.
- Updated `enrich_brief_with_path_constraints` to incorporate path-specific exclusions into semantic briefs, enhancing exercise relevance.
- Modified roadmap step annotation to allow for anti-pattern overrides, improving flexibility in exercise selection.
- Enhanced tests to validate new path exclusion features and ensure correct functionality against learning goals.
- Incremented application version to reflect these updates.
2026-06-11 08:43:59 +02:00
07e147bc76 Enhance Stage Matching and Retrieval Logic in Planning Exercise
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m16s
- Introduced `build_stage_match_brief` to create stage-specific semantic briefs, improving roadmap matching accuracy.
- Updated path retrieval logic to differentiate between general and stage-specific semantic weights, enhancing exercise relevance.
- Added support for anti-patterns and success criteria in stage matching, allowing for more nuanced exercise selection.
- Enhanced tests to validate new stage matching features and ensure correct functionality against learning goals.
- Incremented application version to reflect these updates.
2026-06-10 17:02:21 +02:00
18547613ea Implement Stage Learning Goal Features in Planning Exercise
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added `semantic_brief_for_stage` function to enhance semantic briefs with stage learning goals for improved roadmap matching.
- Introduced `exercise_passes_stage_learning_goal_gate` to validate exercises against stage learning goals, enhancing relevance checks.
- Updated path retrieval and scoring logic to incorporate stage learning goals, allowing for more nuanced exercise selection.
- Enhanced UI to indicate weak matches with stage learning goals, improving user feedback on exercise relevance.
- Incremented application version to reflect these updates.
2026-06-10 16:39:17 +02:00
48d51c07c5 Enhance Exercise Progression Graph Panel and Editor with New Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m26s
- Refactored `ExerciseProgressionGraphPanel` to support a create dialog for new progression graphs, improving user experience.
- Integrated `ProgressionGraphListCard` for better visualization of existing graphs and streamlined management.
- Updated `ProgressionGraphEditor` to handle start/target analysis and improved draft hydration with AI suggestions.
- Added utility functions for managing structured responses from AI, enhancing the planning process.
- Incremented application version to reflect these updates.
2026-06-10 16:17:40 +02:00
3b483346de Enhance Progression Graph Editor with Skills Catalog and AI Draft Handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s
- Introduced skills catalog management in the `ProgressionGraphEditor`, allowing for improved context in AI suggestions.
- Updated the loading mechanism to fetch both focus areas and skills catalog concurrently, enhancing performance.
- Implemented `ensureQuickCreateDraftFromAiSuggestion` utility to streamline the creation of drafts from AI suggestions.
- Enhanced slot management by integrating AI context into the gap fill preparation process, improving user experience.
- Incremented application version to reflect these updates.
2026-06-10 16:04:15 +02:00
e0ddfa6ce5 Add AI Suggestion Handling for Roadmap Gaps and Enhance Progression Graph Components
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Implemented functions to resolve neighboring steps based on major indices and build AI context for unfilled roadmap stages.
- Enhanced `try_suggest_ai_stage_step` to generate AI proposals for empty roadmap stages, improving user experience in gap filling.
- Updated `build_gap_fill_offer` to utilize major step neighbors for better context in offers related to unfilled slots.
- Added tests to ensure correct functionality of AI suggestion handling in the context of roadmap gaps.
- Incremented application version to reflect these updates.
2026-06-10 15:56:30 +02:00
ee22b22970 Refactor Progression Graph Components and Consolidate UI
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m15s
- Updated the `ProgressionGraphSlotEditorSpec.md` to reflect UI consolidation, removing separate editors and integrating functionalities into `ExerciseProgressionGraphPanel`.
- Refactored `ExerciseProgressionGraphPanel` to streamline the editing experience, removing unused state and logic for better performance.
- Enhanced `ProgressionGraphEditor` to support embedded usage and trigger callbacks on save, improving integration with other components.
- Simplified `ProgressionGraphEditPage` to redirect users to the exercises list with deep-linking support for selected graphs.
- Incremented application version to reflect these updates.
2026-06-10 15:42:29 +02:00
c1bf9279ad Add Gap Offer Handling and UI Enhancements in Progression Graph Components
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
- Implemented `_build_evaluate_empty_slot_gap_specs` function to generate gap offer specifications for unfilled roadmap slots in evaluate-only mode.
- Enhanced `ProgressionFindingsPanel` to display AI offers for empty slots and gaps, improving user interaction and clarity.
- Updated `ProgressionGraphEditor` and `ProgressionSlotCard` components to support new functionalities for managing slots and offers.
- Refactored utility functions in `progressionGraphDraft.js` to streamline slot management and offer handling.
- Incremented application version to reflect these updates.
2026-06-10 15:34:37 +02:00
97efe66306 Implement EvaluateStepPayload and SlotContentEntry for Enhanced Planning Features
Some checks failed
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Failing after 45s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s
- Introduced EvaluateStepPayload class to facilitate evaluation of exercise steps with optional attributes for AI proposals and roadmap details.
- Added SlotContentEntry and SlotExerciseContent classes to manage exercise content within the progression graph planning artifact.
- Updated GraphPlanningRoadmapArtifact to include new slot contents and last findings attributes for improved data handling.
- Enhanced Exercise Progression Graph Panel with links to the new Slot Editor for streamlined editing of progression graphs.
- Incremented application version to reflect these updates.
2026-06-10 13:05:49 +02:00
8d5f0b533c Enhance Exercise Progression Graph Panel and Path Builder with New Features
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 48s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m31s
- Introduced a primary chain selection in the Exercise Progression Graph Panel to streamline exercise path management.
- Updated the ProgressionChainEditor to support single path mode, allowing users to manage a single progression path more effectively.
- Enhanced the ExerciseProgressionPathBuilder with improved logic for merging graph nodes into path steps and filtering gap offers.
- Updated UI elements for better clarity and user experience, including new notifications and styling adjustments.
- Incremented application version to reflect these updates.
2026-06-10 11:17:05 +02:00
800189ff8f Enhance Exercise Progression Graph Panel and Path Builder with New Features
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m16s
- Added `ProgressionChainEditor` to the Exercise Progression Graph Panel for improved management of exercise chains.
- Refactored state management to utilize `useRef` for chain editor references and removed unused sequence step logic.
- Introduced a path insert notice in the Exercise Progression Path Builder to inform users about unsaved changes.
- Updated UI elements to enhance clarity regarding the status of paths before saving.
- Incremented application version to reflect these updates.
2026-06-10 07:55:51 +02:00
3be7606d90 Update documentation and enhance planning features in Progression Graph
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m25s
- Updated the `CLAUDE.md` to reflect changes in the Progression Graph, including the new Ist-Stand and roadmap specifications.
- Enhanced `PLANNING_EXERCISE_SUGGEST_CONTEXT.md` with detailed descriptions of the current state and features of the planning exercise.
- Revised `PLANNING_PROGRESSION_ROADMAP_SPEC.md` to document the implementation status of various phases and their corresponding migrations.
- Incremented application version to 0.8.217 to incorporate recent updates and improvements in the planning context and roadmap functionalities.
2026-06-10 07:50:29 +02:00
ca3a9c6fa4 Enhance Exercise Progression Path Builder with Planning Wizard Stepper
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced a new Planning Wizard Stepper component to guide users through the exercise planning process in four steps.
- Implemented logic to compute the maximum reachable step based on user input and current progress.
- Updated state management to track the current wizard step and ensure it aligns with user interactions.
- Enhanced the user interface to improve clarity and navigation through the planning stages.
- Incremented application version to reflect these changes.
2026-06-10 07:30:01 +02:00
5692931d07 Update version to 0.8.217 and enhance Exercise Progression Path Builder with planning roadmap features
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Incremented application version to 0.8.217 to reflect recent changes.
- Added support for a planning roadmap in the Exercise Progression Path Builder, allowing users to save and load structured planning artifacts.
- Enhanced the persistence logic for the planning roadmap, ensuring updates are correctly handled during graph modifications.
- Improved the user interface to display saved planning hints, enriching the user experience and interaction with the progression graphs.
2026-06-10 07:25:57 +02:00
98b279fa89 Update version to 0.8.216 and enhance Exercise Progression Path Builder
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m18s
- Incremented application version to 0.8.216 to reflect recent changes.
- Added skill expectations handling in the Exercise Progression Path Builder, improving the integration of expected skills into the roadmap steps.
- Enhanced the mapping of major steps to include load profiles, success criteria, anti-patterns, and exercise types, enriching the user experience and functionality.
2026-06-10 07:14:18 +02:00
1e7941f57b Enhance Gap Fill Goal Text and Skill Expectations Integration
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 48s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Updated `build_gap_fill_goal_text` to include expected skills in the generated text, improving clarity for users.
- Enhanced `_roadmap_gap_snapshot_for_spec` to incorporate skill expectations from the progression stage, enriching the roadmap context.
- Modified `_annotate_roadmap_step` to append skill expectations to the step reasons, providing additional insights.
- Updated tests to verify the inclusion of expected skills in the gap fill goal text.
- Incremented application version to 0.8.215 to reflect these changes.
2026-06-10 07:09:46 +02:00
0adf20c9e1 Enhance Gap Planning Context with Stage Overrides and Trainer Supplements
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m25s
- Added `stage_learning_goal_override` and `gap_trainer_supplements` parameters to `build_progression_path_gap_planning_context`, allowing for customized learning goals and additional trainer notes.
- Updated `gapOfferContextDisplayLines` to include trainer supplements in the context display.
- Enhanced `ExerciseProgressionPathBuilder` to utilize new parameters for improved gap fill offer handling.
- Incremented application version to 0.8.214 to reflect these changes.
2026-06-10 06:54:49 +02:00
d4b1780193 Enhance Gap Fill Offer with Context Preview and Update Version
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added `context_preview` to the `build_gap_fill_offer` function, providing a structured overview of the roadmap snapshot.
- Introduced `gapOfferContextDisplayLines` utility to format context information for UI display, improving clarity for users.
- Updated `ExerciseProgressionPathBuilder` and related components to utilize the new context preview, enhancing the user experience.
- Incremented application version to 0.8.213 to reflect these changes.
2026-06-09 16:27:03 +02:00
f2650dac57 Enhance Planning Context with Progression Gap Snapshot and Start/Target Analysis
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `build_progression_gap_snapshot` function to create a compact roadmap context for gap exercises, integrating start situation, target state, and stage specifications.
- Updated `build_gap_fill_goal_text` to include roadmap snapshot details, enhancing the context for AI-generated exercises.
- Enhanced `ProgressionPathSuggestRequest` and related components to support new structured inputs for start/target analysis, improving user experience and AI suggestions.
- Incremented application version to 0.8.212 to reflect these changes.
2026-06-09 16:22:16 +02:00
fad1058d54 Enhance Progression Path Features with LLM Start/Target Extraction
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added `include_llm_start_target` option to `ProgressionPathSuggestRequest` for improved roadmap suggestions.
- Introduced new classes `StartTargetExtractArtifact` and `StartTargetResolveMeta` to handle LLM extraction results and metadata.
- Implemented `try_llm_start_target_extract` function to extract start and target states from goal queries using LLM.
- Updated `resolve_roadmap_structured_input` to prioritize user inputs, LLM extractions, and regex parsing for start/target resolution.
- Enhanced `ExerciseProgressionPathBuilder` to utilize new structured inputs and display extraction sources.
- Incremented application version to 0.8.211 to reflect these changes.
2026-06-09 12:54:08 +02:00
9dd44ce3ca Add Structured Roadmap Inputs and Enhance Goal Analysis Features
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced `RoadmapStructuredInput` to encapsulate structured inputs for start situation, target state, and roadmap notes.
- Updated `ProgressionPathSuggestRequest` to include new fields for structured roadmap inputs.
- Implemented parsing logic for goal queries to extract start and target states, enhancing the goal analysis process.
- Enhanced `build_goal_analysis` to utilize structured inputs, improving the clarity and relevance of generated goals.
- Updated the `ExerciseProgressionPathBuilder` component to support new structured input fields, enhancing user experience.
- Incremented application version to 0.8.210 to reflect these changes.
2026-06-09 11:10:46 +02:00
87f258be38 Enhance Path QA with Roadmap-First Features and Gap Detection Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced `roadmap_qa_mode` to manage QA behavior based on roadmap-first logic, improving gap detection between major steps.
- Updated `detect_path_gaps` to skip gaps for roadmap-planned neighbor pairs, enhancing the accuracy of path assessments.
- Added new helper function `is_roadmap_planned_neighbor_pair` to facilitate roadmap neighbor checks.
- Updated relevant tests to validate new functionality and ensure robustness.
- Incremented application version to 0.8.209 to reflect these changes.
2026-06-09 10:17:30 +02:00
779e2477ba Implement Planning Context Integration for Exercise AI Suggestions
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added `planning_context` to the `suggestExerciseAi` endpoint, enabling structured planning context for new exercise creation.
- Updated relevant components and backend logic to handle the new planning context, enhancing the AI's exercise suggestion capabilities.
- Incremented application version to 0.8.208 to reflect these changes.
2026-06-08 15:15:03 +02:00
f074a8bef0 Implement Roadmap Review Features and Enhance Progression Path Management
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added support for editable major steps in the roadmap, allowing users to modify phase, learning goals, and order before exercise matching.
- Introduced a new `roadmap_override` feature to facilitate customized retrieval without re-invoking the roadmap AI.
- Updated the `ExerciseProgressionPathBuilder` component to incorporate these new features, enhancing user interaction and flexibility.
- Incremented application version to 0.8.207 to reflect these changes.
2026-06-08 14:59:24 +02:00
0677663268 Enhance Exercise Progression Path Management with Dynamic Step Capacity
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 46s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced logic to manage path capacity dynamically, allowing users to expand the maximum number of steps when inserting new offers.
- Implemented confirmation prompts for users when the path is full, enhancing user experience and decision-making.
- Updated the `ExerciseProgressionPathBuilder` component to reflect these changes, improving the handling of gap-fill offers and user interactions.
- Adjusted UI messages to clarify the implications of adding new steps and the conditions under which users can expand the path.
2026-06-08 14:51:15 +02:00
d4e9bded23 Implement Roadmap-First Retrieval and Enhance Planning AI Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m26s
- Introduced a roadmap-first approach for retrieval, allowing for structured exercise suggestions based on stage specifications and major steps.
- Added functionality to generate gap-fill offers for unfilled roadmap stages, improving the relevance of exercise recommendations.
- Updated the `ExerciseProgressionPathBuilder` to support the new roadmap-first feature, enhancing user experience with clearer exercise paths.
- Incremented application version to 0.8.206 and updated the database schema version to reflect these changes.
2026-06-08 12:40:17 +02:00
7411543a97 Enhance Planning AI with Roadmap-First Architecture and New Features
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 46s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 40s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced a roadmap-first approach for the planning AI, allowing for a structured progression graph that aligns with the overall project roadmap.
- Updated the `ExerciseProgressionPathBuilder` to include a roadmap preview and improved handling of focus areas and skills catalog.
- Added functionality to strip off-topic steps from the exercise path, enhancing the relevance of generated paths.
- Implemented a new method to build detailed goal texts for AI-generated exercises, improving clarity and context.
- Incremented application version to 0.8.205 and updated database schema version to 20260606086 to reflect these changes.
2026-06-08 08:23:33 +02:00
dd0fae4bf5 Enhance Planning AI with Roadmap-First Architecture and New Features
Some checks failed
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Failing after 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 44s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced a roadmap-first approach for the planning AI, allowing for a structured progression graph that aligns with the overall project roadmap.
- Added new functionality to strip off-topic steps from exercise paths, improving the relevance of generated exercise suggestions.
- Implemented a detailed goal text generation for AI proposals, enhancing the context provided for new exercises.
- Updated the ExerciseProgressionPathBuilder component to support new features, including roadmap previews and improved focus area handling.
- Incremented application version to 0.8.205 and updated database schema version to 20260606086 to reflect these changes.
2026-06-08 08:10:53 +02:00
a9a6153ed5 Implement Club Feature Enforcement Logic and Update Versioning
Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Failing after 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced a new environment variable `CLUB_FEATURE_ENFORCE` to control club feature access, allowing values of 1, true, or yes for activation.
- Updated the backend logic to check for club feature enforcement, raising HTTP exceptions when access is denied without an active club context.
- Enhanced the admin rights router with a new endpoint to check the enforcement status of club features.
- Incremented application version to 0.8.202 to reflect these changes.
2026-06-07 15:47:49 +02:00
4130a63dfe Implement Registry-First Approach for Rights and Capabilities Management
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m51s
- Updated the capability catalog to reflect a registry-first approach, requiring modules to register rights and quotas upon implementation.
- Enhanced the backend to synchronize the rights registry with the database, ensuring only registered capabilities and features are displayed in the admin matrix.
- Modified SQL queries in the admin rights router to filter capabilities and features based on module registration.
- Updated documentation to clarify the new rights and features registry process, replacing the previous catalog-first method.
- Incremented application version to 0.8.201 and updated database schema version to 20260606084 to reflect these changes.
2026-06-07 15:36:31 +02:00
9d52aeab67 Update Membership RBAC Decisions and Enhance Admin Rights Management
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Updated the Membership RBAC Decisions document to reflect the latest implementation status and roadmap, including new features and enhancements.
- Incremented application version to 0.8.200 and updated database schema version to 20260606083.
- Added a new API endpoint to clear capability grants for club roles, improving admin rights management.
- Enhanced the Admin Rights page in the frontend to display enforcement status and feature consumption details for capabilities.
- Improved the user interface for better clarity on rights and capabilities management.
2026-06-07 15:27:37 +02:00
b68185842e Enhance Club Feature Consumption Logic and Update Versioning
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m40s
- Introduced the `consume_club_feature_with_usage` function to standardize feature consumption across endpoints, improving code reusability and clarity.
- Implemented `merge_feature_usage_into_response` to embed feature usage data in API responses, streamlining frontend integration.
- Updated various backend routers to utilize the new consumption logic, ensuring consistent feature usage tracking during AI-related actions.
- Enhanced tests to validate the new consumption and logging behavior.
- Incremented application version to 0.8.199 and updated module version for 'club_features' to 1.6.0 to reflect these changes.
2026-06-07 10:32:49 +02:00
40641594ac Add admin rights router to access layer hints exemption list
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 36s
Test Suite / playwright-tests (push) Successful in 1m20s
- Included "admin_rights.py" in the EXEMPT_ROUTERS frozenset to ensure proper access control for superadmin roles.
- This change enhances the management of admin capabilities and aligns with recent updates to the admin rights management system.
2026-06-07 09:24:16 +02:00
e4cb491d46 Refactor Admin Rights Management and Update Versioning
Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Has been cancelled
- Replaced the admin club feature exemptions router with a new admin rights router to streamline capability management.
- Added new API endpoints for managing admin rights, including capability grants and quota bypass for portal roles and profiles.
- Updated the frontend to include navigation and lazy loading for the new Admin Rights page.
- Incremented application version to 0.8.197 to reflect these changes and enhancements.
2026-06-07 09:21:59 +02:00
8404a42b6c Implement Club Feature Quota Bypass and Update Versioning
Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 42s
Test Suite / playwright-tests (push) Successful in 1m19s
- Added support for club feature quota bypass based on portal roles and profile grants in the capabilities check.
- Introduced new functions to handle quota bypass logic in club feature access and consumption.
- Updated the FeatureUsageBadge component to reflect platform exemptions for features.
- Incremented application version to 0.8.195 and database schema version to 20260606083 to reflect these changes.
- Enhanced backend routers to include new logic for consuming club features during AI-related actions.
2026-06-07 07:43:35 +02:00
fa10450315 Update Version and Enhance Club Creation Request Management
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Incremented application version to 0.8.192 and database schema version to 20260606081.
- Updated club module versions for 'clubs' and 'club_creation_requests' to reflect recent changes.
- Implemented logic to mark approved club creation requests as 'superseded' when the associated club is deleted.
- Refactored frontend components to clear session storage for coach-related keys upon logout and during login checks.
- Enhanced onboarding page to accurately display the status of club creation requests based on their validity.
2026-06-07 07:31:05 +02:00
37785135b1 Refactor Org Inbox Context and Enhance Club Creation Management
Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Failing after 1m21s
- Updated the OrgInboxContext to include handling for club creation requests, allowing for better management of inbox items.
- Refactored components to utilize the new `canShowInboxNav` and `canAccessClubCreationInbox` flags for improved access control.
- Enhanced the InboxPage to display club creation requests with appropriate actions for approval and rejection.
- Updated the DashboardOrgInboxWidget to show both club creation and join requests, improving the user interface for managing inbox items.
2026-06-07 07:18:43 +02:00
8ee8f52e0f Add Club Creation Request Management Features
Some checks failed
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Failing after 1m14s
- Introduced endpoints for managing club creation requests, including fetching, creating, and withdrawing requests.
- Updated the onboarding page to allow users to submit new club creation requests and view their existing requests.
- Enhanced the admin interface with navigation and routing for club creation requests management.
- Incremented version to 0.8.191 to reflect these new features and updates in the application.
2026-06-07 07:09:39 +02:00
8718cf5c70 Enhance Authentication and Feature Usage Handling
Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Failing after 1m15s
- Refactored the logout function in AuthContext to handle asynchronous logout operations, improving session management.
- Updated the FeatureUsageBadge component to display error messages when feature data retrieval fails, enhancing user feedback.
- Replaced lazy loading of OnboardingPage with lazyWithRetry for improved loading reliability.
- Adjusted the EntitlementsContext to determine club ID using utility functions for better governance form handling.
2026-06-07 07:05:02 +02:00
91dae7b614 Update Gitea Workflow to Restrict Test Triggers
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Modified the Gitea workflow configuration to trigger tests only on pushes and pull requests to the 'develop' branch, preventing duplicate test runs on the 'main' branch during merges.
- Added comments to clarify the purpose of the workflow triggers for better understanding.
2026-06-07 06:50:36 +02:00
20927a5969 Fix capability ID reference in migration script
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m15s
- Updated the SQL migration script to select the correct capability ID from the capabilities table, ensuring accurate role capability grants during database migrations.
2026-06-07 06:43:01 +02:00
7db77f4738 Improve Deployment Workflow and Database Migration Logic
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Failing after 43s
Test Suite / pytest-backend (push) Failing after 31s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Enhanced the deployment workflow to include error handling for the DEV API, ensuring logs are captured if the API is unreachable.
- Updated the migration scripts to safely rename existing tables by checking for their existence, preventing potential conflicts during migrations.
- Added exception handling in migration 079 to ensure the prerequisites are met before proceeding with the creation of the capabilities table.
2026-06-07 06:41:22 +02:00
3e87f7515a Enhance Backend Testing Workflow and API Onboarding Logic
Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Has been skipped
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / playwright-tests (push) Has been cancelled
Test Suite / k6 /health Baseline (push) Has been cancelled
- Updated the Gitea workflow to ensure backend tests run only on successful workflow runs or pull requests, improving CI reliability.
- Added a timeout mechanism to wait for the backend container to be ready before executing tests, enhancing test stability.
- Refactored the onboarding gate logic to include a middleware session lookup check, ensuring proper access control based on database state.
- Improved code readability and added comments for better understanding of the onboarding gate functionality.
2026-06-07 06:26:46 +02:00
a2f60d3f46 Update Capability Catalog and Club Membership Documentation
Some checks failed
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / playwright-tests (push) Has been cancelled
Test Suite / k6 /health Baseline (push) Has been cancelled
- Revised the status in the Capability Catalog to reflect partial implementation (M3).
- Added a new reference to `MEMBERSHIP_RBAC_DECISIONS_2026-06.md` in both the Capability Catalog and Club Membership documentation.
- Enhanced the Club Membership documentation with details on product decisions and onboarding phases.
- Implemented middleware in the backend to restrict access for unverified users and those pending club membership.
- Updated versioning in `version.py` to reflect changes in account lifecycle management.
2026-06-07 05:57:13 +02:00
30dc30c7aa Enhance Tenant Context and Access Control Features
Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Failing after 4m0s
Test Suite / playwright-tests (push) Failing after 3m41s
- Introduced `email_verified` and `account_state` attributes in the `TenantContext` to improve user state management.
- Updated the `resolve_tenant_context` function to dynamically fetch `email_verified` status from the database and determine `account_state` based on user roles and memberships.
- Implemented `assert_min_account_state` checks across various endpoints to enforce access control based on user account status.
- Incremented version to 1.1.0 in version.py to reflect these enhancements in tenant context management and access control.
2026-06-06 21:10:52 +02:00
7cfbca40bb Implement Club Feature Access Probing and Inventory Count
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m42s
- Introduced `probe_club_feature_access` to check club feature limits and log access attempts without blocking by default.
- Added `_live_inventory_count` function to retrieve current counts for specific features, enhancing feature limit management.
- Updated various endpoints to utilize the new probing functionality, ensuring compliance with club feature access rules.
- Incremented version to 1.1.0 in version.py to reflect these enhancements in club feature management.
2026-06-06 21:00:42 +02:00
c294c27de8 Update Access Layer and Governance Documentation
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m12s
- Enhanced the ACCESS_LAYER_AND_GOVERNANCE_PLAN.md with new specifications for capability documentation and community features.
- Added references to new documents detailing capability IDs and club membership features.
- Updated MULTI_TENANCY_RBAC_ARCHITECTURE.md to include links to the new specifications.
- Marked certain features as deprecated in backend/auth.py, indicating migration paths for club feature access.
- Incremented DB_SCHEMA_VERSION to 20260606078 in version.py to reflect recent changes.
2026-06-06 20:44:51 +02:00
bd5a409fa7 Add Admin User Content Management Features
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m18s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 38s
Test Suite / playwright-tests (pull_request) Successful in 1m12s
- Introduced a new admin user content management endpoint for superadmins, allowing for moderation of user-generated content.
- Updated the backend to include new API functions for retrieving, patching, and deleting user content items.
- Enhanced the frontend with a new Admin User Content page and navigation link for easy access to user content management.
- Updated access layer documentation to reflect the new endpoint and its exempt status.
- Incremented version to 0.8.191 and updated changelog to document these additions in admin functionality.
2026-06-06 17:53:25 +02:00
3450a9296a Enhance Planning Exercise Path AI and UI Integration
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Updated the AI gap filling logic to include structured offers for unfilled gaps, improving the user experience in the Exercise Progression Path Builder.
- Introduced new functions for detecting off-topic steps and parsing LLM-suggested exercises, enhancing the contextual relevance of exercise suggestions.
- Enhanced the frontend components to support new AI proposal features, including quick creation modals for newly suggested exercises.
- Incremented version to 0.8.190 and updated changelog to reflect these improvements in planning AI functionality.
2026-05-23 12:59:46 +02:00
8d1dd59c3c Refactor Planning Exercise Path Logic and Enhance Semantic Gating
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Replaced the manual path selection logic with a new `pick_best_path_hit` function to streamline the process of selecting the best exercise based on semantic scores and gating criteria.
- Updated the semantic gating logic to apply a soft penalty for off-topic exercises, improving the flexibility of exercise selection.
- Enhanced the handling of title, summary, and goal parameters in semantic checks to ensure more accurate relevance assessments.
- Incremented version to 0.8.189 and updated changelog to reflect these improvements in planning AI functionality.
2026-05-23 12:50:55 +02:00
5b73d1a1f5 Enhance Planning Exercise Path Builder and Retrieval Logic
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m23s
- Updated the path selection logic to incorporate semantic gating, ensuring only relevant exercises are considered based on semantic scores.
- Introduced new functions for building path target profiles and resolving semantic skill weights, enhancing the contextual understanding of exercise suggestions.
- Improved the retrieval process by applying dynamic retrieval weights based on semantic strength, refining the accuracy of exercise recommendations.
- Incremented version to 0.8.188 and updated changelog to document these enhancements in planning AI functionality.
2026-05-23 12:38:38 +02:00
c2c736dafc Implement Phase E2 Enhancements for Planning Exercise Suggestion
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 51s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m20s
- Introduced path reordering functionality using LLM with `ordered_step_indices`, allowing for dynamic adjustment of exercise progression paths.
- Added AI gap filling capabilities, enabling the system to propose new exercises when unbridgeable gaps are detected.
- Updated the backend to support new request parameters for path reordering and AI gap filling.
- Enhanced frontend components to reflect these new features, including alerts for AI proposals and adjustments in exercise display.
- Incremented version to 0.8.187 and updated changelog to document these significant enhancements in planning AI functionality.
2026-05-23 12:32:14 +02:00
c6b8c396ad Enhance Planning Exercise Retrieval and Suggestion with Semantic Features
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced new functions to load exercise goals and variant names in chunks, improving data retrieval efficiency.
- Integrated semantic scoring into the ranking logic, allowing for more nuanced exercise suggestions based on semantic relevance.
- Updated the planning exercise suggestion process to include semantic brief handling, enriching the context for exercise recommendations.
- Adjusted the retrieval phase to incorporate dynamic retrieval weights based on semantic strength, enhancing the overall suggestion accuracy.
- Incremented version to 0.8.186 and updated changelog to reflect these significant enhancements in planning AI functionality.
2026-05-23 12:02:57 +02:00
a19ed02300 Implement Phase C3 Enhancements for Progression Path Suggestion
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
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 1m13s
- Incremented version to 0.8.185, reflecting the implementation of Phase C3 features.
- Introduced the `POST /api/planning/progression-path-suggest` endpoint for generating exercise progression paths.
- Enhanced the ExerciseProgressionGraphPanel with a new ExerciseProgressionPathBuilder for reviewing and saving paths.
- Updated changelog to document the new capabilities in planning AI functionality.
2026-05-23 11:46:25 +02:00
a34e748be5 Implement Phase C2 Enhancements for Exercise Suggestions
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 18s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Incremented version to 0.8.184, reflecting the implementation of Phase C2 features.
- Added support for displaying variant lists and suggested variant names in exercise suggestions.
- Enhanced the ExercisePickerModal to allow selection of exercise variants and improved handling of variant IDs.
- Updated backend logic to enrich planning hits with variant metadata, ensuring accurate exercise variant selection.
- Documented changes in the changelog to highlight the new capabilities in planning AI functionality.
2026-05-23 11:39:18 +02:00
b2157d8a40 Update Planning Exercise Suggestion and Context Handling
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
Test Suite / pytest-backend (pull_request) Successful in 37s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m15s
- Incremented version to 0.8.183, reflecting the implementation of Phase C1 enhancements.
- Added support for progression graph auto-matching and variant-aware successors in exercise suggestions.
- Updated request and response structures to include `anchor_exercise_variant_id`, `progression_graph_name`, and `suggested_variant_id`.
- Enhanced frontend components to integrate planning AI search capabilities, including a new modal for exercise creation and improved context display in the exercise list.
- Updated changelog to document these significant improvements in planning AI functionality.
2026-05-23 10:42:17 +02:00
50aff849d8 Enhance Planning Exercise Suggestion and Ranking Logic
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
- Introduced a new function `hybrid_ranking_ambiguous` to determine when to rerank candidates based on score proximity, improving the decision-making process for exercise suggestions.
- Updated `should_run_llm_rank_pipeline` to incorporate the new ranking logic and handle scenarios with ambiguous rankings more effectively.
- Adjusted the frontend to always include LLM ranking in requests, ensuring consistent behavior across different query lengths.
- Incremented version to 0.8.182 and updated changelog to reflect these enhancements in planning AI capabilities.
2026-05-23 10:28:03 +02:00
a0a891e550 Implement Phase B Enhancements for Planning Exercise Profiles
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Added support for section guidance notes and titles in the planning target profile, enabling richer context for exercise suggestions.
- Introduced deterministic text-to-catalog signal mapping, allowing for improved integration of planning text signals into the exercise retrieval process.
- Implemented a partner-related filter in exercise retrieval, enhancing the relevance of suggested exercises based on user intent.
- Updated the retrieval phase to account for text signals, improving the accuracy of exercise recommendations.
- Incremented version to 0.8.181 and updated changelog to reflect these significant enhancements in planning AI capabilities.
2026-05-23 10:26:03 +02:00
46fae3da33 Enhance Exercise Enrichment Admin Functionality and Update Documentation
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
Test Suite / pytest-backend (pull_request) Successful in 36s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m23s
- Implemented a maximum of 3 exercises per preview request to prevent Gateway-504 errors, improving the stability of the exercise enrichment process.
- Adjusted batch sizes for applying exercises and previewing to optimize performance and resource management.
- Updated the frontend to reflect changes in preview handling, including user notifications about chunk sizes and potential timeouts.
- Incremented version to 0.8.180 and updated changelog to document these enhancements and fixes.
2026-05-23 07:46:35 +02:00
f4196c3580 Add Exercise Enrichment Admin API and Update Documentation
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced the `exercise_enrichment_admin` API for batch exercise enrichment, allowing superadmins to filter candidates, preview, and apply skills.
- Updated the access layer documentation to include the new endpoint and its exempt status.
- Enhanced the frontend with a new admin page for exercise enrichment and updated navigation to include this feature.
- Incremented version to 0.8.179 and updated changelog to reflect these additions and improvements.
2026-05-23 07:35:45 +02:00
d1d8539b42 Refactor Planning Exercise Retrieval and Suggestion Logic
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 1s
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
- Updated the planning exercise retrieval process to implement a multistage approach, ranking the entire visible library deterministically against the expectation profile.
- Removed the previous profile OR pool mechanism, simplifying the retrieval logic and ensuring full-text search is only used as a scoring signal.
- Adjusted the `compose_retrieval_phase` function to accommodate the new full library ranking strategy.
- Incremented version to 0.8.177 and updated changelog to reflect these changes in planning exercise capabilities.
2026-05-23 06:35:45 +02:00
a8633235f2 Add ExerciseAiQuickCreateTeaser Component and Update ExercisePickerModal
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced the ExerciseAiQuickCreateTeaser component for a compact entry point in the exercise creation process.
- Updated ExercisePickerModal to integrate the new teaser, allowing users to expand and create exercises directly from the search results.
- Enhanced the quick create functionality with dynamic headlines and hints based on user input and context.
- Refactored conditional rendering logic to improve user experience when no exercises are found.
2026-05-23 06:16:37 +02:00
5c882985e0 Enhance Planning Exercise Functionality and LLM Integration
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m15s
- Added support for the new planning exercise expectation profile slug in the AI prompt runtime.
- Refactored SQL parameter handling in the planning exercise retrieval process to ensure correct binding for full-text search.
- Updated the planning exercise suggestion logic to incorporate LLM expectation handling, improving the accuracy of exercise recommendations.
- Introduced new functions to determine when to run the LLM expectation pipeline, enhancing the decision-making process for exercise suggestions.
- Incremented version to 0.8.176 and updated changelog to reflect these enhancements in planning AI capabilities.
2026-05-22 23:08:53 +02:00
04cc77d501 Enhance Planning Exercise Profiles and Context Handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
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 1m14s
- Introduced new functions to generate skill profiles from exercise IDs, improving the ability to summarize skills for both units and sections.
- Updated the planning target profile to incorporate section-specific exercise IDs, allowing for more granular skill tracking and context.
- Enhanced the ExercisePickerModal and related pages to support section context, including titles, guidance notes, and exercise counts.
- Implemented expectation mode handling in the planning target pipeline to differentiate between planning references and query-only scenarios.
- Incremented version to 0.8.174 and updated changelog to reflect these enhancements in planning AI capabilities.
2026-05-22 23:00:31 +02:00
8e68261bc1 Refactor Planning Exercise Suggestion and Enhance LLM Integration
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m16s
- Replaced the previous exercise matching logic with a new multistage planning retrieval process, improving the accuracy of exercise suggestions.
- Introduced LLM gates to limit LLM calls based on query length and intent application, optimizing performance and resource usage.
- Updated the `compose_retrieval_phase` function to include profile preselection, enhancing the retrieval process.
- Incremented version to 0.5.0 and updated changelog to reflect these significant enhancements in planning AI capabilities.
2026-05-22 22:56:28 +02:00
b0611b9f7f Update ExercisePickerModal to Enforce Backend Suggestion Limit
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced a constant `PLANNING_SUGGEST_LIMIT` set to 50 to align with backend constraints for exercise suggestions.
- Updated the API request limit in `ExercisePickerModal` to utilize the new constant, ensuring compliance with backend specifications.
2026-05-22 22:46:00 +02:00
614c2dcfaa Enhance Planning Exercise Suggestion with Client Context and Group ID Support
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m16s
- Made `unit_id` and `group_id` optional in `PlanningExerciseSuggestRequest` to support client context without a saved unit.
- Refactored `_load_group_recent_exercise_ids` to handle cases where `exclude_unit_id` is optional.
- Introduced `build_client_planning_context_pack` for improved context handling in client-free searches.
- Updated `suggest_planning_exercises` to utilize the new client context pack when `unit_id` is not provided.
- Incremented version to 0.8.172 and updated changelog to reflect these enhancements in the planning AI capabilities.
2026-05-22 22:38:21 +02:00
f5c886fc13 Enhance ExercisePickerModal with Improved Planning Context Handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m39s
- Introduced `planningUnitId` and `expectPlanningSearch` props to better manage planning context for exercise suggestions.
- Refactored logic to resolve planning unit ID and construct active planning context, enhancing the accuracy of exercise suggestions.
- Implemented checks to block planning search when necessary, providing clearer user feedback in the UI.
- Updated `TrainingUnitEditPage` to pass the correct planning unit ID, ensuring seamless integration with the exercise picker.
2026-05-22 22:30:29 +02:00
d019c20338 Refactor ExercisePickerModal for Enhanced Search Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
- Updated `effectivePickerQuery` logic to improve search handling based on planning context, allowing for a single input field in planning mode.
- Simplified query construction by utilizing `effectivePickerQuery` throughout the component, enhancing clarity and user experience.
- Adjusted UI elements and labels to better reflect the context of the search, providing clearer guidance for users.
- Modified `TrainingUnitEditPage` to ensure proper unit ID resolution, improving integration with the exercise picker.
2026-05-22 22:24:49 +02:00
905bce198f Refactor ExercisePickerModal to Utilize Effective Query for AI Suggestions
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced `effectivePickerQuery` to streamline search input handling, combining `debouncedSearch` and `debouncedAi` for improved query accuracy.
- Updated the `useExerciseAiQuickCreateFields` hook to use the new effective query, enhancing the quick create functionality.
- Modified conditional checks to utilize `effectivePickerQuery`, ensuring better user feedback based on search input.
- Improved placeholder text and labels for clarity in the search fields, enhancing user experience during exercise selection.
2026-05-22 22:21:06 +02:00
45e3b5f4f6 Implement Phase 1 of Planning Exercise Suggestion with Scenario Pipeline and LLM Intent Overlay
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Introduced the Scenario Pipeline for planning exercises, allowing for more nuanced query handling and exercise suggestions based on user intent.
- Enhanced the `suggestPlanningExercises` API to include `include_llm_intent`, `scenario_kind`, and `query_intent_summary`, improving the context provided to the frontend.
- Updated the `ExercisePickerModal` to display new information related to query intent and scenario classification, enhancing user experience during exercise selection.
- Incremented application version to 0.8.171 and updated changelog to document the new features and improvements in the planning AI capabilities.
2026-05-22 22:15:19 +02:00
207817376d Enhance Planning Exercise Suggestion with LLM-Rerank and Client Overrides
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 47s
Test Suite / playwright-tests (push) Successful in 1m14s
- Implemented optional LLM-Rerank functionality in the planning exercise suggestion process, allowing for improved exercise ranking based on user-defined criteria.
- Updated the `suggestPlanningExercises` API to accept `planned_exercise_ids` for client-side overrides, enhancing flexibility in exercise selection.
- Enhanced the `ExercisePickerModal` to reflect LLM ranking status and support new planning context features.
- Incremented application version to 0.8.170 and updated changelog to document the new features and improvements in the planning AI capabilities.
2026-05-22 22:09:28 +02:00
128a9d752e Enhance Planning Exercise Suggestion Features and Update Application Version to 0.8.169
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Implemented Phase 1.1 of the planning exercise suggestion functionality, integrating `ExerciseMatchProfile` and `PlanningTargetProfile` for improved exercise scoring based on profile dimensions.
- Updated the `suggestPlanningExercises` API to include a new `retrieval_phase` and `target_profile_summary`, enhancing the context provided to the frontend.
- Enhanced the `ExercisePickerModal` to display additional information from the planning target profile, including focus areas and top skills, improving user experience during exercise selection.
- Incremented application version to 0.8.169 and updated changelog to reflect the new features and improvements in the planning AI capabilities.
2026-05-22 22:04:34 +02:00
d7d45a8927 Integrate Planning AI Features and Update Application Version to 0.8.167
Some checks failed
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Failing after 3m59s
Test Suite / playwright-tests (push) Failing after 3m41s
- Added new planning AI functionality with the introduction of the `suggestPlanningExercises` API endpoint for context-based exercise suggestions.
- Enhanced `ExercisePickerModal` to utilize planning context, allowing for a more tailored exercise selection experience.
- Updated `TrainingUnitEditPage` to pass planning context to the exercise picker, improving integration with the new planning features.
- Incremented application version to 0.8.167 and updated changelog to reflect the new planning AI capabilities and related enhancements.
2026-05-22 21:52:18 +02:00
9d880e2346 Enhance Exercise List Page with AI Assistant Toggle and UI Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 16s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
Test Suite / pytest-backend (pull_request) Successful in 37s
Test Suite / lint-backend (pull_request) Successful in 1s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m17s
- Introduced a new AI assistant toggle in the Exercise List Page header, allowing users to enable quick exercise creation via AI suggestions.
- Updated the ExerciseListSearchBar component to remove deprecated AI quick create functionality, streamlining the interface.
- Enhanced CSS styles for the AI assistant toggle, improving visual feedback and user interaction.
- Improved overall layout and spacing in the exercises page for better usability.
2026-05-22 19:44:15 +02:00
c816e50c68 Refactor Exercise Creation Components to Utilize Custom Hook for Quick Create Fields
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Updated ExerciseAiQuickCreateOffer to set showSketchField to true by default and introduced sketchOptional prop for improved flexibility in exercise creation.
- Refactored ExercisePickerModal and ExercisesListPageRoot to leverage useExerciseAiQuickCreateFields hook, simplifying state management for quick create fields.
- Removed deprecated parsing logic and streamlined error handling for sketch input, enhancing user experience during exercise creation.
- Improved placeholder text and labels for clarity, ensuring better guidance for users when providing input for AI-generated exercises.
2026-05-22 19:36:22 +02:00
294740b780 Increment application version to 0.8.166 and update changelog for new features in AI exercise creation
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Updated APP_VERSION to 0.8.166 and modified BUILD_DATE to reflect recent changes.
- Enhanced AI exercise creation process with a new quick create feature, allowing users to generate exercises based on search input.
- Introduced a rich text editor for editing AI-generated drafts, improving user experience in exercise creation.
- Updated ExercisePickerModal and related components to support the new quick create functionality, including error handling and input validation.
- Added new utility functions for parsing search queries and building exercise payloads from drafts.
2026-05-22 19:24:36 +02:00
675cfa85f0 Enhance AI Quick Create Functionality in ExercisePickerModal
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Updated the quick create process to include a preview feature for AI-generated exercises, allowing users to review goals, execution, preparation, and trainer notes.
- Introduced new constants for instruction fields and refactored the payload building function to utilize the preview data.
- Improved error handling to ensure at least one of the goal or execution fields is populated.
- Deprecated the previous payload building function in favor of the new preview-based approach, streamlining the exercise creation workflow.
2026-05-22 19:10:16 +02:00
4725eaa90b Increment application version to 0.8.164 and update changelog for new features in ExercisePickerModal
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Updated APP_VERSION to 0.8.164 and added changelog entry for the new version.
- Enhanced ExercisePickerModal to support quick exercise creation using AI, including fields for sketch and focus area.
- Implemented error handling for AI suggestions and improved user prompts for input validation.
- Updated UI elements to reflect changes in exercise creation workflow.
2026-05-22 19:01:01 +02:00
9f4678f418 Implement exercise_instruction_rewrite for AI Prompt System
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 36s
Test Suite / playwright-tests (push) Successful in 1m16s
- Added `exercise_instruction_rewrite` functionality to enhance AI-generated instructions, incorporating fields for goal, execution, preparation, and trainer notes.
- Updated `ExerciseFormAiPromptContext` to include new fields and methods for instruction handling.
- Enhanced the `run_exercise_form_ai_suggestion` function to support instruction rewriting and validation.
- Modified API endpoints and frontend components to integrate instruction features, including a new button for AI instruction revision.
- Incremented application version to 0.8.163 and updated changelog to reflect these changes, including migration details and new functionality.
2026-05-22 18:53:36 +02:00
5331eab39c Implement ExerciseFormAiPromptContext and Refactor AI Prompt Job Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m23s
- Introduced `ExerciseFormAiPromptContext` for unified handling of prompt-related data, enhancing the admin preview and exercise API.
- Added `run_exercise_form_ai_suggestion` function to streamline AI suggestion processing, integrating with the OpenRouter.
- Updated various modules to utilize the new context model, improving code clarity and reducing redundancy.
- Incremented application version to 0.8.162 and updated changelog to reflect these changes, including migration details and new functionality.
2026-05-22 18:47:09 +02:00
93b8d09d05 Implement OpenRouter Model Support in AI Prompt System
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added `openrouter_model` field to the `ai_prompts` table, allowing for optional model overrides per prompt.
- Updated the `exercise_ai` module to utilize the effective OpenRouter model based on prompt-specific settings, enhancing flexibility in AI interactions.
- Enhanced the admin interface to support OpenRouter model configuration for prompts, improving usability for Superadmins.
- Incremented application version to 0.8.161 and updated changelog to reflect these changes, including migration details and new functionality.
2026-05-22 12:37:43 +02:00
0551bb3d80 Refactor AI Prompt System and Enhance Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m18s
- Introduced `load_and_render_ai_prompt` and `render_ai_prompt_template_for_row` in `ai_prompt_runtime` to streamline prompt loading and rendering processes.
- Added `AiPromptUnavailableError` for better error handling when prompts are inactive or missing.
- Created `ai_prompt_job` module with `ExerciseFormAiPromptContext` and `resolve_exercise_form_variables` to support admin preview functionality.
- Updated documentation and target architecture to reflect changes in the AI prompt system.
- Incremented application version to 0.8.160 and updated changelog accordingly.
2026-05-22 12:19:52 +02:00
e22266a18c Update ACCESS_LAYER_ENDPOINT_AUDIT and Matrix Editor Documentation
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
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 1m20s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Added the `matrix_editor` endpoint to the ACCESS_LAYER_ENDPOINT_AUDIT.md, specifying its access requirements and exempt status for superadmins.
- Updated comments in the `matrix_editor.py` file to clarify its role as a superadmin tool and its access restrictions.
- Included the `matrix_editor.py` in the EXEMPT_ROUTERS list in the access layer hints script, ensuring proper access control documentation.
2026-05-22 11:54:59 +02:00
d58db3d5dd Enhance Maturity Matrix Tools and API Integration
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Failing after 2s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m19s
- Added new functionality for exporting and importing matrix editor data in JSON and CSV formats within the MaturityMatrixToolsAdmin component.
- Updated the API utility functions to support matrix editor exports and imports, enhancing the backend communication for Superadmin tasks.
- Refactored the client API to streamline request handling and improve code clarity.
- Included new UI elements for file upload and download actions, improving user experience in managing matrix data.
2026-05-22 11:12:50 +02:00
cdeddc7cec Update AI Prompt System and Documentation
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 42s
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 1m18s
- Added a new target architecture document for the AI Prompt System, detailing context types, composition, and planning phases.
- Refactored the backend to utilize a shared function for loading AI prompt rows, reducing SQL duplication in the `exercise_ai` module.
- Incremented the application version to 0.8.159 and updated the changelog to reflect these changes, including enhancements to the AI prompt management and documentation links.
2026-05-22 11:05:35 +02:00
2148d0aa7f Update AI Prompt System and Admin API
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
- Incremented version to 1.1 and updated the status to reflect the implementation of core features including `ai_prompts`, `prompt_resolver`, and the Superadmin HTTP API.
- Documented the current API endpoints for managing AI prompts, including CRUD operations and preview functionality.
- Introduced a new placeholder catalog and preview capabilities for the Superadmin interface.
- Enhanced the backend with new functions for handling AI prompt templates and integrated them into the API.
- Updated frontend components to include navigation and routing for the new Admin AI Prompts page.
- Incremented application version to 0.8.158 and updated changelog to reflect these changes.
2026-05-22 11:02:02 +02:00
f9e295bce0 Remove AI suggestion preview dialog from ExerciseFormPageRoot component to streamline user interface and improve performance. This change eliminates unnecessary complexity in the component's rendering logic.
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 38s
Test Suite / playwright-tests (push) Successful in 1m30s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m31s
2026-05-22 10:29:49 +02:00
888d0bd009 Update environment configuration for AI debugging support
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Successful in 37s
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 1m16s
- Added `SHINKAN_AI_DEBUG` environment variable to `.env.example`, enabling detailed logging for AI operations in Docker containers.
- Integrated `SHINKAN_AI_DEBUG` into both `docker-compose.dev-env.yml` and `docker-compose.yml` to facilitate debugging during development and production environments.
2026-05-22 10:20:16 +02:00
1942585546 Enhance exercise_ai and openrouter_chat modules with AI debugging and improved content handling
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / build-frontend (push) Waiting to run
Test Suite / k6 /health Baseline (push) Waiting to run
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Has been cancelled
- Introduced detailed logging for AI operations in the `exercise_ai` and `openrouter_chat` modules, activated by the `SHINKAN_AI_DEBUG` environment variable, to aid in debugging and performance monitoring.
- Updated the `run_exercise_ai_suggestion` function to log prompt lengths, response sizes, and JSON parsing errors, enhancing transparency in AI interactions.
- Improved the `_flatten_message_content` function to handle nested content structures more effectively, ensuring compatibility with various AI response formats.
- Incremented the application version to 0.8.157 and updated the changelog to reflect these enhancements, including new logging features and content handling improvements.
2026-05-22 10:19:31 +02:00
a28a9d399a Enhance exercise_ai and openrouter_chat modules with improved JSON handling and error management
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m18s
- Added a new function `_first_balanced_json_array` to extract the first complete top-level JSON array from arbitrary text, enhancing robustness in parsing.
- Updated the `run_exercise_ai_suggestion` function to raise clear HTTP exceptions for empty responses from the OpenRouter, ensuring better error handling.
- Introduced `_flatten_message_content` in the `openrouter_chat` module to handle structured message content from OpenAI, improving compatibility with various content formats.
- Incremented the application version to 0.8.156 and updated the changelog to reflect these enhancements, including improved error messages and JSON parsing capabilities.
2026-05-22 10:09:07 +02:00
9be69ace5c Enhance exercise_ai module with skill input sanitization and version update
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
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 1m16s
- Introduced a new constant `_MAX_SANITIZE_SKILL_INPUT_ROWS` to limit the number of skill entries processed, improving performance and preventing issues with excessively long skill arrays.
- Updated the `_extract_json_array` and `_sanitize_skill_entries` functions to enforce this limit, ensuring that only a maximum of 250 skill entries are handled and that processing stops after 5 valid entries.
- Incremented the application version to 0.8.155 and updated the changelog to reflect these changes, including a note on the improvements made to the AI endpoint for skill arrays.
2026-05-22 09:59:56 +02:00
286c36e9d7 Document Superadmin API for AI Skill Retrieval Profiles and Update Access Layer
Some checks failed
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 43s
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) Has been cancelled
- Added documentation for the new Superadmin CRUD endpoints for managing AI Skill Retrieval Profiles (`/api/admin/ai-skill-retrieval-profiles*`).
- Updated the ACCESS_LAYER_ENDPOINT_AUDIT.md to include the new Superadmin API and its exempt status.
- Registered the ai_skill_retrieval_admin router in the backend and updated versioning to reflect the changes.
- Enhanced the frontend with a new Admin page for AI Skill Retrieval, including navigation and API integration for profile management.
2026-05-22 09:57:39 +02:00
294b09a5d9 Implement AI Skill Retrieval Profiles and Enhance Exercise AI Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced migration 068 for `ai_skill_retrieval_profiles`, enabling configurable weights and quotes for skill catalog prioritization in exercise AI suggestions.
- Updated the `POST /api/exercises/ai/suggest` endpoint to include an optional `focus_areas_context` field, allowing for enhanced context in AI-generated suggestions.
- Enhanced the `exercise_ai` module to utilize context-based skill selection, incorporating scoring, category caps, and keyword patches for improved AI responses.
- Updated the ExerciseFormPageRoot component to pass focus area context to the AI suggestion API, streamlining user interaction with AI-generated content.
- Incremented version numbers in `backend/version.py` to reflect the latest changes and ensure accurate tracking in the changelog.
2026-05-22 09:49:08 +02:00
e5291256d0 Enhance AI Exercise Suggestion Functionality and UX
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 38s
Test Suite / playwright-tests (push) Successful in 1m21s
- Updated the AI Exercise Implementation Plan to include a detailed description of the new suggestion dialog for AI proposals, allowing users to preview and selectively adopt AI-generated summaries and skills.
- Implemented a new preview feature in the ExerciseFormPageRoot component, enabling users to review AI suggestions before applying them to the form.
- Enhanced the skill management process by normalizing AI-suggested skills and integrating them into the exercise form, improving user interaction and data handling.
2026-05-22 09:21:44 +02:00
4d36bbf634 Update AI Training Planning Document and Versioning
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Incremented the version number from 0.2 to 0.3 in the AI Training Planning document to reflect the latest changes.
- Added a new reference to the `working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md` document, outlining the architecture preview for the planning AI.
- Updated the changelog in `backend/version.py` to include the latest version entry, ensuring accurate tracking of changes.
2026-05-22 07:56:56 +02:00
e4451e1362 Enhance Exercise Management and AI Integration
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
7245bbb1da bug_fix Variantenerzeugung
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m35s
Test Suite / pytest-backend (pull_request) Successful in 36s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m22s
2026-05-22 07:14:03 +02:00
5f67c01cef Refactor Exercise Visibility Labels for Consistency Across Components
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 42s
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 1m17s
- Replaced hardcoded visibility labels with the new constant EXERCISE_VISIBILITY_FIELD_LABEL in multiple components, ensuring consistent terminology throughout the application.
- Updated UI text to reflect the change from "Sichtbarkeit" to "Freigabelevel" in various contexts, enhancing clarity for users.
- Improved accessibility by standardizing the visibility-related labels in the ExercisePickerModal, ExerciseProgressionGraphPanel, and other related components.
2026-05-21 15:08:35 +02:00
13a1d3a060 Enhance Exercise Form UI and Functionality with New Tab Structure
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
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
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 15s
Test Suite / k6 /health Baseline (pull_request) Successful in 34s
Test Suite / playwright-tests (pull_request) Successful in 1m15s
- Introduced a tabbed interface for the exercise form, allowing users to navigate between different sections (Stammdaten, Anleitung, Einordnung, etc.) more intuitively.
- Added new CSS styles for the exercise form, including improved layout and visual differentiation for various sections.
- Implemented dynamic tab management based on exercise type and edit state, enhancing user experience during form interactions.
- Refactored existing components to integrate the new tab structure, ensuring a cohesive design and functionality across the exercise form.
2026-05-21 14:58:14 +02:00
7f62b6ceee Enhance Exercise Form Functionality with Variant Management Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Introduced snapshot and dirty check functions for variant payloads, enabling better tracking of unsaved changes.
- Implemented synchronization of saved variant snapshots to improve data integrity during edits.
- Enhanced the variant saving process with validation for variant names and automatic saving of changes.
- Updated the UI to reflect changes in variant management, ensuring a smoother user experience when editing exercise variants.
2026-05-21 14:54:32 +02:00
9b3f594007 Implement Exercise Form Enhancements with New Meta Panel
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added a new meta panel for exercise classification and target groups, improving the organization of exercise attributes.
- Introduced ExerciseCatalogAssocEditor component to manage focus areas, training styles, and target groups, enhancing user interaction.
- Refactored CSS styles for the new meta panel and associated components, ensuring a cohesive design and improved responsiveness.
- Removed the MultiAssocBlock component to streamline the code and improve maintainability.
2026-05-21 14:40:50 +02:00
5d308b20ba Add VARIANT_DIFFICULTY constant to ExerciseFormPageRoot component
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 37s
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 1m15s
- Introduced a new constant, VARIANT_DIFFICULTY, to define difficulty options for exercises.
- Improved code organization by separating the import statements for better readability.
2026-05-21 12:58:18 +02:00
1d698e4b0a Implement Phase 3 Enhancements for Skill Scoring and Profiles
Some checks failed
Deploy Development / deploy (push) Failing after 22s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 3s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added capabilities for weighted skill profiles, allowing trainers to compare training modules, frameworks, and regression paths based on skill contributions.
- Updated the skill scoring specification to include peer context separation and list filtering, ensuring accurate comparisons among visible artifacts of the same type.
- Enhanced the API to support batch summaries for skill profiles and discovery suggestions, improving data retrieval efficiency.
- Refactored frontend components to display skill metrics, including scores and peer percentages, with improved filtering options for better user experience.
- Updated documentation to reflect the latest changes and enhancements in the skill scoring system.
2026-05-21 12:35:45 +02:00
a7a428745f Enhance SkillTreeMultiSelect Component and CSS Styles
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 48s
Test Suite / playwright-tests (push) Successful in 1m28s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m16s
- Updated the SkillTreeMultiSelect component to support dynamic positioning and improved accessibility through the use of portals.
- Refactored the dropdown panel rendering logic to enhance user experience when selecting skills.
- Added CSS styles for the exercise filter modal to improve layout and responsiveness.
- Introduced new styles for the skill tree multiselect panel, ensuring better visual integration and usability.
2026-05-21 10:31:21 +02:00
2d187447bb Enhance Framework Programs Filtering and UI Components
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Updated the FrameworkProgramsFilterBlock to include a search input and filter modal, improving user interaction and accessibility.
- Refactored CSS styles for filter components to ensure consistent layout and spacing.
- Removed deprecated panel open state management, streamlining the component logic.
- Integrated new filtering capabilities in the TrainingPlanningFrameworkImportModal and TrainingModulesListPage, enhancing the overall filtering experience.
- Improved the display of active filters and results count, providing clearer feedback to users.
2026-05-21 10:23:05 +02:00
2de4c0b7c9 Refactor Skill Scoring Functions and Enhance Corpus Handling
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
- Introduced new helper functions for managing artifact type corpus, improving code organization and readability.
- Updated the `compute_club_corpus_reference` function to utilize the new corpus handling methods, enhancing clarity and maintainability.
- Refactored skill profile functions to leverage the new corpus structure, ensuring consistent data retrieval across different artifact types.
- Improved the handling of visibility clauses for library content, streamlining database queries for skill profiles.
- Enhanced the batch skill profile summary function to aggregate reference data by artifact type, improving performance and accuracy.
2026-05-21 10:17:22 +02:00
34966b9e84 Update Skill Profile Summary and Frontend Components
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Modified the `compact_profile_summary` function to allow for dynamic skill and category limits, enhancing flexibility in profile data retrieval.
- Updated frontend components to display skill weights and scores more effectively, improving user interaction with skill metrics.
- Adjusted CSS styles for skill KPI tiles to better differentiate between score and percentage displays, ensuring a clearer visual representation.
- Refactored utility functions to streamline skill summary handling, enhancing overall code maintainability and performance.
2026-05-21 09:42:13 +02:00
9a0cf7f823 Enhance Skill Scoring and Profile Functionality
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced a new function to calculate club-specific skill percentages, ensuring values are capped at 100%.
- Updated skill profile calculations to include indicators for the best club performance per skill.
- Enhanced frontend components to display club best indicators and improved layout for skill profiles.
- Refactored CSS styles for skill profile components, ensuring a more cohesive and user-friendly interface.
- Updated tests to validate new functionality and ensure accurate representation of skill metrics.
2026-05-21 09:33:20 +02:00
78c6c51520 Enhance Skill Scoring and Profile Features
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m15s
- Updated the skill scoring specification to include club-specific metrics and improved aggregation methods for skill profiles.
- Introduced new API endpoints for batch skill profile summaries, allowing for efficient retrieval of compact skill data.
- Enhanced frontend components to display skill profiles with club comparisons, improving user interaction and visibility of skill strengths.
- Added filtering options for skills in the framework programs, enabling users to refine selections based on training weight relative to club maximums.
- Improved CSS styles for skill profile displays, ensuring a cohesive and user-friendly interface across the application.
2026-05-21 09:05:13 +02:00
5200895a73 Update Skill Scoring Specification and Implementation to v1.2
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m27s
- Enhanced the skill scoring system with category grouping and a universal scale for improved comparability across programs.
- Introduced new calculations for artifact share percentage and universal percent, allowing for a more nuanced understanding of skill contributions.
- Updated the API to reflect changes in the skill profile structure, including main category and top skill details.
- Improved frontend components to display skills by main category, enhancing user experience in skill discovery and profile visualization.
- Adjusted tests to validate the new scoring logic and ensure accurate representation of skills and their weights.
2026-05-21 08:37:58 +02:00
8f8bdf6d8b Update Skill Scoring Specification and Implementation to v1.1
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m21s
- Enhanced the skill scoring formula to incorporate intensity and level range factors, improving the accuracy of skill contributions.
- Removed the use of `is_primary` and `development_contribution` from calculations, streamlining the scoring process.
- Updated documentation to reflect changes in the scoring logic and versioning.
- Adjusted frontend components to align with the new scoring criteria, ensuring consistent user experience across the application.
2026-05-21 08:24:23 +02:00
f67bf280c3 Add Skill Profiles API Integration for Phase 3
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 35s
Test Suite / playwright-tests (push) Successful in 1m21s
2026-05-21 08:11:46 +02:00
732b322c52 Implement Phase 3 Features for Skill Profiles and Discovery
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m27s
- Updated the framework program documentation to reflect the completion of Phase 3 v1.0, including new skill scoring and API enhancements.
- Added new API endpoints for skill profile retrieval and suggestions, improving the ability to aggregate and display skills based on training data.
- Introduced new UI components for skill profiles and discovery in the frontend, enhancing user interaction with training frameworks and skills.
- Updated version information to 0.8.151, reflecting the addition of skill profiles and related features.
2026-05-20 16:42:25 +02:00
e382b6ed35 Enhance Training Framework Program Edit Page with New Styling and Structure
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m16s
Test Suite / pytest-backend (pull_request) Successful in 36s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m16s
- Introduced a new PageFormEditorChrome component for improved layout and user experience.
- Updated CSS styles for the framework editor, including danger zone and delete button hover effects.
- Refactored the TrainingFrameworkProgramEditPage to utilize the new component and streamline the action configuration.
- Enhanced the page title handling for better clarity when creating or editing framework programs.
2026-05-20 16:23:10 +02:00
a4548f5587 Refactor Training Framework Programs List Page with Enhanced Styling and New Utility Functions
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Updated the TrainingFrameworkProgramsListPage to utilize new CSS classes for improved layout and styling.
- Removed deprecated components and functions, streamlining the codebase for better maintainability.
- Introduced utility functions for splitting aggregated strings, enhancing data handling for framework program attributes.
- Enhanced the user interface with loading and empty state indicators, improving overall user experience.
2026-05-20 16:21:16 +02:00
9d122d4808 Enhance Training Framework Programs List with Filtering and Styling Improvements
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Introduced a new filter block for training framework programs, allowing users to refine their selections based on focus areas, training types, and target groups.
- Updated the TrainingFrameworkProgramsListPage to integrate the filter block and manage filter states effectively.
- Enhanced CSS styles for the filter block and program list, improving layout and spacing for better user experience.
- Removed unused filter-related logic from the TrainingPlanningFrameworkImportModal, streamlining the component's functionality.
2026-05-20 16:05:18 +02:00
9c3494a7ea Implement Framework Import Modal with Enhanced Filtering and Styling
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m27s
- Introduced a new modal for importing training framework sessions, featuring a backdrop and panel for improved user experience.
- Added comprehensive filtering options for focus areas, training types, and target groups, utilizing a structured filter state.
- Enhanced session duration handling with distinct duration collection and display, improving clarity in program selection.
- Updated utility functions to support new filtering capabilities and session duration management, ensuring a cohesive user interface.
2026-05-20 15:06:03 +02:00
9353909fda Enhance Training Framework Programs with Session Duration and Filtering Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m29s
- Added SQL aggregations for session duration (min/max) and goal titles in the training framework programs query.
- Updated the TrainingPlanningFrameworkImportModal component to include filtering options for focus areas, training types, and target groups.
- Implemented session duration display in the TrainingFrameworkProgramsListPage, improving user visibility of program details.
- Introduced utility functions for formatting session duration ranges, enhancing the overall user experience in training planning.
2026-05-20 13:19:40 +02:00
5a8a212f40 Update version information and add training duration features
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Bumped application version to 0.8.150 and updated build date and database schema version.
- Introduced new SQL migration for planned duration fields in training units and sections.
- Added functions to handle focus areas and style directions in training framework programs.
- Enhanced training planning components to support planned duration input and display.
- Updated frontend components to manage and display planned duration for training units and sections.
2026-05-20 13:02:09 +02:00
ab612a5335 Update Skill Tree Picker and Catalog Functions for Clarity
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 40s
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 1m15s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m14s
- Modified comments in `SkillTreePickerPanel` and `skillCatalogTree.js` to accurately reflect the default expansion behavior, specifying that only main groups are open by default.
- Refactored `defaultExpandedKeysForSkillTree` to simplify the logic, ensuring only main groups are returned as expanded.
- Adjusted tests in `skillCatalogTree.test.js` to validate the updated behavior, confirming that only main groups are opened in the skill tree.
2026-05-20 11:21:18 +02:00
b2f77ca627 Refactor Skill Tree Picker and Catalog Functions for Clarity and Efficiency
Some checks failed
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Has been cancelled
- Simplified the rendering of skill group labels in `SkillTreePickerPanel` for improved readability.
- Updated comments in `skillCatalogTree.js` to clarify the structure of expandable nodes and default expansion behavior.
- Refactored the skill catalog tree building logic to streamline the mapping of categories and skills, enhancing performance and maintainability.
- Adjusted tests in `skillCatalogTree.test.js` to reflect changes in the structure of skill nodes, ensuring accurate validation of functionality.
2026-05-20 11:18:34 +02:00
39b1fd04f0 Enhance Skills Catalog Admin with Unassigned Skill Handling and Improved Selection Logic
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Introduced utility functions to count skills without main categories and categories, enhancing data management in the Skills Catalog Admin.
- Updated the SkillsCatalogAdmin component to handle unassigned main and category IDs, improving user experience when managing skills.
- Refactored skill selection logic to utilize new utility functions, ensuring accurate filtering of skills based on selected categories and main categories.
- Enhanced the UI to display unassigned skills clearly, improving overall usability and clarity in skill management.
2026-05-20 11:16:49 +02:00
9020e5eb16 Enhance Skill Tree Components with Improved Group Labeling and Default Expansion
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Has been cancelled
- Added CSS styles for skill group labels in the skill tree to improve visual hierarchy and readability.
- Updated `SkillTreePickerPanel` and `SkillTreeMultiSelect` components to utilize the new default expansion logic, ensuring main and category nodes are open by default while skill groups remain collapsed.
- Refactored state management in `SkillTreePickerPanel` to align with the new default expansion behavior.
- Enhanced utility functions to support the new default expansion logic for skill trees.
2026-05-20 11:13:34 +02:00
46feb4c867 Add defaultCollapsed prop to SkillTree components for improved user experience
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m15s
- Introduced a `defaultCollapsed` prop in `SkillTreeMultiSelect` and `SkillTreePickerPanel` to control the initial expansion state of skill trees.
- Updated `SkillTreeSelect` to accept a `defaultCollapsed` prop, enhancing flexibility in component usage.
- Adjusted state management in `SkillTreePickerPanel` to respect the `defaultCollapsed` setting, improving user interaction with skill selection.
2026-05-20 11:05:27 +02:00
3067b2e6a8 Implement Skill Tree Selection Components and Update API Calls
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 37s
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 1m15s
- Introduced new `SkillTreeSelect` and `SkillTreeMultiSelect` components for enhanced skill selection in various modals and forms.
- Updated API calls to use `listSkillsCatalog` instead of `listSkills` for improved data retrieval.
- Enhanced CSS styles for skill selection components to improve user experience and visual consistency across the application.
2026-05-20 10:59:17 +02:00
728b37ad5f Export PLANNING_HUB_PATH from navReturnContext.js to enhance navigation context management.
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m14s
2026-05-20 08:01:53 +02:00
8afdd811db Refactor full-page editor navigation and update return context handling
Some checks failed
Deploy Development / deploy (push) Failing after 23s
Test Suite / pytest-backend (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 7s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m19s
- Adjusted `PageFormEditorChrome` to set `showReturn` to false by default, making the back button optional.
- Removed `PageReturnButton` from `TrainingFrameworkProgramEditPage`, `TrainingModuleEditPage`, and `TrainingPlanTemplateEditPage` to streamline navigation.
- Updated documentation to reflect changes in editor actions and return context behavior for improved clarity.
2026-05-20 07:56:22 +02:00
4588ef4c7e Refactor navigation components and enhance return context handling
Some checks failed
Deploy Development / deploy (push) Failing after 24s
Test Suite / pytest-backend (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 7s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Replaced `PageReturnLink` with `PageReturnButton` for consistent back navigation across various pages.
- Updated multiple components, including `ExercisePeekModal`, `PageFormEditorChrome`, and `ExerciseDetailPage`, to utilize the new return context features.
- Enhanced CSS styles for the new return button to improve visual consistency.
- Improved navigation logic in `TrainingFrameworkProgramEditPage` and `TrainingModuleEditPage` to ensure seamless user experience when navigating back to previous locations.
2026-05-20 07:42:46 +02:00
6e6270b717 Enhance navigation and return context in exercise and training module components
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
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 1m17s
- Introduced a new `PageReturnLink` component for consistent back navigation across pages.
- Updated `SaveSelectedExercisesAsModuleModal` and `SaveExercisesAsModuleModal` to utilize `navigateWithAppReturn`, preserving navigation context when redirecting after saving.
- Enhanced `TrainingModuleEditPage` and `TrainingUnitEditPage` with improved return context handling, allowing users to navigate back to their previous locations seamlessly.
- Added CSS styles for the new return link to improve visual consistency and user experience.
2026-05-20 07:25:05 +02:00
14b005e9b8 Enhance exercise selection and display features
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
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 1m18s
- Introduced new CSS styles for exercise cards and selection sections, improving visual feedback for selected exercises.
- Updated ExerciseListCard to support a new `selectionPinned` prop, allowing for a badge display on selected exercises.
- Refactored selection handling in ExercisesListPageRoot to manage selected entries more effectively, replacing the previous Set-based approach.
- Enhanced SaveSelectedExercisesAsModuleModal to support appending exercises to existing modules, improving module management capabilities.
- Updated session state handling to include selected entries, ensuring persistence across sessions.
2026-05-20 07:03:15 +02:00
ef4dd93324 Enhance exercise card interactivity and toolbar functionality
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Added clickable behavior to exercise card body, allowing users to navigate to exercise details by clicking anywhere on the card.
- Introduced keyboard accessibility for exercise cards, enabling navigation via Enter and Space keys.
- Updated ExerciseListBulkToolbar to include a new button for saving selected exercises as a module, enhancing bulk action capabilities.
- Improved CSS styles for clickable exercise card body to indicate interactivity.
2026-05-20 06:56:58 +02:00
e50c18f92e Enhance ExerciseFormPageRoot with save and close functionality
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Added a new `handleSaveAndClose` function to allow users to save and navigate back to the exercise list.
- Updated `performSaveAttempt` to accept a `closeAfter` parameter for conditional navigation.
- Refactored form submission handling to include separate actions for saving and saving with closure.
- Integrated `PageFormEditorChrome` for improved layout and user experience, including a back navigation option.
2026-05-20 06:38:53 +02:00
d19a1061d8 Enhance exercise listing with variant and media counts
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / build-frontend (push) Waiting to run
Test Suite / k6 /health Baseline (push) Waiting to run
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Has been cancelled
- Updated the `list_exercises` function to include counts for exercise variants and media, improving data retrieval for exercise details.
- Added new CSS styles for the exercise card footer to display variant and media statistics in a visually appealing manner.
- Implemented `ExerciseCardContentStats` component to conditionally render variant and media counts, enhancing the user interface of exercise cards.
2026-05-20 06:37:40 +02:00
cb868373f4 Enhance PlanningLayout and TrainingUnitEditPage with unsaved changes handling
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m16s
Test Suite / pytest-backend (pull_request) Successful in 36s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 34s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Updated PlanningLayout to conditionally render the PlanningRouteNav based on the current path, improving navigation for planning unit editors.
- Enhanced TrainingUnitEditPage with unsaved changes detection, integrating a prompt for users to confirm before leaving the page with unsaved changes.
- Introduced utility functions for creating a stable snapshot of form data to facilitate dirty-checking, ensuring better user experience during form editing.
- Added tests for the new utility functions to validate their behavior in various scenarios.
2026-05-19 14:39:46 +02:00
472cf1afdb Refactor full-page editor layout and enhance mobile dock functionality
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 37s
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 1m20s
- Updated CSS styles for the full-page editor, modifying the header to include a back link and title, while fixing the action dock for all viewports.
- Removed the sticky header and integrated the action bar at the bottom for improved usability across devices.
- Simplified the PageFormEditorChrome component by eliminating unnecessary memoization and restructuring the header layout.
2026-05-19 12:55:08 +02:00
0cb0e81d27 Add AuthContext to App component for authentication management
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s
2026-05-19 12:45:08 +02:00
6a9351874f Enhance full-page editor layout and integrate form editor actions
Some checks failed
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Failing after 6m4s
- Updated CSS styles for the full-page editor, introducing a sticky header and mobile dock for improved navigation.
- Refactored App component to include FormEditorActionsProvider and FormEditorBottomSlot for better form handling.
- Simplified TrainingUnitFormShell by removing the FormActionBar, streamlining the form structure and enhancing usability.
2026-05-19 11:18:54 +02:00
734d943d73 Implement full-page planning unit editor layout and enhance form behavior
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added new CSS styles for the planning unit editor to manage scrolling and layout, ensuring a responsive design.
- Updated the TrainingUnitFormShell component to include new class names for better styling and structure.
- Modified the TrainingUnitEditPage to support the new layout, improving user experience when editing training units.
2026-05-19 11:13:18 +02:00
16eaf839e7 Enhance frontend testing setup and refactor TrainingPlanningPageRoot component
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 19s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m21s
- Added Vitest as a testing framework and included test scripts in package.json for improved testing capabilities.
- Refactored TrainingPlanningPageRoot component by removing unused state variables and imports, streamlining the code for better readability and performance.
- Introduced new utility functions for planning routes to enhance navigation within the training planning interface.
2026-05-19 11:02:03 +02:00
295c7e7efc Refactor modal components to use FormModalOverlay for improved consistency and functionality
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m9s
- Replaced div elements with FormModalOverlay in SaveExercisesAsModuleModal, TrainingPlanningUnitFormModal, and TrainingPublishToFrameworkModal for a unified modal structure.
- Enhanced modal styling and behavior, including adjustments for responsiveness and accessibility.
- Introduced new CSS rules to manage overflow and scrolling behavior when modals are active, improving user experience across devices.
2026-05-19 10:47:44 +02:00
c9175bd2fd Refactor modal components and enhance FormActionBar functionality
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Updated modal components to utilize a consistent overlay and panel structure, improving layout and responsiveness.
- Enhanced FormActionBar to support short labels for action buttons, improving usability in mobile views.
- Introduced new styling for action buttons and modal titles, ensuring better alignment and visual consistency across forms.
- Improved accessibility by adding aria-labels and titles to buttons for better screen reader support.
2026-05-19 10:20:49 +02:00
f15aa7c415 Update version to 0.8.148 and enhance training plan template functionality
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
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 1m7s
- Incremented app version to 0.8.148 and updated changelog to reflect new features.
- Improved the training plan template structure by adding a preview of sections, including support for split sessions.
- Introduced a new editing page for training plan templates, allowing users to modify templates directly.
- Enhanced the TrainingPlanningPageRoot to include a description field when saving templates, improving user guidance.
- Updated permissions to allow editing of training plan templates based on user roles.
2026-05-19 10:13:26 +02:00
1684892bcb Fix layout in TrainingPlanningUnitFormModal by closing the div tag for improved structure and rendering.
All checks were successful
Deploy Development / deploy (push) Successful in 41s
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 34s
Test Suite / playwright-tests (push) Successful in 1m11s
2026-05-19 10:05:33 +02:00
4fee5a2b47 Implement planning layout and enhance training module functionality
Some checks failed
Deploy Development / deploy (push) Failing after 23s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 6s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Has been cancelled
- Added a new PlanningLayout component to manage the training planning interface, allowing for better organization of related pages.
- Introduced a FormActionBar component across various modals and forms to standardize action buttons for saving and canceling.
- Updated the TrainingPlanningPageRoot and TrainingPlanningUnitFormModal to utilize the new FormActionBar for improved user experience.
- Enhanced the TrainingModuleEditPage and TrainingFrameworkProgramEditPage with save and close functionality, streamlining the editing process.
- Refactored existing modals to incorporate the new layout and action bar, ensuring consistency across the application.
2026-05-19 10:02:39 +02:00
82705f0c3e Update version to 0.8.146 and add changelog entry for new features
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m12s
- Incremented app version to 0.8.146 and updated changelog to include the new version details.
- Documented the addition of the publish-to-framework feature for training units, enhancing the training planning capabilities.
2026-05-19 09:45:37 +02:00
a51f794945 Update version to 0.8.147 and add functionality to save exercises as training modules
Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Has been cancelled
- Incremented app version to 0.8.147 and updated changelog to reflect the new version.
- Introduced a new modal for saving exercises as training modules within the training planning interface.
- Enhanced the TrainingPlanningPageRoot component to manage the new save module functionality, including state management for the modal.
- Updated the TrainingPlanningUnitFormModal to include an option for saving exercises as a module, improving user experience in training planning.
2026-05-19 09:41:27 +02:00
7693139242 Update version to 0.8.146 and implement publish-to-framework feature for training units
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m19s
- Incremented app version to 0.8.146 and updated build date to 2026-05-19.
- Added new API endpoint to publish training units as session blueprints to framework programs.
- Introduced frontend functionality to support publishing training units, including a modal for user interaction.
- Updated changelog to reflect the new feature and its associated changes.
2026-05-19 08:51:48 +02:00
623af621b4 Enhance MediaWiki import functionality with category normalization and skill attributes
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 40s
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 1m17s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
- Introduced `_normalize_mw_category` function to clean category names for API calls, ensuring consistent handling of category prefixes.
- Updated `SmwClient` methods to utilize normalized category names, improving data retrieval accuracy.
- Added `_wiki_category_or_default` function to provide default categories based on import type, enhancing user experience during imports.
- Integrated new fields `karate_relevance` and `relevance_level` into various admin components, allowing for better skill management.
- Incremented app version to 0.8.145 and updated changelog to reflect these changes.
2026-05-16 11:05:15 +02:00
949a77fe38 Enhance skill model and import functionality with karate relevance and relevance level
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 15s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m11s
- Added `karate_relevance` and `relevance_level` fields to the SkillCreate and SkillResponse models, allowing for more detailed skill attributes.
- Updated the SMW property mapping to include these new fields, facilitating their integration during data import.
- Implemented parsing logic for relevance levels from Wiki data, ensuring proper handling of values between 1 and 3.
- Modified the upsert and create skill functions to support the new fields, ensuring they are correctly stored and updated in the database.
- Incremented app version to 0.8.143 and updated changelog to reflect these changes.
2026-05-16 10:56:15 +02:00
0275f76432 Implement RBAC for library content management in club tenancy
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Has been cancelled
- Introduced new functions for managing edit, delete, and governance transition permissions for library content, aligning with role-based access control (RBAC) principles.
- Updated existing routers to utilize these new functions, ensuring consistent permission checks across training frameworks, modules, and progression graphs.
- Enhanced visibility and governance handling for training plan templates and library content, improving overall content management and user experience.
- Incremented app version to 0.8.142 and updated changelog to reflect these changes.
2026-05-16 10:53:00 +02:00
bc1790bd82 Refactor section movement logic in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m14s
- Streamlined the section movement process by consolidating validation checks and enhancing the handling of parallel phase indices.
- Improved the overall clarity and efficiency of the section management functionality, ensuring a smoother user experience during edits.
2026-05-16 09:04:09 +02:00
8c07cf36ee Enhance section movement functionality in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
Test Suite / pytest-backend (pull_request) Successful in 34s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
- Updated the onMoveSectionsAcrossSlots function to support additional parameters for improved section movement across slots, including handling for parallel phase indices and insertion points.
- Refined logic for moving sections between slots, ensuring proper handling of parallel streams and enhancing the overall section management experience.
2026-05-16 08:34:30 +02:00
7d2661a8e8 Remove redundant boundary checks for section movement in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Eliminated the checks that prevented section movement across slots when crossing the boundary between 'parallel' and 'whole_group' phases, streamlining the section management logic and improving code clarity.
2026-05-16 08:17:35 +02:00
0fdee610ed Enhance section movement validation in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m8s
- Added a new check to prevent section movement across slots that crosses the boundary between 'parallel' and 'whole_group' phases, improving the logic for section management and ensuring valid operations during edits.
2026-05-16 08:09:24 +02:00
f1c470a8a3 Improve section movement validation in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 38s
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
- Added a check to ensure that the source slot is not the same as the target slot when moving sections across slots, enhancing the logic for section management and preventing unnecessary operations.
2026-05-16 08:05:10 +02:00
736656bde8 Refactor enrichFrameworkSlotSections to improve section handling in training framework
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
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 1m8s
- Updated the enrichFrameworkSlotSections function to utilize default sections when no sections are provided, enhancing data integrity.
- Simplified the normalization process by directly using base sections, improving code clarity and maintainability.
2026-05-16 07:53:05 +02:00
e441f59bff Add delete functionality for training plan templates
All checks were successful
Deploy Development / deploy (push) Successful in 39s
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 1m22s
2026-05-16 07:45:53 +02:00
c3eb5a62c4 Update version to 0.8.141 and enhance training plan template handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 37s
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 1m13s
- Incremented app version to 0.8.141 and updated build date to 2026-05-14.
- Modified the planning module version to 0.12.0, improving template section handling with phase metadata.
- Introduced new functions for normalizing and inserting training plan template sections, ensuring accurate phase representation during saves.
- Updated frontend components to utilize new utility functions for managing training plan templates, enhancing user experience and data integrity.
2026-05-16 07:41:08 +02:00
79e748b470 Refactor phase handling in training unit sections for improved data integrity
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Introduced canonicalization for plan locations in phased saves to ensure consistent phase representation.
- Enhanced the `inheritPlanLocForPhasedSave` function to utilize the new canonicalization logic, improving data flow.
- Updated payload building logic to check for canonicalized plan locations, ensuring accurate phase detection during saves.
2026-05-16 07:35:35 +02:00
88c4201f80 Refactor training framework to improve phase management and user experience
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
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 1m8s
- Enhanced phase handling in training unit hydration and insertion processes, ensuring better data integrity.
- Updated frontend components to support phase representation in training framework slots.
- Improved user interface controls for managing parallel phases, optimizing user experience during training program edits.
- Refactored payload building functions to accommodate phase adjustments, enhancing save functionality for training plans.
2026-05-16 07:27:18 +02:00
6e1cc62065 Enhance training framework with phase handling and payload adjustments
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m9s
- Updated backend logic to include phases in training unit hydration and insertion processes, improving data integrity.
- Modified frontend components to support phases in training framework slots, ensuring consistent data representation.
- Refactored payload building functions to accommodate phases, enhancing the save functionality for training plans.
- Improved user interface to enable controls for parallel phases, optimizing the user experience during training program edits.
2026-05-16 07:02:12 +02:00
76cc81a385 Update project documentation and enhance training features for parallel streams
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 20s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Updated CLAUDE.md and PROJECT_STATUS.md to reflect the latest app version (0.8.140) and database schema (20260515063) as of 2026-05-14.
- Enhanced DOMAIN_MODEL.md and PARALLEL_TRAINING_STREAMS_CONCEPT.md to clarify the implementation of phases and parallel streams in training units.
- Improved HANDOVER.md with detailed descriptions of the coaching and breakout functionalities, including rejoin logic and session management.
- Updated FACHLICHE_NUTZERFUNKTIONEN.md to include new features related to training planning and execution, emphasizing the integration of phases and parallel streams.
- Revised FEATURES_DELIVERED_2026-Q2.md to document the latest changes and improvements in the training framework and media management.
2026-05-15 22:11:05 +02:00
a4f11a8225 Enhance TrainingCoachPage and trainingPlanUtils with split rejoin transition logic
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m11s
Test Suite / pytest-backend (pull_request) Successful in 34s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 34s
Test Suite / playwright-tests (pull_request) Successful in 1m20s
- Introduced a new utility function to determine when to prompt for a split rejoin transition between phases, improving user guidance during training sessions.
- Updated TrainingCoachPage to incorporate this logic, enhancing the flow of navigation through training timelines.
- Refactored button actions to provide clearer options for users when managing group transitions, optimizing the user experience during training.
2026-05-15 18:52:27 +02:00
5e5350d5ac Enhance TrainingCoachPage and trainingPlanUtils with session storage management and phase handling improvements
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
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 1m8s
- Introduced a new function in TrainingCoachPage to clear session storage for coach steps, improving state management during training sessions.
- Updated the sectionsWithPlanLocForDisplay function in trainingPlanUtils to include logic for determining the maximum phase order index, enhancing phase handling.
- Enhanced the mapping of sections to include default plan locations for parallel phases, optimizing data representation and user experience.
2026-05-15 17:01:00 +02:00
73ac2218c7 Enhance TrainingCoachPage and trainingPlanUtils with split rejoin functionality
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
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 1m8s
- Updated TrainingCoachPage to implement a prompt for users to rejoin parallel phases, improving user guidance during training sessions.
- Refactored step management logic to ensure accurate navigation through the timeline, utilizing safe step calculations.
- Introduced new utility functions in trainingPlanUtils for building save payloads and determining when to prompt for split rejoin, optimizing data handling.
- Enhanced state management for split rejoin prompts, ensuring a seamless user experience during training.
2026-05-15 16:49:05 +02:00
352237bbb9 Refactor TrainingCoachPage and TrainingUnitRunPage to enhance coach branching functionality
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Updated TrainingCoachPage to implement branching logic for coach steps, allowing for improved navigation through training phases.
- Enhanced session storage handling to manage branch picks and streamline state management during training sessions.
- Modified TrainingUnitRunPage to update links for coaching views, reflecting the new branching structure and improving user experience.
- Introduced new utility functions in trainingPlanUtils for managing coach branch picks and timeline navigation, optimizing data handling across components.
2026-05-15 16:31:54 +02:00
4cf7133bce Enhance TrainingCoachPage and TrainingUnitRunPage with improved coach focus handling and UI updates
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m8s
- Updated TrainingCoachPage to incorporate coach focus parameters from search queries, allowing for more precise control over displayed training streams.
- Refactored session storage handling to better manage state related to coach focus, ensuring accurate step tracking during training sessions.
- Enhanced TrainingUnitRunPage with improved layout for stream titles and added links for direct navigation to coaching views, improving user experience.
- Introduced new utility functions in trainingPlanUtils for managing coach stream focus options and duration overrides, streamlining data handling across components.
2026-05-15 16:08:01 +02:00
c182ced7cd Refactor TrainingUnitRunPage and trainingPlanUtils for improved section handling
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m26s
- Updated TrainingUnitRunPage to utilize new utility functions for managing sections with plan locations, enhancing data representation.
- Refactored trainingPlanUtils to introduce functions for building view models from sections and for displaying sections with plan locations, streamlining the data flow.
- Improved logic for handling phase runs and section indices, ensuring accurate representation of training units during rendering.
2026-05-15 14:00:46 +02:00
5338871f36 Enhance TrainingCoachPage and TrainingUnitRunPage with improved context display and print functionality
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 37s
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 1m8s
- Updated TrainingCoachPage to include coach context in timeline entries and section titles, enhancing clarity for users.
- Refactored TrainingUnitRunPage to support printing options for parallel streams, allowing for better organization during printouts.
- Introduced new CSS styles for page breaks and layout adjustments in app.css, improving print formatting for training plans.
- Enhanced utility functions in trainingPlanUtils.js to support new phase and stream management features, streamlining data handling.
2026-05-15 12:21:08 +02:00
3005f1cb3e Refactor TrainingUnitSectionsEditor to support new parallel stream functionality
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 41s
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 1m8s
- Renamed functions and parameters to clarify the handling of sections within parallel streams, enhancing code readability.
- Updated drag-and-drop event handlers to accommodate additional parameters for managing sections in parallel streams.
- Introduced a new utility function to reorder sections as the first entry in a parallel stream, improving section management.
- Enhanced the visual representation of drop zones for sections, ensuring a better user experience during reordering operations.
2026-05-15 12:10:44 +02:00
72e8f31cff Enhance TrainingUnitSectionsEditor with new drag-and-drop functionality for parallel phases
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Added new event handlers for drag-and-drop operations to manage sections above and below split headers in parallel phases.
- Implemented utility functions to reorder sections as whole groups or as the first entry in parallel phases, improving section management.
- Updated CSS styles to visually represent new drop zones for sections, enhancing user experience during reordering.
- Refactored existing logic to accommodate new features and ensure proper handling of section placements within parallel streams.
2026-05-15 10:59:30 +02:00
73975d3402 Enhance TrainingUnitSectionsEditor and TrainingFrameworkProgramEditPage with parallel stream support
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Updated the TrainingUnitSectionsEditor to include additional parameters for moving sections across slots, allowing for better handling of parallel streams.
- Improved the removeSection function to ensure proper reordering and handling of sections in parallel phases.
- Enhanced the moveSectionsAcrossFrameworkSlots function to apply changes to sections when moved into parallel streams, ensuring accurate updates.
- Refactored the reorderBlocksImmutableWithPlanLoc function to accommodate new logic for managing phase orders and section types during reordering.
2026-05-15 10:21:15 +02:00
4902771772 Enhance TrainingUnitSectionsEditor with improved section visibility logic
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
- Added logic to determine visibility of drop bands for sections in parallel phases, enhancing user experience during reordering.
- Updated the reorderBlocksImmutableWithPlanLoc function to better handle plan location adjustments based on section types and their positions.
- Improved handling of section indices for parallel streams, ensuring accurate representation of active streams and their corresponding sections.
2026-05-15 10:14:17 +02:00
c2efbee4ee Implement drag-and-drop functionality for parallel stream sections in TrainingUnitSectionsEditor
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 1s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Added new CSS styles for visual feedback during drag-and-drop operations, enhancing user experience.
- Introduced logic to manage section drops into parallel streams, allowing for intuitive reordering of sections.
- Implemented utility functions to facilitate the movement of sections within parallel streams, ensuring proper plan location adjustments.
- Updated the TrainingUnitSectionsEditor component to utilize the new drag-and-drop features, improving overall functionality and usability.
2026-05-15 09:10:54 +02:00
514b64682c Implement visual enhancements and logic for section drop bands in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Added CSS styles for visual representation of section drop bands, differentiating between split and whole group regions.
- Introduced a new function to determine the appropriate drop band class based on section types, improving user experience during section reordering.
- Enhanced drag-and-drop functionality to support parallel phase management, allowing for more intuitive section placement within the editor.
- Updated utility functions to handle the movement of parallel phase runs, ensuring proper plan location adjustments during reordering.
2026-05-15 08:37:16 +02:00
a0a0be8bef Enhance TrainingUnitSectionsEditor with advanced phase management features
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Introduced new utility functions for managing phase runs, including the ability to swap adjacent phase runs and move phase runs up or down by their order.
- Updated the TrainingUnitSectionsEditor to incorporate these new functions, allowing for improved reordering of sections within parallel phases.
- Enhanced the moveSection logic to support movement across different phase types, ensuring better user experience when managing sections.
- Refactored the reorderBlocksImmutable function to accommodate plan location adjustments during section reordering, improving overall functionality.
2026-05-15 08:20:43 +02:00
613fedfaff Refactor training phase handling in backend and enhance TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m7s
- Updated the backend logic to ensure strict ordering of phase indices, preventing UNIQUE constraint violations when phases are duplicated.
- Enhanced the TrainingUnitSectionsEditor component with new state management for editing phase titles and stream names, improving user interaction.
- Implemented conditional rendering for input fields to facilitate inline editing of phase titles and stream names, streamlining the editing process.
2026-05-15 08:02:18 +02:00
2e761161ef Enhance TrainingUnitSectionsEditor with new section management features
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m7s
- Introduced functions to add and manage parallel phases and sections, allowing for more flexible training unit configurations.
- Implemented logic to handle the addition of whole group sections and parallel streams, improving the user experience in the editor.
- Added utility functions for reordering sections and checking for content within parallel stream buckets.
- Updated state management to ensure proper handling of section titles and removal of streams, enhancing overall functionality.
2026-05-15 07:50:16 +02:00
0a203aaf75 Enhance TrainingUnitSectionsEditor with parallel phase management features
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
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 1m10s
- Introduced logic to limit the number of streams per parallel phase, ensuring compliance with the defined maximum.
- Added utility functions for managing stream indices and visual representation of streams.
- Implemented section movement within parallel streams, allowing for reordering while maintaining stream integrity.
- Updated UI components to reflect changes in stream handling, including disabling buttons when limits are reached.
- Enhanced state management for parallel stream tabs, improving user experience in navigating between streams.
2026-05-15 07:37:51 +02:00
f50e9db523 chore(version): update version and changelog for release 0.8.140
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
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 1m8s
- Bumped APP_VERSION to 0.8.140 and updated the changelog to reflect recent changes.
- Enhanced the Training Planning Module with new controls for managing whole group and parallel phases, including the ability to add streams to existing parallel phases.
- Introduced utility functions for handling phase and stream configurations, improving the overall structure and usability of the training unit sections editor.
- Updated the TrainingPlanningUnitFormModal to support the new phase controls, ensuring seamless integration with the frontend components.
2026-05-15 07:25:59 +02:00
749c185e3d chore(version): update version and changelog for release 0.8.139
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m11s
- Bumped APP_VERSION to 0.8.139 and updated the changelog to reflect recent changes.
- Enhanced the Training Planning Module to support new phase handling, including improved labeling for sections in the editor.
- Updated the API payload structure to accommodate parallel streams and phases, ensuring better integration with the frontend components.
- Refactored utility functions for improved clarity and maintainability in handling training unit sections and phases.
2026-05-15 07:12:43 +02:00
214f90d39b chore(version): update version and changelog for release 0.8.138
All checks were successful
Deploy Development / deploy (push) Successful in 39s
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 1m9s
- Bumped APP_VERSION to 0.8.138 and updated the changelog to reflect recent changes.
- Enhanced training unit planning with support for POST/PUT requests including phases and parallel streams.
- Fixed role assignment validation for stream co-trainers and added integration tests for phase handling.
- Updated the training planning API to improve data structure and retrieval for nested phases and sections.
2026-05-15 07:04:24 +02:00
357 changed files with 76106 additions and 3648 deletions

View File

@ -1,8 +1,8 @@
# Shinkan Jinkendo - Projekt-Status # Shinkan Jinkendo - Projekt-Status
**Stand:** 2026-05-12 **Stand:** 2026-05-14
**Version (Code):** 0.8.110 (`backend/version.py`, APP_VERSION) **Version (Code):** 0.8.140 (`backend/version.py`, APP_VERSION)
**DB-Schema-Version:** `20260512057` (`backend/version.py`, DB_SCHEMA_VERSION) **DB-Schema-Version:** `20260515063` (`backend/version.py`, DB_SCHEMA_VERSION)
**Branch:** develop **Branch:** develop
--- ---
@ -15,7 +15,7 @@
**Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent). **Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent).
**Parallel weiter relevant:** **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.1370.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Planungs-KI Progressionsgraph** (Roadmap-first, Auto-Optimierung, Katalog-Kontext **0.8.233**, **F15** Match-Dialog + getrennte Pfad-QS lokal): Ist-Doku **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**, Handover **`docs/HANDOVER.md`** §2.8.
**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md) **Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md)
@ -36,7 +36,8 @@
1. KalenderUI: „Aus Rahmen übernehmen“ an **`from-framework-slot`** anbinden; ggf. Bulk. 1. KalenderUI: „Aus Rahmen übernehmen“ an **`from-framework-slot`** anbinden; ggf. Bulk.
2. Governance: Sichtbarkeit **club/official** für Rahmen so ausprägen, dass andere Trainer kopieren dürfen (Policy + API). 2. Governance: Sichtbarkeit **club/official** für Rahmen so ausprägen, dass andere Trainer kopieren dürfen (Policy + API).
3. Optional Backlog Graph: Alternativgruppen / bessere Visualisierung (**§4**). 3. Optional Backlog Graph: Alternativgruppen / bessere Visualisierung (**§4**).
4. **Kombinationsübungen / Coach (Fachspez §10.6):** Coach **Stufe B/C** (archetypgesteuerte Durchführung); **Archetyp-Verwaltung** jenseits Code-Konstanten; **Massen-Vorbelegung** aller Slot-Zeit/Anzahl-Felder; **serverseitige** Validierung Profil ↔ Archetyp — siehe `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Pakete **4e4g**) und `COMBINATION_TIMING_PROFILE_PLAN.md`. 4. **Breakout / Coaching (Arbeitspaket):** Backend-Konsistenz `phases`↔`sections`, Run-UI vs. Spec (Stream-Tabs), Vorlagen phasenfähig, E2E-Smoke — siehe **`docs/HANDOVER.md`** (Tabelle „Coaching & Breakout“).
5. **Kombinationsübungen / Coach (Fachspez §10.6):** Coach **Stufe B/C** (archetypgesteuerte Durchführung); **Archetyp-Verwaltung** jenseits Code-Konstanten; **Massen-Vorbelegung** aller Slot-Zeit/Anzahl-Felder; **serverseitige** Validierung Profil ↔ Archetyp — siehe `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Pakete **4e4g**) und `COMBINATION_TIMING_PROFILE_PLAN.md`.
--- ---
@ -82,7 +83,9 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
- [x] **Varianten** (CRUD, Reorder, Voraussetzung) + Anzeige im Detail - [x] **Varianten** (CRUD, Reorder, Voraussetzung) + Anzeige im Detail
- [x] **Progressionsgraph zwischen Übungen** (Bibliotheks-Container, Kanten, Sequenz-Bulk, Varianten-Knoten — Zwischenstand, siehe TRAINING_FRAMEWORK_SPEC §4) - [x] **Progressionsgraph zwischen Übungen** (Bibliotheks-Container, Kanten, Sequenz-Bulk, Varianten-Knoten — Zwischenstand, siehe TRAINING_FRAMEWORK_SPEC §4)
- [x] Medien (Upload/Embed, rollenabhängige Größenlimits) - [x] Medien (Upload/Embed, rollenabhängige Größenlimits)
- [x] Suche & Filter (Multi-Filter, Chips, Fokus beim Suchen) - [x] Suche & Filter (Multi-Filter, Chips, Fokus beim Suchen; **Freigabelevel** als UI-Begriff für `visibility`)
- [x] **Übungsformular:** Registerkarten (Stammdaten … Medien & Mehr), kompakte Chip-Editoren, Varianten-Speichern über Aktionsleiste
- [x] **Fähigkeiten-Intensität** ohne Primär-Flag (`niedrig`/`mittel`/`hoch`; Backend `is_primary` immer false)
- [x] Exercise Blocks (Bausteine) - [x] Exercise Blocks (Bausteine)
- [x] Saved Searches (wo implementiert) - [x] Saved Searches (wo implementiert)
@ -92,6 +95,8 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
- [x] **Optionale Zuordnung einer Übungsvariante** pro Eintrag (`exercise_variant_id`) - [x] **Optionale Zuordnung einer Übungsvariante** pro Eintrag (`exercise_variant_id`)
- [x] **Trainingsrahmenprogramm Bibliothek** (Ziele, Slots, Kontext) + **SlotBlueprints** in `training_units` (036037) - [x] **Trainingsrahmenprogramm Bibliothek** (Ziele, Slots, Kontext) + **SlotBlueprints** in `training_units` (036037)
- [x] **Materialisierung** aus RahmenSlot (`POST …/training-units/from-framework-slot`; UIAnbindung optional) - [x] **Materialisierung** aus RahmenSlot (`POST …/training-units/from-framework-slot`; UIAnbindung optional)
- [x] **Phasenmodell & parallele Streams** pro Einheit (Migration **063**): `training_unit_phases`, `training_unit_parallel_streams`; GET mit **`phases`** + flachen **`sections`**; PUT mit **`phases`** (App **0.8.1370.8.140**)
- [x] **Coaching-Modus** für Breakout: Timeline mit Split-Wahl, Rejoin vor Ganzgruppe/nächstem Split, Nachbereitung speichern → Plan & Ablauf (`TrainingCoachPage`, `trainingPlanUtils.js`)
- [ ] Kalender-View / erweiterte Roadmap (Backlog) - [ ] Kalender-View / erweiterte Roadmap (Backlog)
**MediaWiki Import:** **MediaWiki Import:**
@ -101,6 +106,7 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
**Skills-System:** **Skills-System:**
- [x] Hierarchisches Schema, Fokusbereich-Zuordnung, Exercise-Skill mit Levels - [x] Hierarchisches Schema, Fokusbereich-Zuordnung, Exercise-Skill mit Levels
- [x] **Gewichtetes Fähigkeiten-Profil (Phase 3):** Module, Rahmenprogramme, Regressionspfade; Peer-Kontext getrennt; Listen-Filter + Discovery — **`technical/SKILL_SCORING_SPEC.md`**
**Admin-UI:** **Admin-UI:**
@ -155,18 +161,19 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes
| Dokument | Pfad | Stand | Status | | Dokument | Pfad | Stand | Status |
|----------|------|-------|--------| |----------|------|-------|--------|
| Fachliche Nutzerfunktionen (Design/Product) | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | 2026-05-12 | neu, Ist-Überblick | | Fachliche Nutzerfunktionen (Design/Product) | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | 2026-05-14 | Phasen/Coach/Rejoin |
| Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-12 | Verweis Version siehe `version.py` | | Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-14 | §11a Breakout |
| Trainingsrahmen + Graph | `technical/TRAINING_FRAMEWORK_SPEC.md` | 2026-05-05 | ✅ §2 Blueprint | | Trainingsrahmen + Graph | `technical/TRAINING_FRAMEWORK_SPEC.md` | 2026-05-05 | ✅ §2 Blueprint |
| Anforderungen (Index) | `functional/SHINKAN_REQUIREMENTS.md` | 2026-05-12 | Verweis Nutzerüberblick | | Anforderungen (Index) | `functional/SHINKAN_REQUIREMENTS.md` | 2026-05-12 | Verweis Nutzerüberblick |
| Database Schema | `technical/DATABASE_SCHEMA.md` | 2026-05-07 | ✅ Hinweis 040046 Medien (Kurz) | | Database Schema | `technical/DATABASE_SCHEMA.md` | 2026-05-07 | ✅ Hinweis 040046 Medien (Kurz) |
| Domain Model | `functional/DOMAIN_MODEL.md` | 2026-05-12 | Version 0.4.5, Verweis Nutzerüberblick | | Domain Model | `functional/DOMAIN_MODEL.md` | 2026-05-14 | Parallele Streams Ist 063 |
| API Übungen | `technical/EXERCISES_API_SPEC.md` | 2026-05-08 | ✅ Medien/Inline-Workflow ergänzt | | API Übungen | `technical/EXERCISES_API_SPEC.md` | 2026-05-08 | ✅ Medien/Inline-Workflow ergänzt |
| Frontend Routing | `technical/EXERCISES_FRONTEND_ROUTING.md` | 2026-04-30 | ✅ Ergänzt UI-Hinweise | | Frontend Routing | `technical/EXERCISES_FRONTEND_ROUTING.md` | 2026-04-30 | ✅ Ergänzt UI-Hinweise |
| Search & Filter | `technical/SEARCH_FILTER_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Liste UX) | | Search & Filter | `technical/SEARCH_FILTER_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Liste UX) |
| Media Upload | `technical/MEDIA_UPLOAD_SPEC.md` | 2026-05-07 | ✅ Verweis Archiv/Inline | | Media Upload | `technical/MEDIA_UPLOAD_SPEC.md` | 2026-05-07 | ✅ Verweis Archiv/Inline |
| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | 2026-05-08 | ✅ Ist-Changelog + §11 Inline erweitert | | Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | 2026-05-08 | ✅ Ist-Changelog + §11 Inline erweitert |
| Projektstatus | `PROJECT_STATUS.md` | 2026-05-12 | auf 0.8.96 + P-13/P-01 + Nutzerüberblick | | Parallele Streams (Fach/Technik) | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | 2026-05-14 | Ist-Stand P1 teils |
| Projektstatus | `PROJECT_STATUS.md` | 2026-05-14 | Keyset, KPIs, Breakout/Coach Kurzverweis |
--- ---
@ -177,4 +184,4 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes
--- ---
**Letzte Aktualisierung:** 2026-05-12 (Version 0.8.96, Executive Summary P-13/P-01, `docs/FACHLICHE_NUTZERFUNKTIONEN.md`) **Letzte Aktualisierung:** 2026-05-14 (Version 0.8.140, DB 063, Handover Coaching/Breakout)

View File

@ -0,0 +1,100 @@
# KI-Unterstützung bei Übungen Produkt-Vision
**Version:** 0.1
**Datum:** 2026-05-22
**Status:** Zielbild / Anforderungsgrundlage (nicht gleich Ist-Spec technische Schnittstellen: **`technical/KI_FEATURES_SPEC.md`**, **`technical/AI_PROMPT_SYSTEM_SPEC.md`**, **`technical/AI_TRAINING_PLANNING_CONCEPT.md` §1.1**)
**Zielgruppe:** Product, Trainer-UX, später Admin-Werkzeuge
---
## 1. Übergeordnete Prinzipien
1. **Immer Vorschlag, nie blind überschreiben**
Die KI liefert **Vorschläge** (Änderungen, Ergänzungen, Strukturen). Bestehende Inhalte werden **nicht** still ersetzt. Übernahme erfolgt durch den Nutzer: **teilweise** (Felder/Stellen/Blöcke) oder **komplett** („Vorschlag gesamt akzeptieren“).
2. **Granulare Anforderung im Editor**
Innerhalb einer Übung soll KI-Unterstützung **feldbasiert oder bereichsbasiert** auslösbar sein (z.B. nur „Anleitung schärfen“, nur „Fähigkeiten“, nur „Variantenrahmen“) **oder** als **Komplettüberarbeitung** mit klarem Warnhinweis (Umfang/transparenter Diff).
3. **Nachweisliche Herkunft**
Übernommene KI-Inhalte werden technisch dort abgebildet, wo bereits vorgesehen (z.B. **`summary_ai_generated`**, **`exercise_skills.ai_suggested`**) und um analogen Hinweis für weitergehende Textfelder/Varianten **erweitert**, sobald Implementierung konkret wird.
---
## 2. Funktionsbereiche (Vision)
### 2.1 Von der Idee zur kompletten Übung („Zielausbau“)
**Einstieg minimal:** Kurzbeschreibung oder Stichwort, **Ziel** („was soll erreicht werden?“), wenige **Rahmenparameter** (z.B. Fokusbereich, Trainingszeit, Teilnehmerzahl, Alter, Platzausstattung, Sicherheitshinweise konkrete Dropdowns/Freifelder in UX später festlegen).
**KI-Aufgabe:** aus diesem dünnen Kontext einen **übernehmbaren Entwurf** einer **ganzen Übung** erzeugen: TitelVorschlag, Ziel-/Durchführungstext, Sicherheit/Organisation, ggf. Trainerhinweise **immer als Vorschlagspaket**, nicht als Speicher ohne Bestätigung.
**Abgrenzung:** Kombinationsübungen / komplexe Methodenprofile können **phasenweise** später einbezogen werden (Verweis Fachspez Trainingsmodule).
### 2.2 Anleitung (Durchführung / „Ausführung“) maximal hilfreich
**Ziel:** Die **Ausführungs-/Anleitungsbereiche** sollen sich **didaktisch klar**, **teilbar** und **wieder verwendbar** lesen ohne den Trainer zu entmindigen.
**KI-Aufgabe:** Überarbeitungsvorschlag für Struktur (nummerierte Schritte, Zeiten pro Block, häufige Fehler, Progressionshinweise innerhalb der Übung wo sinnvoll). **Selektiver** Aufruf: nur diese Felder oder nur ein markierter Abschnitt (wenn UX Textauswahl unterstützt).
### 2.3 Kurzbeschreibung (`summary`)
**KI-Aufgabe:** Aus den **relevanten Übungstexten** eine **Liste-/Karte-taugliche** Kurzfassung generieren — wie in **`KI_FEATURES_SPEC.md`** beschrieben, mit **Ablehnen / Bearbeiten / Übernehmen**.
### 2.4 Einordnung primär **Fähigkeiten**
**KI-Aufgabe:** automatische Erkennung und **Zuordnung** zum **globale Skills-Katalog** inklusive:
- **Intensität** (`exercise_skills`)
- **Skill-Level**: `required_level` / `target_level` nach **kanonischen Slugs** (Backend-konform)
- **`is_primary`** / Priorisierung wo fachlich sinnvoll
**Prompt-Kontext für Qualität:** Stammfelder wie `skills.description`, **`karate_relevance`**, **`relevance_level`**, **`focus_areas`**, optional **`skill_level_definitions`** nur für eine **kurze Kandidatenliste** (zweite Runde möglich) keine vollständigen Romane für den gesamten Katalog auf einmal.
### 2.5 Varianten (optional, später prioritär erwägenswert)
**Vision:** Aus Ziel-/Durchführungstext **mehrere sinnvolle Ausprägungen** als **Übungsvarianten** vorschlagen oder einzelne erzeugen (**progression**, **Schwierigkeit**, andere Paararbeit, Gerätevariation) mit **übernehmbarem** Datenmodell gleich dem bestehenden `exercise_variants`.
**Randbedingungen:** Validierung gegen Übungstyp (Kombinationsübungen ohne Varianten laut Produktstand), keine Halluzination fremder IDs.
---
## 3. Kontextbezug später: Nachbearbeitung aus der Trainingsplanung
**Vision:** Hinweise aus der **Nachbearbeitung** einer Trainingseinheit (IstMinuten, Trainer-Notizen, Abweichungen „was lief nicht?“ je nach Datenmodell) fließen **optional** als Kontext in eine **erneute KI-Überarbeitung der betroffenen Übung** ein („Übung aus den Erfahrungen der Gruppe verbessern“).
**Konsequenz technisch später:** Zugriffsrechte, Mandant, keine unzulässige Verknüpfung personenbezogener Sportlerdaten; Aggregation auf **Einheit-/Gruppe** und **bereits dokumentierte Trainer-Insights**.
---
## 4. Admin: Massenverarbeitung und Analyse
**Vision für Plattform-/Vereins-Admins:**
| Thema | Richtungsziel |
|-------|----------------|
| **Massenverarbeitung** | Batch: z.B. Zusammenfassungen nachziehen, fehlende Skills vorschlagen, einheitlicher Stil bei importiertem Bestand — immer mit **Review-Queue**, nicht ohne menschliche Freigabe skalierungskritisch. |
| **Analyse / Qualität** | Werkzeugkasten oder Berichte: **welche Übungen** sollten überarbeitet werden? z.B. leere/kurze `summary`, fehlende `goal`/`execution`, **fehlende oder widersprüchliche Skill-Zuordnung**, Import-Herkunft ohne Plausibilität, Kombi-Slots unvollständig, sehr alte Imports. |
| **Lückenkarten** | Z.B. Abgleich gegen **Skill-Discovery**/Profil-Analysen („keine Übung deckt Fähigkeit X ab“ auf gewähltem Korpus); Verbindung zu **`skill-discovery`** entscheidend später im Detail (kein automatischer Rewrite ohne Policy). |
**Governance:** Sichtbarkeit (`official`, Verein), Rechte (**Superadmin** vs. Vereinsinhalt), Audit der KI-Anwendung bei Massenjobs.
---
## 5. Phasierung (überarbeitungsfähig)
| Phase | Inhalt |
|-------|--------|
| **P0** | KI-Service + Prompts aus DB + **Suggestion-only** UX; Kern: **Summary** + **Skills** (wie Spec-Minimum), **ein Feld / Komplettpaket mit Diff** nach UX. |
| **P1** | **Anleitung überarbeiten** + **„von Idee zur Übung“** (Zielausbau) mit Rahmenparameter-Form |
| **P2** | **Variantenvorschläge** mit strenger Validation |
| **P3** | **Planungs-/Nachbereitungskontext** |
| **P4** | **Admin** Massen-/Analyse (Queue + Reports + Governance) |
---
## 6. Offene Produkt-/Fachfragen
- Minimaler **Parameterbau** beim Zielausbau (Pflicht vs. optional).
- Umgang mit **Medien**/Inline-Verweisen beim KI-Text nichts zerstören, Platzhalter erhalten (siehe Medien-Spec §11).
- **Kombinationsübungen:** welche Teilaspekte dürfen KI anfassen?
- Limits: **Tokens**, **Rate-Limits**, Kostenüberwachung pro Verein/global.

View File

@ -57,7 +57,7 @@ Haupt-Kategorie (KARATE / ALLGEMEINE)
- Selbstverteidigung ✓ - Selbstverteidigung ✓
- Gewaltschutz ✓ - Gewaltschutz ✓
**Technische Umsetzung:** M:N Beziehungen mit `is_primary` Flag. **Technische Umsetzung:** M:N-Beziehungen mit optionalem `is_primary`-Flag bei **Fokusbereichen, Stilrichtungen, Trainingsstilen und Zielgruppen** — nicht bei `exercise_skills` (dort nur Intensität `niedrig|mittel|hoch`).
### 3. Hierarchischer Kontext (§8.1) ### 3. Hierarchischer Kontext (§8.1)
@ -407,10 +407,9 @@ skill_level_definitions (
- Reaktion (Koordination, target_level: 2, intensity: mittel) - Reaktion (Koordination, target_level: 2, intensity: mittel)
**Attribute pro Fähigkeitsbezug:** **Attribute pro Fähigkeitsbezug:**
- is_primary (Haupt- oder Nebenfähigkeit) - `intensity` — Nutzeneinschätzung: **niedrig | mittel | hoch** (Standard **mittel**)
- intensity (niedrig/mittel/hoch) - `required_level` / `target_level` — Stufen-Spanne (kanonische Slugs basis … optimierung)
- required_level (Voraussetzung, 1-5) - `is_primary` — Legacy-Feld; **nicht mehr in der UI**, beim Speichern immer false; Scoring ignoriert es
- target_level (Ziel-Level, 1-5)
**🆕 Fokusbereich-Filterung:** **🆕 Fokusbereich-Filterung:**
- Bei Übungen mit Fokusbereich "Karate" sollten primär KARATE-Fähigkeiten zugeordnet werden - Bei Übungen mit Fokusbereich "Karate" sollten primär KARATE-Fähigkeiten zugeordnet werden
@ -466,6 +465,8 @@ skill_level_definitions (
**Fachliche Grenze aktuell:** Mehrere gleichwertige „Pakete“ paralleler Alternativen sind **modellierbar** (mehrere ausgehende Kanten), aber noch **nicht** über eine dedizierte „Alternativgruppe“ in der UI trivial pflegbar; siehe `technical/TRAINING_FRAMEWORK_SPEC.md` §4. **Fachliche Grenze aktuell:** Mehrere gleichwertige „Pakete“ paralleler Alternativen sind **modellierbar** (mehrere ausgehende Kanten), aber noch **nicht** über eine dedizierte „Alternativgruppe“ in der UI trivial pflegbar; siehe `technical/TRAINING_FRAMEWORK_SPEC.md` §4.
**KI-Planung (Workbench, Stand 0.8.233):** Am Graph können Trainer neben Kanten ein **`planning_roadmap`**-Artefakt (Curriculum-Stufen) und **`planning_catalog_context`** (Primärfokus, Stilrichtung, Trainingsstil, Zielgruppe aus den Katalog-Dimensionen §1) pflegen. Die Roadmap-first-Pipeline matcht Übungen pro Stufe; Didaktik und Reihenfolge kommen aus Roadmap + QS, nicht aus Technik-Hardcoding. **Geplant (H1):** Katalog-Dimensionen zusätzlich als **Prompt-Snippets** in LLM-Aufrufen (Priorität Primärfokus → Trainingsstil → Zielgruppe → Stilrichtung) — **`docs/architecture/PLANNING_CATALOG_PROMPT_SNIPPETS.md`**. Technische Details: **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`**. Für **Trainingsplanung** (Einheit, Abschnitt, Rahmen-Slot) gelten dieselben Katalog- und Retrieval-Bausteine mit anderen Scopes — Phase G, siehe Roadmap **`PLANNING_KI_ROADMAP.md`**.
### TrainingsrahmenVorlage (Rahmenprogramm, CURR002 Stufe2 / CURR009) ### TrainingsrahmenVorlage (Rahmenprogramm, CURR002 Stufe2 / CURR009)
**Abgrenzung:** Eine **einzeilige** TrainingsplanMikrovorlage (`training_plan_template`) strukturiert **eine** Einheit; das **Rahmenprogramm** ist eine **eigene Bibliotheksentität** mit **sortierten SessionSlots**, **mindestens einem** formulierten **Entwicklungsziel** (Zielliste, **CURR011**) und einem **vollständigen Ablauf** pro Slot (**`training_unit_sections` + `training_unit_section_items`** wie bei geplanten Einheiten — **CURR010** inhaltlich, technisch seit **037** identisch zur Planungsstruktur). Der persistierte **Progressionsgraph** zwischen Übungen bleibt **optional** (**CURR013**). **Abgrenzung:** Eine **einzeilige** TrainingsplanMikrovorlage (`training_plan_template`) strukturiert **eine** Einheit; das **Rahmenprogramm** ist eine **eigene Bibliotheksentität** mit **sortierten SessionSlots**, **mindestens einem** formulierten **Entwicklungsziel** (Zielliste, **CURR011**) und einem **vollständigen Ablauf** pro Slot (**`training_unit_sections` + `training_unit_section_items`** wie bei geplanten Einheiten — **CURR010** inhaltlich, technisch seit **037** identisch zur Planungsstruktur). Der persistierte **Progressionsgraph** zwischen Übungen bleibt **optional** (**CURR013**).
@ -474,25 +475,51 @@ skill_level_definitions (
**Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem SlotBlueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/LineageKonzept: Konzeptpapier Schritt **E**). **Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem SlotBlueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/LineageKonzept: Konzeptpapier Schritt **E**).
### Parallele Trainingsstreams (Breakout, Entwurf) ### Trainingsmodul (Bibliothek)
**Abgrenzung:** Wiederverwendbare **Übungsfolge** (`training_modules` + `training_module_items`) — kein Kalendertermin, kein Rahmen-Slot. Übernahme in geplante Einheiten über Planung (`apply-training-module`).
**Governance:** wie andere Bibliotheksartefakte (`visibility`, `club_id`, `library_content_visibility_sql`).
### Gewichtetes Fähigkeiten-Profil (Planungs-Bausteine, Phase 3)
**Zweck:** Aus den verknüpften Übungen eines Planungsartefakts wird ein **Fähigkeiten-Profil** berechnet (Trainingsgewicht je Fähigkeit). Trainer vergleichen Bausteine **innerhalb desselben Typs**, um z.B. das passendste Modul für eine Ziel-Fähigkeit zu finden.
**Artefakttypen (getrennte Peer-Kontexte):**
| Typ | Vergleich |
|-----|-----------|
| `training_module` | nur sichtbare **Module** |
| `framework_program` | nur sichtbare **Rahmenprogramme** |
| `progression_graph` | nur sichtbare **Regressionspfade** |
**Metriken (Nutzer):**
- **Score / Gewicht** — absolut (Dauer × Häufigkeit × Intensität × Stufen-Spanne)
- **Prozent** — Anteil am stärksten sichtbaren Peer **desselben Typs** für diese Fähigkeit (max. 100 %)
- **★** — stärkster Peer in diesem Kontext
**UI:** Profile in Editoren; KPI-Kacheln und Filter in Listen (`/planning/framework-programs`, `/planning/training-modules`); Discovery auf der Fähigkeiten-Seite.
**Technik:** `backend/skill_scoring.py`, `routers/skill_profiles.py` — Spec **`technical/SKILL_SCORING_SPEC.md`**.
### Parallele Trainingsstreams (Breakout)
**Fachlich:** Eine Kalender**Einheit** kann aus **Phasen** bestehen — z.B. gemeinsamer Block, dann **beliebig viele parallele** „Teilstrecken“ (**Streams**) mit je eigenem Miniplan (Abschnitte/Übungen), erneut gemeinsamer Block. Das ist **nicht** dasselbe wie ein **RahmenprogrammSlot** (SerienSession über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit. **Fachlich:** Eine Kalender**Einheit** kann aus **Phasen** bestehen — z.B. gemeinsamer Block, dann **beliebig viele parallele** „Teilstrecken“ (**Streams**) mit je eigenem Miniplan (Abschnitte/Übungen), erneut gemeinsamer Block. Das ist **nicht** dasselbe wie ein **RahmenprogrammSlot** (SerienSession über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit.
**Sonderfall Stationen:** Rotation kann **innerhalb** einer StreamPlanung über **Kombinationsübungen** (Methodenprofil/Archetyp) abgebildet werden; hallenweit **synchron** getaktete Rotation ist eine **erweiterte** Ausbaustufe (siehe Fachkonzept). **Sonderfall Stationen:** Rotation kann **innerhalb** einer StreamPlanung über **Kombinationsübungen** (Methodenprofil/Archetyp) abgebildet werden; hallenweit **synchron** getaktete Rotation ist eine **erweiterte** Ausbaustufe (siehe Fachkonzept).
**Umsetzung (2026-05, Migration 063, App 0.8.137 ff.):** Tabellen **`training_unit_phases`** und **`training_unit_parallel_streams`**; **`training_unit_sections`** mit **`phase_id`** und **`parallel_stream_id`** (exakt eine Zuordnung pro Sektion). **`GET /api/training-units/:id`** liefert **`phases`** (verschachtelt) und flache **`sections`**. **Coaching** und **Durchführung** nutzen dieselbe Phasenlogik im Frontend (`trainingPlanUtils.js`).
**Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`. **Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
**Schema-Hinweis (2026-05):** Tabelle `training_unit_sections` hat **`UNIQUE (training_unit_id, order_index)`** (Migration 031). Damit sind **zwei gleichzeitige „Spuren“ mit jeweils eigener Sektion auf derselben `order_index`** nicht abbildbar — Voraussetzung für Parallele Streams ist eine **geplante Migrations-/Constraint-Anpassung** (partielle Uniques pro Phase/Stream); siehe Arbeitsdokument `.claude/docs/working/PARALLEL_TRAINING_STREAMS_ANALYSIS_AND_IMPLEMENTATION_PLAN.md`. **Keine invasive Migration ohne explizite Freigabe.**
---
## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07) ## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07)
- **`media_assets`:** Zentrale Datei-/Asset-Zeile (technisch u.a. SHADedupe, Sichtbarkeit, `club_id`, Lifecycle, Copyright, Speicherreferenz unter `library/…`). Siehe **`DATABASE_SCHEMA.md`**, **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. - **`media_assets`:** Zentrale Datei-/Asset-Zeile (technisch u.a. SHADedupe, Sichtbarkeit, `club_id`, Lifecycle, Copyright, Speicherreferenz unter `library/…`). Siehe **`DATABASE_SCHEMA.md`**, **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**.
- **`exercise_media`:** Verknüpfung **Übung ↔ Asset** (`media_asset_id`) oder **Embed** ohne Asset; Felder wie `context` (`ablauf` \| `detail` \| `trainer_hint`), Sortierung, Primär-Medium. - **`exercise_media`:** Verknüpfung **Übung ↔ Asset** (`media_asset_id`) oder **Embed** ohne Asset; Felder wie `context` (`ablauf` \| `detail` \| `trainer_hint`), Sortierung, Primär-Medium.
- **`platform_media_storage`:** Konfiguration effektiver Medienwurzel (Superadmin, relativ zu `MEDIA_ROOT`). - **`platform_media_storage`:** Konfiguration effektiver Medienwurzel (Superadmin, relativ zu `MEDIA_ROOT`).
- **Produkt:** Medienbibliothek **`/media`**; in der Übungsbearbeitung Upload, Entfernen der Verknüpfung, **Aus Archiv verknüpfen**; Governance **`official`** und Copyright-Regeln wie in der Norm beschrieben. - **Produkt:** Medienbibliothek **`/media`**; in der Übungsbearbeitung Upload, Entfernen der Verknüpfung, **Aus Archiv verknüpfen**; Governance **`official`** und Copyright-Regeln wie in der Norm beschrieben.
- **Geplant:** **Inline-Verweise** in Fließtextfeldern auf dieselbe Verknüpfung (`exercise_media.id`) — **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**, **`docs/HANDOVER.md`** §5. - **Inline-Verweise** in Fließtextfeldern: **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**, **`docs/HANDOVER.md`** §5.
--- ---
@ -650,12 +677,13 @@ skill_level_definitions (
- [ ] Level-Definitionen aus Fähigkeitsmatrix extrahieren (optional) - [ ] Level-Definitionen aus Fähigkeitsmatrix extrahieren (optional)
- [ ] Skills-Beschreibungen aus Wiki importieren (Migration 024) - [ ] Skills-Beschreibungen aus Wiki importieren (Migration 024)
- [ ] Admin-UI für Fähigkeiten-Kategorien (CRUD) - [ ] Admin-UI für Fähigkeiten-Kategorien (CRUD)
- [ ] Skill-Filter in Übungssuche integrieren - [x] Skill-Filter in Übungssuche (SkillTreeMultiSelect + Stufen)
- [x] Gewichtetes Fähigkeiten-Profil für Planungs-Bausteine (Module, Rahmen, Pfade) — siehe `technical/SKILL_SCORING_SPEC.md`
- [ ] Reifegradmodelle definieren (Kombination Fokusbereich + Stil + Zielgruppe) - [ ] Reifegradmodelle definieren (Kombination Fokusbereich + Stil + Zielgruppe)
- [ ] KI-Unterstützung für Trainingsplanung (basierend auf Fähigkeiten-Level) - [ ] KI-Unterstützung für Trainingsplanung (basierend auf Fähigkeiten-Level)
--- ---
**Letzte Aktualisierung:** 2026-04-27 **Letzte Aktualisierung:** 2026-05-20
**Verantwortlich:** Claude Code **Verantwortlich:** Claude Code
**Review:** Pending **Review:** Pending

View File

@ -1,6 +1,6 @@
# Parallele Trainingsstreams (Breakout) — Fachkonzept # Parallele Trainingsstreams (Breakout) — Fachkonzept
**Status:** Entwurf zur Abstimmung · **Stand:** 2026-05-14 **Status:** MVP-Umsetzung **teilweise** (Code) · **Stand:** 2026-05-14
**Ziel:** Planung und Durchführung von Training mit **phasenweise gemeinsamem** Ablauf und **beliebig vielen parallelen Teilstrecken** (Breakout-Sessions), inkl. Sonderfall **rotierende Stationen**. **Ziel:** Planung und Durchführung von Training mit **phasenweise gemeinsamem** Ablauf und **beliebig vielen parallelen Teilstrecken** (Breakout-Sessions), inkl. Sonderfall **rotierende Stationen**.
**Technische Ausarbeitung:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md` **Technische Ausarbeitung:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`
@ -95,7 +95,14 @@ Alle Streams (oder alle Kinder insgesamt) **wechseln gleichzeitig** zur nächste
--- ---
## 8. Verwandte Dokumente ## 9. Umsetzungsstand (kurz, 2026-05-14)
- **Erreicht:** Datenmodell Phasen/Streams (**063**), API **GET/PUT** mit **`phases`**, Planungs-Breakout-UI, Durchführung und Coach nutzen dieselbe Phasen-/Stream-Logik im Frontend (`trainingPlanUtils.js`). **Synchronisationspunkt** fachlich umgesetzt: vor nächster Ganzgruppenphase oder nächstem Split erscheint im Coach die **Rejoin-Karte** (mehrere Streams), sofern nicht am absoluten Planende.
- **Noch offen:** vollständige **Persistenz-Konsistenz** bei nachträglich geänderten Sektionen, **Vorlagen** mit Phasen, **Trainer pro Stream** in der UI, ggf. **Stream-Tabs** in der Durchführungsansicht wie in §5.2 skizziert — siehe **`docs/HANDOVER.md`** (Arbeitspaket-Tabelle).
---
## 10. Verwandte Dokumente
| Dokument | Bezug | | Dokument | Bezug |
|----------|--------| |----------|--------|
@ -103,4 +110,5 @@ Alle Streams (oder alle Kinder insgesamt) **wechseln gleichzeitig** zur nächste
| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombinationsübungen, Archetypen, Stationslogik **im Item** | | `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombinationsübungen, Archetypen, Stationslogik **im Item** |
| `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` | Fachliche Tiefe Kombi | | `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` | Fachliche Tiefe Kombi |
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick | | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick |
| `docs/HANDOVER.md` | Ist-Stand Coach, offene Breakout-Punkte |
| `technical/DATABASE_SCHEMA.md` | Aktueller Stand Tabellen | | `technical/DATABASE_SCHEMA.md` | Aktueller Stand Tabellen |

View File

@ -12,6 +12,8 @@ Ausführliche fachliche Inhalte:
| [**Trainingsmodule & Kombinationsübungen (Fachspez V3)**](./Shinkan%20Trainingsmodule%20Kombinationsuebungen%20Spezifikation%20V2.md) | Produktlogik Module/Kombinationen, **Methoden-Archetypen**, **Coaching-Stufen (§10.4)**, kanonische Archetyp-IDs **§10.2.1**, **Anhang A** Implementierungsabgleich | | [**Trainingsmodule & Kombinationsübungen (Fachspez V3)**](./Shinkan%20Trainingsmodule%20Kombinationsuebungen%20Spezifikation%20V2.md) | Produktlogik Module/Kombinationen, **Methoden-Archetypen**, **Coaching-Stufen (§10.4)**, kanonische Archetyp-IDs **§10.2.1**, **Anhang A** Implementierungsabgleich |
| [**Umsetzungsplan Trainingsmodule & Kombination**](../working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md) | Phase 15, Coaching-Pakete 4a4d, Verweis auf Code-Stand | | [**Umsetzungsplan Trainingsmodule & Kombination**](../working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md) | Phase 15, Coaching-Pakete 4a4d, Verweis auf Code-Stand |
| [**Technischer Entwurf Module/Kombination**](../technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md) | API/Daten-Ideen; aktueller Coach-/Archetyp-Abgleich im Kopfabschnitt | | [**Technischer Entwurf Module/Kombination**](../technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md) | API/Daten-Ideen; aktueller Coach-/Archetyp-Abgleich im Kopfabschnitt |
| [**KI-Unterstützung Übungen (Vision)**](./AI_EXERCISE_ASSISTANT_VISION.md) | Zielbild Zielausbau, Vorschlags-UX (teilweise/komplett), Skills/Varianten, später Planungskontext, Admin-Masse/Qualität |
| [**KI Übungen Umsetzungsplan**](../working/AI_EXERCISE_IMPLEMENTATION_PLAN.md) | Stufen S0S6, Driftschutz-Regeln, Checkliste gegen Specs |
**Lieferstand & Umsetzung (Stand Code):** [`../PROJECT_STATUS.md`](../PROJECT_STATUS.md), [`../library/FEATURES_DELIVERED_2026-Q2.md`](../library/FEATURES_DELIVERED_2026-Q2.md) (Abschnitt 12), Repo-Root **`docs/HANDOVER.md`**, **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**. **Lieferstand & Umsetzung (Stand Code):** [`../PROJECT_STATUS.md`](../PROJECT_STATUS.md), [`../library/FEATURES_DELIVERED_2026-Q2.md`](../library/FEATURES_DELIVERED_2026-Q2.md) (Abschnitt 12), Repo-Root **`docs/HANDOVER.md`**, **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**.

View File

@ -1,6 +1,6 @@
# Gelieferte Features & technische Basis (Q2 2026) # Gelieferte Features & technische Basis (Q2 2026)
**Stand:** 2026-05-12 **Stand:** 2026-05-20
**Referenz:** `backend/version.py` — aktuelle **APP_VERSION** / **DB_SCHEMA_VERSION** (Stand Code u. a. **0.8.96**) **Referenz:** `backend/version.py` — aktuelle **APP_VERSION** / **DB_SCHEMA_VERSION** (Stand Code u. a. **0.8.96**)
Dieses Dokument bündelt die in der Entwicklungsphase erreichten **lieferbaren** Funktionen und die zugehörigen **technischen Artefakte**. TrainingsrahmenBibliothek + SlotBlueprint: **`technical/TRAINING_FRAMEWORK_SPEC.md`** §2. **Progressionsgraph zwischen Übungen** (Zwischenstand, Grenzen): **§§34**. **Medien-Archiv & Bibliothek:** Abschnitt **12** unten + **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Detail-Spezifikationen bleiben in den verlinkten Pfaden unter `.claude/docs/technical/` und `.claude/docs/functional/`. Dieses Dokument bündelt die in der Entwicklungsphase erreichten **lieferbaren** Funktionen und die zugehörigen **technischen Artefakte**. TrainingsrahmenBibliothek + SlotBlueprint: **`technical/TRAINING_FRAMEWORK_SPEC.md`** §2. **Progressionsgraph zwischen Übungen** (Zwischenstand, Grenzen): **§§34**. **Medien-Archiv & Bibliothek:** Abschnitt **12** unten + **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Detail-Spezifikationen bleiben in den verlinkten Pfaden unter `.claude/docs/technical/` und `.claude/docs/functional/`.
@ -68,7 +68,7 @@ Logik: `_upload_limit_bytes(session)` vor `read()`-Prüfung.
## 5. Frontend Übungsliste (`ExercisesListPage.jsx`) ## 5. Frontend Übungsliste (`ExercisesListPage.jsx`)
- Tabs **Liste** · **Progressionsgraphen** (`ExerciseProgressionGraphPanel`): Graphen anlegen/bearbeiten, Kanten inkl. Sequenz-Bulk und Tabellenansicht. - Tabs **Liste** · **Progressionsgraphen** (`ExerciseProgressionGraphPanel`): Graphen anlegen/bearbeiten, Kanten inkl. Sequenz-Bulk und Tabellenansicht.
- **Filter-Modal** (Fokus, Stilrichtung, Trainingsstil, Zielgruppe, Fähigkeit + Stufen von/bis, Sichtbarkeit, Status). - **Filter-Modal** (Fokus, Stilrichtung, Trainingsstil, Zielgruppe, Fähigkeit + Stufen von/bis, **Freigabelevel**, Status).
- **Filter-Chips** unter der Suchleiste; Klick entfernt einen Filter; Badge am Filter-Button = Anzahl Chips. - **Filter-Chips** unter der Suchleiste; Klick entfernt einen Filter; Badge am Filter-Button = Anzahl Chips.
- **Kein Vollbild-Spinner** bei jeder Suche: nur noch **`listFetching`** — Suchfelder bleiben im DOM (**Fokus/Cursor** bleiben erhalten); Liste zeigt optional „Aktualisiere Treffer…“. - **Kein Vollbild-Spinner** bei jeder Suche: nur noch **`listFetching`** — Suchfelder bleiben im DOM (**Fokus/Cursor** bleiben erhalten); Liste zeigt optional „Aktualisiere Treffer…“.
- **`<datalist>`** mit Titeln der aktuellen Treffer; **`autoComplete="on"`** für Browser-Vorschläge. - **`<datalist>`** mit Titeln der aktuellen Treffer; **`autoComplete="on"`** für Browser-Vorschläge.
@ -76,14 +76,47 @@ Logik: `_upload_limit_bytes(session)` vor `read()`-Prüfung.
--- ---
## 6. Frontend Übung bearbeiten (`ExerciseFormPage.jsx`) ## 6. Frontend Übung bearbeiten (`ExerciseFormPageRoot.jsx`)
**Routing:** `/exercises/new`, `/exercises/:id/edit` — keine separaten Varianten-Routen.
### 6.1 Tab-Navigation (Registerkarten)
Horizontale **`PageSectionNav`** über **`ExerciseFormTabBar`** / **`ExerciseFormPanel`** (`ExerciseFormLayout.jsx`); farbige linke Panel-Ränder (CSS `.exercise-form-edit`, `.exercise-form-panel--*`).
| Tab | Inhalt |
|-----|--------|
| **Stammdaten** | Titel, Kurztext, Dauer/Gruppe, Equipment, **Freigabelevel** (`visibility`), Status, Verein |
| **Anleitung** | Ziel, Durchführung, Vorbereitung, Trainerhinweise (Rich-Text inkl. Inline-Medien) |
| **Einordnung** | Fokusbereiche, Stilrichtungen, Trainingsstile, Zielgruppen, Altersgruppen, **Fähigkeiten** (kompakte Chip-Editoren) |
| **Kombination** | nur bei `exercise_kind=combination`: Slots, Archetyp, `method_profile` |
| **Varianten** | nur nach erstem Speichern; **nicht** bei Kombinationsübungen |
| **Medien & Mehr** | Medien, Progressionsgraph, KI-Hilfen, Löschen — nach erstem Speichern |
Neue Übungen: Tabs **Varianten** und **Medien & Mehr** deaktiviert bis zur ersten Speicherung.
### 6.2 Freigabelevel (UI-Begriff)
Feld **`exercises.visibility`** heißt in der UI durchgängig **Freigabelevel** (`frontend/src/constants/exerciseGovernanceLabels.js`) — Liste, Filter, Bulk, Picker, Formular. API/DB-Feldname **`visibility`** unverändert.
### 6.3 Fähigkeiten am Übungsobjekt
- Intensität je Fähigkeit: **`niedrig` \| `mittel` \| `hoch`**, Standard **`mittel`** (`exerciseSkillIntensity.js`).
- Kein „Primär“-Schalter mehr in der UI; **`is_primary`** bei `exercise_skills` ist Legacy — Backend speichert immer **`false`**, Scoring ignoriert das Feld.
- Kompakte **Chip-Editoren** für Katalog-Zuordnungen und Fähigkeiten (`ExerciseCatalogAssocEditor`, `ExerciseSkillsEditor`).
### 6.4 Varianten-Editor
- Tab **Varianten**: **eine Variante zur Zeit** (Dropdown oder „Erste Variante anlegen“); Felder über **`ExerciseVariantFields`**; Reihenfolge Nach oben/unten; Löschen pro Variante.
- **Speichern über Aktionsleiste:** `performSaveAttempt` ruft zuerst **`persistPendingVariantChanges()`** auf (geänderte Varianten per PUT, danach optional Entwurf **`createVariantFromDraft()`**).
- Button **„Variante anlegen“** (`type="button"`, kein verschachteltes `<form>`): legt Entwurf sofort per API an; alternativ mitgesichert über **Speichern** in der Aktionsleiste.
- Snapshot **`variantsSavedSnapshotRef`** für Dirty-Erkennung; Hinweis im Panel: Änderungen werden mit Speichern in der Aktionsleiste mitgesichert.
### 6.5 Medien & Progressionsgraph
- **Varianten-Editor**: eingeklappter Bereich (`<details>`), **eine Variante zur Zeit** über Dropdown oder „Neue Variante“; Felder über **`ExerciseVariantFields`**; Reihenfolge Nach oben/unten; Speichern/Löschen pro Variante.
- **Medien:** Upload/Embed, **Archiv verknüpfen** (`from-asset`), Medienliste mit Vorschau, Reaktivierung bei Archiv-Konflikt — Details **§12**. - **Medien:** Upload/Embed, **Archiv verknüpfen** (`from-asset`), Medienliste mit Vorschau, Reaktivierung bei Archiv-Konflikt — Details **§12**.
- Block **Progressionsgraph** (Edit): Kanten mit Bezug zur aktuellen Übung. - Block **Progressionsgraph** (Edit): Kanten mit Bezug zur aktuellen Übung.
Hinweis: Es gibt **keine** separaten Routen `/exercises/:id/variants/...` — Bearbeitung erfolgt unter **`/exercises/:id/edit`** (Routing-Doku ggf. anpassen).
--- ---
## 7. Frontend Übung Detail (`ExerciseDetailPage.jsx`) ## 7. Frontend Übung Detail (`ExerciseDetailPage.jsx`)
@ -123,7 +156,16 @@ Hinweis: Es gibt **keine** separaten Routen `/exercises/:id/variants/...` — Be
--- ---
## 12. Medien-Archiv & Medienbibliothek (Migration **045** ff., App ca. **0.8.410.8.64**) ## 12. Trainingsplan: Phasen & parallele Streams (DB **063**, App **0.8.1370.8.140**)
- **063:** `training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` / `parallel_stream_id`; Default-Ganzgruppenphase für Bestand.
- **API:** `GET /api/training-units/:id` mit **`phases`** + **`sections`**; `PUT`/`POST` mit **`phases`** für Breakout-Einheiten (**0.8.138**); Rahmen-Slot-Materialisierung kopiert Phasen (**0.8.138**).
- **Frontend:** Planung Breakout-Panel (**0.8.1390.8.140**); **`trainingPlanUtils.js`** — `sectionsWithPlanLocForDisplay`, `flattenPlanTimeline`, `buildCoachSavePlanPayload`, Split-Rejoin (`coachShouldPromptSplitRejoinTransition`); **`TrainingCoachPage`**, **`TrainingUnitRunPage`**.
- **Doku:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`, `docs/HANDOVER.md` §3, Arbeitspaket „offen“.
---
## 13. Medien-Archiv & Medienbibliothek (Migration **045** ff., App ca. **0.8.410.8.64**)
Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick geliefert: Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick geliefert:
@ -150,7 +192,7 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
--- ---
## 13. Nächste sinnvolle Schritte (nicht Lieferstand) ## 14. Nächste sinnvolle Schritte (nicht Lieferstand)
- Trainingsplanung: KalenderUIAnbindung **„aus Rahmen übernehmen“**; Visibility/Policies für geteilte Rahmen (**CURR004** später). - Trainingsplanung: KalenderUIAnbindung **„aus Rahmen übernehmen“**; Visibility/Policies für geteilte Rahmen (**CURR004** später).
- Progressions-Serien als **Blöcke** (angekündigt; Voraussetzung: `prerequisite_variant_id` / `progression_level` vorhanden). - Progressions-Serien als **Blöcke** (angekündigt; Voraussetzung: `prerequisite_variant_id` / `progression_level` vorhanden).
@ -160,15 +202,55 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
--- ---
## 14. Verweise ## 15. Gewichtetes Fähigkeiten-Scoring (Phase 3, Stand 2026-05-20)
Norm: **`technical/SKILL_SCORING_SPEC.md`**.
### 15.1 Backend
- **`skill_scoring.py`:** Gewichtung (Dauer × Vorkommen × Intensität × Stufen); `compute_planning_corpus_by_type()` mit getrennten Corpora; `universal_percent` capped auf 100 %
- **`routers/skill_profiles.py`:** Profile-GET pro Artefakt; `POST /api/skill-profiles/batch-summaries`; `GET /api/skill-discovery/suggestions`
- Sichtbarkeit: **`library_content_visibility_sql`** (Planungs-Bibliothek, nicht „nur Verein club“)
### 15.2 Frontend
- **Listen:** Rahmenprogramme + Trainingsmodule — Filter-Modal (wie Übungen), Chips, `SkillTreeMultiSelect` (Portal-Dropdown)
- **KPI:** `SkillProfileCompact` — Top je Unterkategorie, Score + Peer-%
- **Editoren + Modal:** `SkillProfilePanel`, `SkillProfileFullModal`
- **Discovery:** `SkillDiscoveryPanel` auf Fähigkeiten-Seite
### 15.3 Offen
- Corpus-Caching; pytest für Typ-Trennung; Filter-Persistenz; Skill-Filter Import-Dialog „Rahmen übernehmen“
---
## 16. Übungen Governance & Berechtigungen (Ist, Stand 2026-05-20)
**Owner:** `exercises.created_by` (Ersteller). **Varianten** haben kein eigenes `created_by` — Rechte leiten sich von der Eltern-Übung ab.
| Aktion | `private` | `club` | `official` |
|--------|-----------|--------|------------|
| **Lesen** | Ersteller; Plattform-Admin | Aktive Vereinsmitglieder des Objekt-`club_id`; Plattform-Admin ohne Mitgliedschaft (Audit) | Plattform-weit |
| **Bearbeiten** (Übung inkl. Varianten/Medien) | Ersteller; Plattform-Admin | Ersteller; Plattform-Admin; **`can_plan_in_club`** im Objekt-Verein (`trainer`, `content_editor`, `division_lead`, `club_admin`) | Plattform-Admin |
| **Löschen** | Ersteller; Vereins-Admin gemeinsamer Vereine mit Ersteller | Nur **`club_admin`** im Objekt-Verein | Nur Plattform-Admin |
**Code:** `backend/club_tenancy.py` (`exercise_visible_to_profile`, `can_plan_in_club`), `backend/routers/exercises.py` (`_assert_can_edit_exercise`, `_assert_can_delete_exercise`).
---
## 17. Verweise
| Thema | Dokument | | Thema | Dokument |
|--------|----------| |--------|----------|
| Rahmenprogramm / Progressionsgraph | `technical/TRAINING_FRAMEWORK_SPEC.md` | | Rahmenprogramm / Progressionsgraph | `technical/TRAINING_FRAMEWORK_SPEC.md` |
| Fähigkeiten-Scoring Planung | `technical/SKILL_SCORING_SPEC.md` |
| API Übungen | `technical/EXERCISES_API_SPEC.md` | | API Übungen | `technical/EXERCISES_API_SPEC.md` |
| Domänenmodell | `functional/DOMAIN_MODEL.md` | | Domänenmodell | `functional/DOMAIN_MODEL.md` |
| Datenbank Überblick | `technical/DATABASE_SCHEMA.md` | | Datenbank Überblick | `technical/DATABASE_SCHEMA.md` |
| Medien Upload (Limits, MIME) | `technical/MEDIA_UPLOAD_SPEC.md` | | Medien Upload (Limits, MIME) | `technical/MEDIA_UPLOAD_SPEC.md` |
| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | | Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
| Parallele Phasen/Streams | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` |
| Coaching/Breakout-Handover | `docs/HANDOVER.md` |
| Fachlicher Nutzerüberblick | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` (Repo-Root) | | Fachlicher Nutzerüberblick | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` (Repo-Root) |
| Projektstatus-Kachel | `../PROJECT_STATUS.md` | | Projektstatus-Kachel | `../PROJECT_STATUS.md` |

View File

@ -79,16 +79,18 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`).
### Stufe E Capabilities dokumentieren (ohne UI für Custom Roles) ### Stufe E Capabilities dokumentieren (ohne UI für Custom Roles)
- Markdown-Tabelle **Capability-Fingerprint**: Kennungen wie `content.share_club`, `planning.edit_unit`, `org.manage_members`, … mit Zuordnung zu den **heutigen** festen Vereinsrollen. - **Verbindliche Spez v1:** `CAPABILITY_CATALOG.v1.md` — Capability-IDs, Account-Lifecycle, Rollen-Matrix, Endpoint-Mapping.
- Markdown-Tabelle **Capability-Fingerprint**: Kennungen wie `exercises.ai.suggest`, `org.members.manage`, … mit Zuordnung zu den **heutigen** festen Vereinsrollen (siehe Katalog §56).
- Ziel: später `club_custom_roles` nur noch andere Kombination derselben Kennungen keine zweite Philosophie. - Ziel: später `club_custom_roles` nur noch andere Kombination derselben Kennungen keine zweite Philosophie.
### Stufe F Community (eigenes Epic) ### Stufe F Community (eigenes Epic)
- Konzept: Freigabe **additiv** (Flag oder Enum), Moderation, Sichtbarkeit „öffentlich außerhalb meines Vereins“ ohne bestehende `club`-Isolation zu brechen. - Konzept: Freigabe **additiv** (Flag oder Enum), Moderation, Sichtbarkeit „öffentlich außerhalb meines Vereins“ ohne bestehende `club`-Isolation zu brechen.
### Zurückgestellt Vereinsabo / Limits ### Zurückgestellt Vereinsabo / Limits (Konzept liegt vor)
- Wiederöffnen wenn ACCESS_LAYER Stufe C/D stabil; dann Enforcement vor ausgewählten Writes an einen Billing-Stripe binden. - **Spez v1:** `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` — Feature-Registry (Mitai-v9c-Pattern), `club_plans`/`club_subscriptions`, Kontingente an `club_id`.
- Implementierung/Billing (Stripe) weiter zurückgestellt; Schema- und Enforcement-Hooks gemäß 4-Phasen-Rollout (Mitai-Vorbild) vorbereiten, sobald Stufe C/D stabil.
--- ---
@ -117,10 +119,28 @@ Ausgangslage im Code: `private` \| `club` \| `official` (siehe `club_tenancy`).
## 7. Referenzen ## 7. Referenzen
- **`CAPABILITY_CATALOG.v1.md`** Rollen, Capabilities, CRUD-Mapping, `GET /api/me/entitlements`.
- **`CLUB_MEMBERSHIP_AND_FEATURES.v1.md`** Vereinsabo, Feature-Limits, Mitai-Mapping, Ziel-Schema.
- `.claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md` übergeordnetes Zielbild & Begriffe. - `.claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md` übergeordnetes Zielbild & Begriffe.
- `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` verbindliche Domänenregeln für **Medien-Assets** (gleiche Sichtbarkeit wie Übungen, Promotion-Kopplung, Copyright, Papierkorb/Lebenszyklus, externer Speicher). Bei Widerspruch zur Sichtbarkeits-Tabelle in §3 dieses Dokuments: §3 für Enums/`library_content_*`-Semantik, Medien-Spez für Asset-spezifische Zusatzregeln. - `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` verbindliche Domänenregeln für **Medien-Assets** (gleiche Sichtbarkeit wie Übungen, Promotion-Kopplung, Copyright, Papierkorb/Lebenszyklus, externer Speicher). Bei Widerspruch zur Sichtbarkeits-Tabelle in §3 dieses Dokuments: §3 für Enums/`library_content_*`-Semantik, Medien-Spez für Asset-spezifische Zusatzregeln.
- `backend/club_tenancy.py` bestehende Bausteine (`assert_club_member`, `exercise_visible_to_profile`, …); Ziel ist Deren schrittweise Zusammenführung unter die neue Zugriffsschicht ohne Big-Bang. - `backend/club_tenancy.py` bestehende Bausteine (`assert_club_member`, `exercise_visible_to_profile`, `can_plan_in_club`, …); Ziel ist Deren schrittweise Zusammenführung unter die neue Zugriffsschicht ohne Big-Bang.
--- ---
**Letzte Aktualisierung:** 2026-05-07 ## 8. Anhang Übungen (Ist-Implementierung, Referenz)
**Stand:** 2026-05-20 · **Detail:** `EXERCISES_API_SPEC.md` Permissions, `FEATURES_DELIVERED_2026-Q2.md` §16
| Feld / Konzept | Semantik |
|----------------|----------|
| `created_by` | Owner der Übung; Varianten erben Rechte |
| `visibility` | UI: **Freigabelevel**`private` \| `club` \| `official` |
| Lesen | `exercise_visible_to_profile``official` global; `private` Ersteller + Plattform-Admin; `club` aktive Mitglieder (+ Plattform-Admin Audit) |
| Bearbeiten | Ersteller; Plattform-Admin; bei `club` zusätzlich `can_plan_in_club` (Trainer, Content-Editor, Spartenleitung, Vereins-Admin) |
| Löschen | `official` → Plattform-Admin; `club``club_admin` im Objekt-Verein; `private` → Ersteller oder Vereins-Admin mit gemeinsamem Verein |
**Hinweis:** Dieser Anhang dokumentiert den **produktiven Code-Pfad** in `exercises.py`; die Roadmap in §4 bleibt für die langfristige Vereinheitlichung aller Bibliotheksartefakte maßgeblich.
---
**Letzte Aktualisierung:** 2026-05-20

View File

@ -1,11 +1,20 @@
# KI-Prompt-System Universelle Admin-Konfiguration # KI-Prompt-System Universelle Admin-Konfiguration
**Version:** 1.0 **Version:** 1.1
**Datum:** 2026-04-24 **Datum:** 2026-05-30
**Status:** DRAFT **Status:** Kern umgesetzt (`ai_prompts`, `prompt_resolver`, Superadmin-HTTP-API); Kaskaden geplant (Abschnitt 8)
**Zielbild (Roadmap):** `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` — Kontext-Arten, Composition, Planung/Rahmen, Phasenplan.
**Ist-Stand API (Superadmin):**
- `GET /api/admin/ai-prompts`, `GET /api/admin/ai-prompts/{id}`, `PUT …`, `POST …/preview`, `POST …/reset-template`, `GET /api/admin/ai-prompts/catalog/placeholders`
- Spalte **`openrouter_model`** (Migration **070**): Optional pro Prompt-Zeile; OpenRouter **`model`**-Parameter; **`NULL`/leer ⇒ `OPENROUTER_MODEL`** aus der Umgebung.
**Autor:** Claude Code **Autor:** Claude Code
**Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System **Vorbild:** Mitai Jinkendo Issue #53 + `backend/routers/prompts.py` + Placeholder-System
**Verwandt (Skill-Katalog in Übungs-KI):** `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md` — Tabelle **`ai_skill_retrieval_profiles`** (`config`-JSON ergänzt inhaltliche Prompt-/Katalog-Steuerung neben Platzhaltern).
--- ---
## 1. Konzept ## 1. Konzept
@ -28,6 +37,7 @@ steuerbar. Kein KI-Aufruf ist fest im Code verdrahtet.
|-------------|-----------| |-------------|-----------|
| `exercise_summary` | Generiert `exercises.summary` aus goal + execution | | `exercise_summary` | Generiert `exercises.summary` aus goal + execution |
| `exercise_skill_suggestions` | Empfiehlt Skills + Stufen für eine Übung | | `exercise_skill_suggestions` | Empfiehlt Skills + Stufen für eine Übung |
| `exercise_instruction_rewrite` | Überarbeitet Anleitung: goal, execution, preparation, trainer_notes (JSON, prägnantes HTML) |
| `exercise_category_suggestions` | Empfiehlt Fokusbereich, Stil, Zielgruppe | | `exercise_category_suggestions` | Empfiehlt Fokusbereich, Stil, Zielgruppe |
| `model_skill_level_description` | Generiert Stufen-Beschreibung in der Fähigkeitsmatrix | | `model_skill_level_description` | Generiert Stufen-Beschreibung in der Fähigkeitsmatrix |
| `training_plan_notes` | Erzeugt Trainer-Notizen für Trainingseinheiten | | `training_plan_notes` | Erzeugt Trainer-Notizen für Trainingseinheiten |
@ -174,10 +184,9 @@ Wähle maximal 5 passende Fähigkeiten. Für jede gib an:
- required_level: Voraussetzung (einsteiger|grundlagen|aufbau|fortgeschritten|experte) - required_level: Voraussetzung (einsteiger|grundlagen|aufbau|fortgeschritten|experte)
- target_level: Ziel nach regelmäßigem Training (gleiche Werte) - target_level: Ziel nach regelmäßigem Training (gleiche Werte)
- intensity: Trainingsintensität (niedrig|mittel|hoch) - intensity: Trainingsintensität (niedrig|mittel|hoch)
- is_primary: true wenn Hauptfähigkeit
Antworte NUR als JSON-Array: Antworte NUR als JSON-Array:
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}] [{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch"}]
Wenn keine Fähigkeit passt, antworte mit [].$$, Wenn keine Fähigkeit passt, antworte mit [].$$,
'exercise', 'json', true, NULL, 2), 'exercise', 'json', true, NULL, 2),
@ -597,6 +606,19 @@ AI_PROMPT_SYSTEM_SPEC: ai_service.run_ai_prompt("exercise_summary", ...)
--- ---
**Version:** 1.0 ## 8. Prompt-Kaskaden (geplant — nicht implementiert)
**Datum:** 2026-04-24
**Status:** DRAFT **Ziel:** Vorlagen, die andere Prompts einbinden oder in feste Stufen (System → Fach → Ausgabeformat) zerlegt werden — ohne die DB-Templates mit duplizierten Fliesstexten zu zersplittern.
**Konzeptskizze:**
- Optional neues Feld `base_slug` oder eigene Tabelle `ai_prompt_composition` (Reihenfolge, Rolle: `system|user|prepend`).
- Platzhaltersyntax z. B. `{{include_prompt:slug}}` mit **maximaler Verschachtelungstiefe** und Zykluserkennung.
- Auflösungsreihenfolge: (1) eingebundene Slugs expandieren, (2) Kontext-Variablen wie heute ersetzen.
Bis zur Umsetzung bleiben zusammengesetzte Anweisungen im **einen** Template pro Slug (wie `exercise_skill_suggestions` mit `{{skills_catalog}}`).
---
**Version:** 1.1
**Datum:** 2026-05-30
**Status:** Teile umgesetzt (DB 067/069, Resolver, Superadmin-API + UI); Kaskaden offen

View File

@ -0,0 +1,166 @@
# KI-Prompt-System — Zielarchitektur (Shinkan Jinkendo)
**Version:** 1.0
**Datum:** 2026-05-30
**Status:** VERBINDLICHE ZIELRICHTUNG (Roadmap — nicht alles bereits umgesetzt)
**Ergänzt:** `AI_PROMPT_SYSTEM_SPEC.md` (aktueller Ist-Stand APIs/DB/UI), Mitai-Anleihen aus gleichnamigen Konzepten (Admin-Prompts, Platzhalter)
---
## 1. Zweck
Dieses Dokument beschreibt das **Zielbild**, damit spätere Arbeiten (**Trainingsplanung**, **mehrstufige Rahmenprogramme**, **Phasen/Streams**, weitere KI-Artefakte) **nicht** zu wiederholten Refaktoren von Übungs-KI oder OpenRouter-Anbindung zwingen.
**Leitkriterien:** wenige stabile Schnittflächen, Kontext pro Domäne, komponierbare Prompts, gültige Ausgaben, Betrieb ohne Code-Deploy für kleine Tweaks.
---
## 2. Leitprinzipien
### 2.1 Eine stabile Ausführungsschicht
Alle produktiven KI-Aufrufe sollten mittelfristig über eine **einheitliche Fassade** laufen:
- **Eingabe:** `slug` (+ optional Kontext-Arten-Enum), **serialisierter Domän-Kontext** (Pydantic pro Kind), Konfiguration (Modell, Temperatur, … aus Env/DB).
- **Ausgabe:** Text oder validiertes JSON, Metadaten (`model`, `slug`, ggf. `prompt_version`/Hash), strukturierte Fehler.
Router und Frontend rufen diese Schicht oder schmale Orchestratoren — **nicht** direkt `httpx`/OpenRouter an jeder Ecke verteilt.
**Frühere Konkretisierung (Umsetzung gestartet):** Modul `backend/ai_prompt_runtime.py` (`load_ai_prompt_row`, `load_and_render_ai_prompt`, Kontext-Arten) sowie `backend/ai_prompt_job.py` (Pydantic `ExerciseFormAiPromptContext` fuer Uebungs-Prompts — Admin-Vorschau + erweiterbare Router-Nutzung); `exercise_ai` orchestriert OpenRouter nach dem Rendern.
### 2.2 Trennung: Semantik vs. Transport
- **Semantik:** Was soll das Modell liefern? Das hängt an **Prompt-Definition**, **Ausgabeformat** (`text`/`json`) und nachvollziehbarer Validierung — nicht am HTTP-Client.
- **Transport:** OpenRouter, Modellwahl, Retry, Timeouts bleiben in einem oder wenigen Hilfsmodulen.
### 2.3 Kontext-Namespaces für Platzhalter
Platzhalter und erlaubte Keys sind **pro logischer Kontext-Art** definiert, z.B.:
- `exercise_form_ai` — heute: Übungsformular-Vorschläge.
- später: `training_unit`, `framework_program_slot`, `import_wiki`, …
Damit kann der Katalog wachsen, ohne dass alle Keys in einen globalen Soup-Namespace müssen (`exercise_*` vs. `framework_*` ohne Kollisionen). Optional später **präfixierte** Keys (`exercise.title`, `slot.index`).
### 2.4 Komposition / Kaskade explizit
**Ziel:** Mehrteilige Prompts („System“„Nutzer“Anhänge) und **Einbindung anderer Vorlagen** als **Daten** (Kompositionsmodell), nicht nur als unbearbeiteter Freitext mit `{{include}}`.
Skizzen (noch nicht vollständig umgesetzt):
- Tabelle oder JSON-Spalte `composition`/`ai_prompt_segments`: geordnete Segmente mit `role` (`system` \| `user` \| äquivalent zum jeweiligen API-Shape), Quelle (`inline`, `ref_slug`), optional `ref_slug`, Schema-Version.
- Einbindungen mit **Maximaltiefe** und **Zykluserkennung** — keine unbegrenzten Makro-Ketten.
Bis dahin bleiben zusammenhängende Anweisungen in **einem** DB-Template pro Slug tragbar (`exercise_skill_suggestions` + `{{skills_catalog}}` bleiben gültig).
---
## 3. Zieldatenmodell (Schichten)
### 3.1 Definition (`ai_prompts` — bereits vorhanden, evolviert)
| Konzept | Bedeutung |
|--------|-----------|
| `slug`, `category`, `output_format`, `active` | Adressierung & Schalter |
| `template` | aktueller Inhalt |
| `default_template` | Referenz zum Zurücksetzen (Migration **069**) |
| `output_schema` (JSONB) | optional: JSON-Outputs validieren |
**Ausbaustufen:**
1. Nur `template`-Text (**heute**, plus Mustache über `prompt_resolver`).
2. Zusätzlich **Versionierung**: Historie oder `template_version`/Audit (wer hat wann geändert).
3. **Segmentierte Composition** wie in Abschnitt 2.4.
### 3.2 Kontext-Builder pro Domäne
Pro **Kontext-Art** eine klar genannte Routine (Pattern: registrierbare Builder):
| Kontext-Art | Beispiel-Input aus der App | Beispiel-Platzhalter / Daten |
|-------------|----------------------------|------------------------------|
| `exercise_form_ai` | Titel, Ziel/Durchführung (HTML→Plain), Fokuskontext, Retrieval-Profil-Influenza | `exercise_*`, `skills_catalog` |
| `training_unit` (geplant) | Sektionen, Zeiten, Phasen/Streams, verknüpfte Übungs-IDs | `unit_*`, `sections_summary_*` |
| `framework_program` (geplant) | Ziele pro Woche/Schicht, Slots, bereits geplante Einheiten, Skill-Scores | `framework_*`, `slot_*`, aggregierte KPIs |
**Regel:** Planungs-UI baut keine Prompt-Strings; sie liefert **Domän-DTOs** → Builder erzeugen **Platzhalter-Map + ggf. Anhänge**.
### 3.3 Skill-Retrieval und Prompts
`ai_skill_retrieval_profiles` steuert **KatalogZusammenstellung** vor dem Platzhalter `{{skills_catalog}}` — das bleibt **orthogonal** zur Prompt-Verwaltung: Prompt ändert *Anweisung*, Profil ändert *welche Skills im Kontextfenster sind*.
---
## 4. Trainingsplanung & Rahmen — erwartete Komplexität
Risiken: sehr große Kontexte (viele Slots, Streams, Bibliotheken), wiederholte KI-Anfragen, Token-Limits.
**Vorbereitende Strategien:**
1. **Gestufte Kontexte:** Rohdaten → interne Kurzfassungen (optional zweiter Prompt oder heuristisch) → finale Generator-Prompt nur mit komprimierten Summaries.
2. **Slug-Pro-Use-Case:** z.B. `training_unit_trainer_notes`, `framework_slot_coach_hint` — jeweils schmaler Vertrag statt „ein Prompt für alles“.
3. **Output-Verträge:** JSON-Schema + Server-Validierung vor UI; Fehlermeldungen mit Referenz auf Slug/Version.
4. **Feature-Flags / Modell-Overrides** pro Slug (optional in DB oder Env) für Dev/Prod ohne große Codepfade.
---
## 5. Mitai (Jinkendo)
Konzeptionell **gleiche Bausteine** (admin-konfigurierbare Prompts, Platzhalter, Preview), **andere** Kontext-Builder und ggf. andere Mandanten/Overlays. Eine gemeinsame **Resolver-/Mustache-Ebene** ist wünschenswert; **Shinkan-spezifische** Planungs- und Rahmenkontexte bleiben in Shinkan gekapselt.
---
## 6. Betrieb, Sicherheit, Observability
- **Audit:** `updated_by` / Änderungshistorie für Templates (Backlog), heute: Timestamps.
- **Prompt-Injection:** System-/User-Segmente trennen; sensible Regeln in `system`/`developer`-äquivalenten Blöcken (wenn API das hergibt).
- **Logging:** weiter `SHINKAN_AI_DEBUG`; langfristig Hash/Länge des **aufgelösten** Prompts pro Request (ohne Secrets).
- **Kosten/Latenz:** Timeouts, max. Token-Hinweise pro Slug-Konfiguration.
---
## 7. Phasenplan (empfohlen, ohne Big-Bang)
```mermaid
flowchart LR
subgraph laufzeit
A[ai_prompts DB]
B[prompt_resolver Mustache]
C[ai_prompt_runtime]
J[ai_prompt_job Pydantic]
D[exercise_ai OpenRouter]
end
A --> C
C --> B
J --> D
C --> D
B --> D
```
| Phase | Inhalt |
|-------|--------|
| **P0** | `AiPromptContextKind`, `load_ai_prompt_row` zentral; Übungs-KI über Laufzeit. |
| **P1** | `load_and_render_ai_prompt`, `AiPromptUnavailableError`, `render_ai_prompt_template_for_row`; **`ExerciseFormAiPromptContext`** in `ai_prompt_context.py`; **`run_exercise_form_ai_suggestion`**; Übungs-API und Admin-Vorschau nutzen denselben Kontext. |
| **P2** | Versionierung oder Audit-Spalten; **teilweise:** optionales OpenRouter-Modell pro Zeile (`openrouter_model`, Migration 070, Fallback `OPENROUTER_MODEL`); weitere Overrides (Temperatur) offen. |
| **P3** | Composition/Segmente (JSON Schema Version 1) + UI nur für komplexe Slugs. |
| **P4** | Erste Planungs-/Rahmen-Slugs mit dedizierten Buildern und Token-Budget-Strategien. |
---
## 8. Was bewusst vermieden werden soll
- Vollständige „Workflow-Engine“ mit beliebigen Graphen, bevor 23 konkrete Planungs-Anwendungsfälle live sind.
- Pro-Verein-Prompt-Kopien vor klar definierter Produkt-Anforderung (sonst Daten- und Pflege-Spirale).
- Unbegrenzte `include`-rekursive Textmakros ohne Tiefenschutz.
---
## 9. Querverweise
- Ist-Implementierung Prompts/UI: `AI_PROMPT_SYSTEM_SPEC.md`
- Zugriffsrecht Admin-Prompts: `ACCESS_LAYER_ENDPOINT_AUDIT.md`
- Retrieval-Profile: `.claude/docs/working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`
- Übungs-KI-Codepfad: `backend/exercise_ai.py`, `backend/prompt_resolver.py`, `backend/ai_prompt_runtime.py`, `backend/ai_prompt_context.py`, `backend/ai_prompt_job.py`
---
**Version:** 1.0 · **Datum:** 2026-05-30

View File

@ -0,0 +1,202 @@
# KI-gestützte Trainingsplanung Zentrales Konzept
**Version:** 0.3
**Datum:** 2026-05-22
**Status:** Arbeitsdokument (Verfeinerung durch fachliche Konzept-Agentur vorgesehen)
**Ziel:** Einheitlicher Rahmen für **stufenweise** KI-Unterstützung zuerst **Übungsanlage** (Zusammenfassung, Fähigkeiten, Texte), später **Planung** (Abschnitte, Einheiten, Rahmen) ohne vollständigen Übungskatalog im Prompt.
**Maßgebende Version zum Abgleich:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, relevante Einträge in `MODULE_VERSIONS`).
**Verwandte Dokumente:**
`functional/DOMAIN_MODEL.md` · **`functional/AI_EXERCISE_ASSISTANT_VISION.md`** (Übungs-KI: Zielbild vor Planungs-KI) · `functional/TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` (u.a. CURR-003 zu Progressions-/KI-Automatik) · **`working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md`** (mehrstufige Planungs-KI: Daten-„Graph“, Pipeline-Stufen, Code-Schnitte Vorschau gegen späteres Refactoring) · `technical/TRAINING_FRAMEWORK_SPEC.md` · **`technical/SKILL_SCORING_SPEC.md`** (Fähigkeits-Profilierung, Discovery) · `technical/KI_FEATURES_SPEC.md` · `technical/AI_PROMPT_SYSTEM_SPEC.md` · `technical/SKILLS_MATRIX_SPEC.md` · `docs/FACHLICHE_NUTZERFUNKTIONEN.md` · `docs/HANDOVER.md`
---
## 1. Produktliche Leitlinien
- **Nutzer:** Trainer/Vereinskontext, **Gruppenplanung** keine Pflicht zur individuellen Sportler-Verfolgung; Kontext soll primär aus **Gruppe**, **bereits geplanten/durchgeführten Einheiten**, **Rahmen-/Zielen** und **berechtigtem Übungskorpus** bestehen.
- **Human-in-the-loop:** KI liefert **Vorschläge** (Liste, Reihenfolge, Begründung); schreibende Übernahme in Pläne nur nach **Trainer-Bestätigung** oder expliziter Aktion (analog „Manual First“ in `KI_FEATURES_SPEC.md`).
- **Governance-first:** Nur Übungen, die die API bereits für den Mandanten/Kontext **sichtbar** freigibt, dürfen in Kandidatenlisten landen **vor** Retrieval und **vor** jedem Prompt.
### 1.1 Abgleich: aktueller Code- und Schema-Stand (Stand Review 2026-05-22)
| Thema | Ist im Repo | Konsequenz für dieses Konzept |
|--------|-------------|-------------------------------|
| **OpenRouter / LLM im Backend** | Produktiver Aufruf für ÜbungsSuggest in `openrouter_chat.py`, `exercise_ai.py`; Endpunkte **`POST …/exercises/ai/suggest`** und **`POST …/{id}/ai/regenerate`**; Migration **067** (`ai_prompts`, `summary_ai_generated`). **`db.py`**-Bootstrap nutzt **`display_name`**. | **Übungs-Assistent (P0)** vorhanden; generalisierter Service + **Planungs-KI** folgen. |
| **Übungs-KI laut Spec** | P0: Kurzfassung + SkillVorschläge (`include_summary` / `include_skills`); **kein** Auto-KI beim Speichern (S5 im Umsetzungsplan). | Feinspez: `summary_ai_generated` bei manueller Kurzfassung zurücksetzen; Rate-Limits; Prompt-Admin-UI. |
| **Fähigkeiten-Stammdaten** | Migration **`065_skills_wiki_karate_relevance`:** `skills.karate_relevance` (Text), `skills.relevance_level` (13, optional); dazu weiterhin `description`, `focus_areas`, Kategorien, `skill_level_definitions` (Level 15 je Skill). | Diese Felder sind **expliziter Prompt-Kontext** für Skill-Vorschläge (Disambiguierung Karate vs. universal) siehe §6. |
| **Skill-Scoring & Discovery (ohne LLM)** | Router `skill_profiles.py` + Modul `skill_scoring.py`: u.a. `GET …/skill-profile` für **Rahmenprogramm**, **Trainingsmodul**, **Progressionsgraph**; `POST /skill-profiles/batch-summaries`; **`GET /api/skill-discovery/suggestions`** (Match Bibliotheksartefakte ⇄ `skill_ids`, mit `library_content_visibility_sql`). | Ergänzt §3 **Stufe 3**: deterministische **Skill-Abdeckung / Artefakt-Discovery** ist **bereits vorhanden** und kann später die **Planungs-KI** speisen (Ziel-Skill-Mengen, Vergleich „Profil des Rahmens“) ersetzt aber **nicht** die TopK-Selektion aus dem **Übungskatalog** für eine konkrete Session. |
| **Profil / Planungs-Präferenzen** | `profiles.training_planning_prefs` (JSONB, vgl. `MODULE_VERSIONS``profiles`), Planungsmodul mit u.a. **Vorlagen inkl. Split-Sessions** (`planning`), `training_units` mit **Publish in Rahmen-Slot-Blueprint**. | Zukünftige KI-Planung kann **Prefs** und **Vorlagen-Struktur** als weiche Constraints einbeziehen; Rahmen↔Einheit-Fluss ist produktiv erweitert für KI nur relevant, sobald Planungs-Endpunkte angebunden werden. |
| **Übungsliste API** | Keyset-Pagination u.a. `cursor_updated_at` + Tie-break `id` (`exercises`-Modul laut `MODULE_VERSIONS`). | Retrieval-Pipelines sollten **cursorbasiert** paginieren, nicht „alle IDs auf einmal“ laden. |
**Nächster produktiver Fokus:** Prompt-/AdminUI zur Pflege von `ai_prompts`, **Rate-Limits**, optional **Auto-KI beim Speichern**; danach Übergang zur **Planungs-KI** laut diesem Dokument.
**Architektur-Vorschau (Planungs-KI):** Damit die **kleinere, starre** Übungs-Pipeline nicht zur stillen Vorlage für Planung wird, sind **eigenes Modul**, **stufenweise Outputs mit Validierung** und ein **kompaktes Kontext-DTO** vorgesehen — siehe **`working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md`**.
---
## 2. Kernproblem: Skalierung des Kontextes
Aus einer **großen Übungssammlung** („>1000 Übungen“) können weder alle **Felder** (Ziele, Ablauf, Skills, Varianten …) noch alle **Zeilen** sinnvoll in einen LLM-Prompt.
**Abgrenzung Übungsanlage (aktueller Prioritätspfad):** Hier geht der Prompt typischerweise von **einzelnen** Freitexten (`title`, `goal`, `execution`, …) und einem **Skills-Katalog-Auszug** aus nicht vom gesamten Übungsbestand. Trotzdem gilt: Aktive Skills **paginieren** oder **stufig** laden (Subset + zweite Runde nur für Kurzliste), keine vollständigen Romane aus `skill_level_definitions` für hunderte Fähigkeiten auf einmal.
**Festlegung (Planungs-KI):** Der LLM-Prompt erhält immer nur ein **begrenztes Kontext-Paket** mit:
| Paketteil | Zweck |
|-----------|--------|
| **Auftrag** | z.B. Sektionstyp, Dauerbudget, Schwierigkeit, erlaubte Phasen/Streams |
| **Hard Constraints** | Gruppe, Termin/Zeitraum, Governance-Filter bereits angewendet |
| **Komprimierte Historie** | Letzte *N* Einheiten als **Liste von Übungs-IDs + Kurzlabels** (+ optional Haupt-Skills), keine vollen Fließtexte |
| **Ziele / Rahmen** | Kurztexte aus Rahmen-Slot/Zielblöcken oder Trainer-Prompt |
| **Kandidaten-TopK** | z.B. 30120 Übungen, **je Zeile gekürzt** (Titel, `summary`, 25 Skill-Namen/Stufen); **nie** der gesamte Katalog |
| **Strukturierte Kanten optional** | Kleine Mengen Kanten aus Progressionsgraph: „Nachbarn von zuletzt genutzten Übungen“ |
**ZahlenRichtwerte (überarbeitungsfähig):**
Kandidaten **vor** dem LLM typischerweise **50150** Einträge; im Prompt durch Token-Limit weiter **truncate** oder **zweistufig** (grober Ranking-Schritt ohne LLM, dann finer mit LLM auf Top40).
---
## 3. Pipeline: „Selektion vor dem Prompt“
Die **„optimale“** Auswahl entsteht **nicht**, indem das Modell den Katalog „im Kopf“ hält, sondern über eine **mehrstufige Pipeline**:
### Stufe 1 Harte Filter (deterministisch, DB)
Synchron zur bestehenden Suche/List-API-Logik, z.B.:
- Sichtbarkeit / Verein / `official`Regeln
- Aktivitäts-/Archiv-Status der Übung
- Fokusbereich, Stil, Zielgruppe (wenn Trainings-/Gruppenkontext das vorgibt)
- Ausschluss bereits in **dieser Einheit** fester Übernutzung (optional)
Ergebnis: Menge \(M\) kann noch sehr groß sein.
### Stufe 2 Kontext-Verankerung (deterministisch + Graph)
- **Historie:** aus letzten *N* Gruppeneinheiten extrahierte `exercise_id`s (optional Variant).
- **Progressionsgraph:** ausgehend davon Nachbarn (eingehend/ausgehend begrenzte Tiefe) bereits im Produkt als **unterstützend** modelliert (**CURR013**).
- **Rahmen/Slot-Ziele:** Überlapp mit Skill-Tags oder Stichwortliste (falls formalisiert).
- **Variantenketten:** `prerequisite_variant_id` / `progression_level` nur innerhalb bereits gewählter Übung prüfen oder als Hint an den LLM-Block durchreichen.
Ergebnis: **„AnkerMenge“** + **„GraphNachbarschaft“** → priorisierte Kandidaten.
### Stufe 3 Weiches Ranking / Retrieval (halb-strukturiert)
Mindestens **eine** der folgenden Optionen kombinierbar:
1. **Skill-/Facet-Overlap:** Punktezahl, wenn Übungs-Skills mit Ziel-/Matrix-Schwerpunkten übereinstimmen (bereits Daten in `exercise_skills`).
2. **Diversitäts-/Wiederholungsstrafe:** häufig in letzten Wochen geübte Übungen abwerten.
3. **Textsuche:** PostgreSQL **`tsvector`/Volltext** auf `title`, `summary`, ggf. gekürzte `goal` für Trainer-Stichwort „Koordination Sprung“.
4. **Semantische Suche:** Embeddings + **Ähnlichkeitsuche** auf Kurztexte (siehe §5).
5. **Skill-Discovery über Planungs-Artefakte (bereits implementiert):** `GET /api/skill-discovery/suggestions` matching **Bibliotheksartefakte** (u.a. Rahmenprogramm, Trainingsmodul, Progressionsgraph) gegen gegebene `skill_ids`; `GET …/skill-profile` liefert **gewichtete Fähigkeitsprofile** aus den dort verknüpften Übungen (siehe `SKILL_SCORING_SPEC.md`). Das ist ein **deterministischer** Baustein für „welche Artefakte passen zu diesen Skills?“, **nicht** der Ersatz für **TopK-Übung**-Auswahl in einer konkreten Session dort weiter Stufen 12 + Punkte 14/LLM.
Ergebnis: sortierte Liste, **TopK** für den Prompt.
### Stufe 4 LLM (optional zweistufig)
- **Optional 1:** LLM nur **sortiert/rankted** bereits vorgegebene IDs (Ranking mit kurzer Begründung).
- **Optional 2:** Zwei Calls: erst „welche drei Schwerpunkte“ / „Welche Constraints habe ich übersehen?“, zweiter Call nur mit gekürztem TopK nur wenn UX den Mehraufwand rechtfertigt.
**Ausgabe-Contract:** Zurückkommen dürfen **nur gültige `exercise_id`s** aus der übergebenen Kandidatenliste (Server validiert gegen Set); **Halluzinationsrisiko damit entschärft**.
---
## 4. Antwort auf die konkrete Frage: „Alle Übungen in den Prompt?“
**Nein.** Workflow:
1. **DB + Regeln + Graph + Historie** reduzieren auf **einige Hundert bis wenige tausend** Rohzeilen höchstens **intern** aber
2. in den **LLM-Prompt** gehen nur **TopK kompakte Artefakte** (siehe §2).
Das LLM löst dann **Ranking, Reihenfolge, Timing-Hinweise, Trainer-sprachliche Kurzkommentare** nicht die Frage „gibt es diese Übung überhaupt im System?“.
---
## 5. Vector DB (z.B. Qdrant) wann nötig, wann nicht?
### 5.1 Ziel embeddings
Semantic Retrieval: „wie springt Coordinative Belastung ohne das Wort Koordination im Titel zu stehen.“ Das ist **über** reine Filtersuche und oft **über** stumpfe Volltextsuche erreichbar.
### 5.2 Option A ohne separate Vector DB
- **PostgreSQL + `pgvector`** (Extension): Embeddings-Spalte an `exercises` (oder an „Search Document“-Zeilen), Indices, Abfrage zusammen mit SQL-Filtern in **einer Transaktions-DB**.
- **Größenordnung** einige 10k100k Zeilen für Übungen: für Shinkan **oft ausreichend**.
- Vorteile: ein Betriebspfad, Mandanten-/Governance weiter in SQL lösen; Backup wie heute.
### 5.3 Option B Qdrant (oder anderer Dediz-Vektorstore)
Sinnvoller zeitlicher Punkt oder technische Auslöser:
- sehr hohe Latenz-Anforderung mit **Hybrid-Filter** über viele kombinierte Metadaten in nahezu Echtzeit,
- Entkopplung: Embedding-Pipeline läuft asynchron und soll die **Operational DB** nicht beschweren,
- später **mehrsprachig** oder **mehrere Embedding-Versionen**/Re-Index ohne großen Migrationstress,
- Team bevorzugt **Dedicated** Vector-Ops gegenüber Postgres-Ops.
### 5.4 Empfehlung für diese Codebasis (überarbeitungsfähig)
1. **Phase 1:** Harte Filter + Graph + Historie + **PostgreSQL-Volltext** + TopK; LLM erst auf komprimierten Kandidaten → hoher Nutzen ohne neuen Infrastructure-Typen.
2. **Phase 2:** Bei nachweisbaren „Recall-Lücken“ (Trainer findet nichts Passendes trotz großem Korpus) **`pgvector` in Postgres** evaluieren **vor** zusätzlicher Infrastruktur wie Qdrant.
3. **Phase 3:** Qdrant (o. ä.) wenn Größenordnung, Betrieb oder Produkt-Anforderungen **pgvector** sprengen oder klar eine **embedding-first** Produktstraße geplant wird.
**Fazit:** Eine dedizierte **Vector DB ist nicht zwingende Voraussetzung** für vernünftige Selektion; sie ist eine **Ausbaustufe**, wenn **semantische Lücke** und Skalierung es rechtfertigen. **Selektion** ist immer **„Filter + Ranking + kleines TopK“**, unabhängig vom Speicherort der Vektoren.
---
## 6. Datenpflege für gutes Retrieval (fachlicher Hebel)
RetrievalQualität hängt stärker an **Metadaten** als an der Embedding-Technologie allein:
- verlässliche **Skills** (`exercise_skills`, ggf. KI-geholfen bereits laut Spez beim Übungs-Anlegen); `exercise_skills.ai_suggested` und kanonische Stufen (`required_level` / `target_level` als Slugs) für Nachvollziehbarkeit.
- **`skills`-Stamm:** `description`, **`karate_relevance`**, **`relevance_level` (13)**, **`focus_areas`**, Kategorien/Keywords für **Prompt-Kontext** beim Skill-Mapping bei der Übungsanlage; optional **`skill_level_definitions`** für Stufen 15 **gezielt** in die zweite Prompt-Runde (nur Kurzliste Kandidaten).
- sinnvolle **`summary`**-Felder für Karten/Liste/KI-Pack;
- **Progressionsgraph** dort, wo pädagogische Ketten gefestigt sind;
- konsistente **Fokusbereich/Stil**-Zuordnung.
Das fachliche Konzept sollte entscheiden: **wie viel automatische Pflege vs. Trainer-Pflichtfelder**.
---
## 7. Produkt-/Release-Stufen (Anknüpfung)
Priorität **jetzt**: **Übungsanlage**, danach **Planung**.
| Stufe | Nutzen | Technik-Schwerpunkt |
|-------|--------|---------------------|
| **A0** | **Zentraler KI-Service** (ein Modul/Hilfslayer), Prompts aus `ai_prompts` | OpenRouter oder vereinbarter Provider, Timeouts, `503` ohne Key, Parsing/Validation |
| **A1** | **Übungsanlage** (vgl. `KI_FEATURES_SPEC`): `summary`, Skill-Vorschläge inkl. Stufen/Intensität, optional Textglättung | `POST /api/exercises/ai/suggest`, `POST /api/exercises/{id}/ai/regenerate`; Prompt-Kontext: Skills mit `description`, `karate_relevance`, `relevance_level`, optional `skill_level_definitions` für Kurzliste; DB: `summary_ai_generated`, `exercise_skills.ai_suggested` |
| B | „Übungen für Abschnitt vorschlagen“ | Pipeline §3 Stufen 13 + Prompt mit TopK (Übungsliste **keyset-pagination** beachten) |
| C | Reihenfolge / Zeitslots innerhalb bestehender Sektion | Graph + LLM Ranking |
| D | Ganze Einheit (inkl. Phasen/Streams vereinfacht) | strukturiertes JSON + strikte Schema-Validation gegen bestehende `PUT`-Payloads |
| E | Mehreinheiten / RahmenAlignment | Ziele aus Rahmenprogramm, Serie von Slots; **Skill-Profile** (`…/skill-profile`) als Kontextuelle Verstärker |
Die **SelektionsPipeline §3** bleibt für **Planungs**-KI konsistent und wird parametrierbar erweitert; **§1.1** spiegelt den **aktuellen Implementierungs**-Vorsprung (Skill-Scoring ohne LLM) wider.
---
## 8. Compliance & Datenschutz (Kurzhinweis)
- Datenminimierung: **keine Teilnehmerliste** ohne expliziten Scope; Kontext über **Einheiten-Metadaten** und Übungen.
- **OpenRouter**/Modellwahl: Organisation intern klären (AV/Verarbeitungsvertrag, Datenflüsse außerhalb EU siehe Repo-Compliance-Dokumente).
- **Logging:** Prompts keine unnötigen personenbezogenen Daten; wenn Debug: Retention definieren.
---
## 9. Offene Punkte für die fachliche Verfeinerung
- Gewichtung „**Wiederholung** vs. **Progression** vs. **Motivation**“ (domänenspezifisch).
- Umgang mit **Kombinationsübungen** und **Coach-Stufen B/C** in der Datenübergabe.
- Soll das System **„Lücken“** aus der **Matrix-Auflösung** aktiv quantifizieren oder nur Narrative verwenden?
- Akzeptierte **Übersetzungen**: nur Deutsch oder mehrsprachige Embeddings erforderlich?
---
## 10. Glossar
| Begriff | Bedeutung |
|---------|-----------|
| **TopK** | Feste kleine Obergrenze Übungen pro LLM-Anfrage |
| **Hard filter** | Deterministische SQL-/Policy-Einschränkung vor KI |
| **Kontext-Paket** | Zusammengesetztes, tokenlimitiertes Eingabeobjekt für den Prompt |
| **Retrieval** | algorithmischer Schritt ohne Generierung („wer kommt überhaupt in Frage“) |

View File

@ -0,0 +1,331 @@
# Capability-Katalog Shinkan v1
**Status:** Konzept (verbindliche Zieldefinition; M3 teilweise umgesetzt)
**Stand:** 2026-06-06
**Bezüge:** `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` (Stufe E), `MULTI_TENANCY_RBAC_ARCHITECTURE.md`, `CLUB_MEMBERSHIP_AND_FEATURES.v1.md`, **`MEMBERSHIP_RBAC_DECISIONS_2026-06.md`** (Produktentscheidungen)
---
## 1. Zweck
Dieses Dokument definiert **benannte Capabilities** (Wer darf welche **Funktion** ausführen?) — getrennt von:
- **Governance** (Darf ich *dieses Objekt* lesen/ändern? → `visibility`, `club_id`, `created_by`)
- **Feature-Limits** (Wie viel darf der **Verein**? → `CLUB_MEMBERSHIP_AND_FEATURES.v1.md`)
Capabilities beantworten: *„Darf ein Trainer mit Rolle X die Funktion Y im Verein Z überhaupt nutzen?“*
---
## 2. Namenskonvention
```
{domain}.{action}[.{qualifier}]
```
| Segment | Beispiele |
|---------|-----------|
| `domain` | `exercises`, `media`, `planning`, `org`, `platform` |
| `action` | `read`, `create`, `update`, `delete`, `manage`, `execute` |
| `qualifier` | `ai.suggest`, `join_request`, `inbox.review` |
**CRUD-Mapping:**
| Aktion | Capability-Suffix | Bedeutung |
|--------|-------------------|-----------|
| Lesen (Listen/Detail) | `.read` | Navigation + API-Lesen erlaubt |
| Anlegen | `.create` | POST/INSERT |
| Bearbeiten | `.update` | PUT/PATCH (eigenes + berechtigtes Fremdes) |
| Löschen | `.delete` | DELETE (strenger als update) |
| Verwalten | `.manage` | Org-Funktionen, Freigaben, Mitglieder |
| Ausführen (ohne Persistenz) | `.execute` | z. B. KI-Vorschau, Coach-Lauf |
Objektbezogene Feinheiten (nur Ersteller, nur Vereinsadmin des Objekt-Vereins) bleiben in **Governance** — Capabilities sind das **Tür-Schloss** davor.
---
## 3. Account-Lifecycle (Voraussetzung für Capabilities)
| `account_state` | Bedingung | Typische Capabilities |
|-----------------|-----------|------------------------|
| `anonymous` | Keine Session | nur öffentliche Routen (`/login`, Rechtstexte, `clubs/public-directory`) |
| `unverified` | Session, `email_verified=false` | `account.resend_verification`, `account.logout` |
| `verified_pending_club` | Verifiziert, keine aktive `club_members` | `club.join_request`, `club.creation_request` (M7), `account.settings`**kein** Lesezugriff auf Domänen-Inhalte (siehe Entscheidungs-Doc §1.1) |
| `active_member` | Mind. eine aktive Vereinsmitgliedschaft | Domänen-Capabilities gemäß Vereinsrolle |
| `platform_admin` | `role``admin`, `superadmin` | `platform.*` zusätzlich |
**Regel:** Domänen-Capabilities (`exercises.*`, `planning.*`, …) erfordern mindestens `active_member`, sofern nicht `platform_admin`.
---
## 4. Rollen-Scopes
### 4.1 Portal-Rollen (`profiles.role`)
| Rolle | Scope | Kurz |
|-------|-------|------|
| `user` | Portal | Standard nach Registrierung (Zielbild; heute oft `trainer` Legacy) |
| `trainer` | Portal | Legacy — mittelfristig durch `user` + Vereinsrollen ersetzen |
| `admin` | Portal | Plattform-Admin (Vereine anlegen, erweiterte Ops) |
| `superadmin` | Portal | Vollzugriff Plattform + Superadmin-Werkzeuge |
### 4.2 Vereinsrollen (`club_member_roles.role_code`)
| Rolle | Fachlich |
|-------|----------|
| `club_admin` | Vereinsorganisation, Mitglieder, Struktur |
| `trainer` | Planung, Übungen, Durchführung |
| `content_editor` | Inhalte pflegen (Bibliothek) |
| `division_lead` | Spartenleitung (später division-scope) |
Mehrfachrollen pro Mitgliedschaft sind möglich (OR-Verknüpfung der Capabilities).
### 4.3 Mapping heutiger Helfer → Capabilities
| Heutiger Code (`club_tenancy.py`) | Ziel-Capability-Cluster |
|-----------------------------------|-------------------------|
| `can_manage_club_org` | `org.structure.manage`, `org.members.manage`, `org.inbox.review` |
| `can_plan_in_club` | `planning.*`, `exercises.create/update`, `modules.*`, `framework.*` |
| `is_platform_admin` | `platform.*` (Bypass Mandant, Audit-Pflicht) |
| `is_superadmin` | `platform.superadmin.*` |
---
## 5. Capability-Katalog (v1)
Legende Spalten:
- **Min. Account:** `verified_pending_club` | `active_member` | `platform_admin`
- **Vereinsrollen:** leer = alle aktiven Mitglieder; sonst mindestens eine Rolle
- **Feature-ID:** optionales Kontingent (siehe Club-Membership-Doc); leer = kein Limit
- **Governance:** zusätzliche Objektprüfung ja/nein
### 5.1 Account & Onboarding
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Endpoints / UI |
|---------------|--------------|---------------|------------|----------------|
| `account.settings.read` | `unverified` | — | — | `GET /profiles/me`, Einstellungen |
| `account.settings.update` | `unverified` | — | — | `PUT /profiles/{id}` (eigenes Profil) |
| `account.password.change` | `unverified` | — | — | `PUT /api/auth/pin` |
| `account.resend_verification` | `unverified` | — | — | `POST /api/auth/resend-verification` |
| `club.directory.read` | `verified_pending_club` | — | — | `GET /clubs/public-directory` |
| `club.join_request.create` | `verified_pending_club` | — | — | `POST /me/club-join-requests`, Registrierung mit `requested_club_id` |
| `club.join_request.withdraw` | `verified_pending_club` | — | — | `DELETE /me/club-join-requests/{id}` |
| `club.join_request.read_own` | `verified_pending_club` | — | — | `GET /me/club-join-requests` |
### 5.2 Organisation (Verein)
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Endpoints / UI |
|---------------|--------------|---------------|------------|----------------|
| `org.club.read` | `active_member` | * | — | `GET /clubs`, `GET /clubs/{id}` (eigene Vereine) |
| `org.club.create` | `platform_admin` | — | — | `POST /clubs` |
| `org.club.update` | `platform_admin` | `club_admin` | — | `PUT /clubs/{id}` |
| `org.club.delete` | `platform_admin` | — | — | `DELETE /clubs/{id}` |
| `org.structure.manage` | `active_member` | `club_admin` | `training_groups` | Sparten, Gruppen CRUD |
| `org.members.read` | `active_member` | `club_admin` | — | `GET /clubs/{id}/members` |
| `org.members.manage` | `active_member` | `club_admin` | `active_members` | POST/PUT/DELETE Mitglieder |
| `org.members.directory` | `active_member` | * | — | `GET /clubs/{id}/members/directory` (ohne E-Mail für Nicht-Admins) |
| `org.join_request.review` | `active_member` | `club_admin` | — | Join-Request accept/reject, Inbox |
| `org.inbox.read` | `active_member` | `club_admin` | — | Posteingang Join + Content-Reports |
### 5.3 Übungen
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|---------------|--------------|---------------|------------|------------|
| `exercises.read` | `active_member` | * | — | ja (visibility) |
| `exercises.create` | `active_member` | `trainer`, `content_editor`, `club_admin`, `division_lead` | `exercises` | — |
| `exercises.update` | `active_member` | `trainer`, `content_editor`, `club_admin`, `division_lead` | — | ja |
| `exercises.delete` | `active_member` | `club_admin` (+ Ersteller privat) | — | ja |
| `exercises.bulk_metadata` | `active_member` | `content_editor`, `club_admin` | — | ja |
| `exercises.ai.suggest` | `active_member` | `trainer`, `content_editor`, `club_admin` | `ai_calls` | — |
| `exercises.ai.regenerate` | `active_member` | `trainer`, `content_editor`, `club_admin` | `ai_calls` | ja (edit) |
| `exercises.media.read` | `active_member` | * | — | ja |
| `exercises.media.upload` | `active_member` | `trainer`, `content_editor`, `club_admin` | `exercise_media` | ja |
| `exercises.variants.manage` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
**Representative Endpoints:** `/api/exercises*`, `/api/exercises/ai/*`, Medien-Datei-Download.
### 5.4 Medien-Bibliothek (Archiv)
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|---------------|--------------|---------------|------------|------------|
| `media.library.read` | `active_member` | * | — | ja |
| `media.library.upload` | `active_member` | `trainer`, `content_editor`, `club_admin` | `exercise_media` | ja |
| `media.library.update` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
| `media.library.lifecycle` | `active_member` | `trainer`, `club_admin` | — | ja |
| `media.rights.declare` | `active_member` | `trainer`, `club_admin` | — | ja |
| `media.admin.rights_review` | `platform_admin` | — | — | Plattform-Admin Legacy-Review |
### 5.5 Trainingsmodule & Rahmenprogramme
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|---------------|--------------|---------------|------------|------------|
| `modules.read` | `active_member` | * | — | ja |
| `modules.create` | `active_member` | `trainer`, `content_editor`, `club_admin` | `training_programs` | — |
| `modules.update` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
| `modules.delete` | `active_member` | `club_admin` (+ Ersteller) | — | ja |
| `framework.read` | `active_member` | * | — | ja |
| `framework.create` | `active_member` | `trainer`, `club_admin` | `training_programs` | — |
| `framework.update` | `active_member` | `trainer`, `club_admin` | — | ja |
| `framework.delete` | `active_member` | `club_admin` (+ Ersteller) | — | ja |
| `plan_templates.read` | `active_member` | * | — | ja |
| `plan_templates.manage` | `active_member` | `trainer`, `club_admin` | — | ja |
### 5.6 Progressionspfade
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|---------------|--------------|---------------|------------|------------|
| `progression.read` | `active_member` | * | — | ja |
| `progression.manage` | `active_member` | `trainer`, `content_editor`, `club_admin` | — | ja |
### 5.7 Planung & Durchführung
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|---------------|--------------|---------------|------------|------------|
| `planning.calendar.read` | `active_member` | * | — | ja (Gruppe/Verein) |
| `planning.units.create` | `active_member` | `trainer`, `club_admin`, `division_lead` | `training_units` | ja |
| `planning.units.update` | `active_member` | `trainer`, `club_admin`, `division_lead` | — | ja |
| `planning.units.delete` | `active_member` | `club_admin`, `trainer` (eigene) | — | ja |
| `planning.units.run` | `active_member` | `trainer`, `club_admin`, `division_lead` | — | ja |
| `planning.coach.execute` | `active_member` | `trainer`, `club_admin` | — | ja |
| `planning.ai.suggest` | `active_member` | `trainer`, `club_admin` | `ai_calls` | — |
| `planning.ai.progression_path` | `active_member` | `trainer`, `club_admin` | `ai_calls` | — |
### 5.8 Fähigkeiten & Scoring
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID | Governance |
|---------------|--------------|---------------|------------|------------|
| `skills.catalog.read` | `active_member` | * | — | globaler Katalog |
| `skills.discovery.read` | `active_member` | `trainer`, `content_editor` | — | — |
| `skill_profiles.read` | `active_member` | * | — | ja (Artefakt) |
### 5.9 Governance & Meldungen
| Capability-ID | Min. Account | Vereinsrollen | Feature-ID |
|---------------|--------------|---------------|------------|
| `governance.content_report.create` | `active_member` | * | — |
| `governance.content_report.review` | `active_member` | `club_admin` | — |
| `governance.change_request.*` | `active_member` | `content_editor`, `club_admin` | — |
### 5.10 Plattform (nur Portal-Admin / Superadmin)
| Capability-ID | Min. Account | Portal-Rolle | Feature-ID |
|---------------|--------------|--------------|------------|
| `platform.admin.access` | `platform_admin` | `admin`, `superadmin` | — |
| `platform.users.manage` | `platform_admin` | `superadmin` | — |
| `platform.catalogs.manage` | `platform_admin` | `superadmin` | — |
| `platform.maturity_models.manage` | `platform_admin` | `superadmin` | — |
| `platform.wiki_import.execute` | `platform_admin` | `superadmin` | `wiki_import` |
| `platform.ai_prompts.manage` | `platform_admin` | `superadmin` | — |
| `platform.exercise_enrichment.execute` | `platform_admin` | `superadmin` | `ai_calls` |
| `platform.user_content.moderate` | `platform_admin` | `superadmin` | — |
| `platform.legal_documents.manage` | `platform_admin` | `superadmin` | — |
| `platform.media_storage.manage` | `platform_admin` | `superadmin` | — |
| `platform.club_creation.approve` | `platform_admin` | `superadmin` | — |
*Geplant:* `club.creation_request.submit``verified_pending_club`; Freigabe über `platform.club_creation.approve`.
---
## 6. Standard-Zuordnung Vereinsrolle → Capabilities (v1, fest)
Diese Tabelle ist die **initiale** Grant-Matrix (`club_role_capability_grants`). Später durch Custom Roles ersetzbar — gleiche Capability-IDs.
| Capability-Cluster | `club_admin` | `trainer` | `content_editor` | `division_lead` |
|--------------------|:------------:|:---------:|:----------------:|:---------------:|
| `org.structure.manage` | ✓ | — | — | ✓ (eigene Sparte, später) |
| `org.members.manage` | ✓ | — | — | — |
| `org.join_request.review` | ✓ | — | — | — |
| `exercises.read` | ✓ | ✓ | ✓ | ✓ |
| `exercises.create/update` | ✓ | ✓ | ✓ | ✓ |
| `exercises.delete` | ✓ | — | — | — |
| `exercises.ai.*` | ✓ | ✓ | ✓ | ✓ |
| `media.library.*` | ✓ | ✓ | ✓ | ✓ |
| `modules.*` / `framework.*` | ✓ | ✓ | ✓ | ✓ |
| `planning.*` | ✓ | ✓ | — | ✓ |
| `planning.coach.execute` | ✓ | ✓ | — | ✓ |
| `governance.content_report.review` | ✓ | — | — | — |
---
## 7. API-Vertrag (Ziel)
### 7.1 Effektive Rechte für Frontend
```
GET /api/me/entitlements?club_id={optional}
```
Antwort (Ausschnitt):
```json
{
"account_state": "active_member",
"portal_role": "user",
"club_id": 12,
"club_roles": ["trainer"],
"capabilities": {
"exercises.read": true,
"exercises.ai.suggest": true,
"org.members.manage": false
},
"features": {
"ai_calls": { "allowed": true, "used": 4, "limit": 50, "remaining": 46, "reset_at": "2026-07-01T00:00:00Z" }
}
}
```
Frontend: Navigation und Buttons nur aus dieser Antwort — **keine** duplizierten Rollen-Checks in JSX (Ausnahme: rein kosmetische Labels).
### 7.2 Backend-Enforcement
Zentral (Zielmodul `authorization/capabilities.py` oder Erweiterung `club_tenancy.py`):
```python
assert_capability(tenant, "exercises.ai.suggest", club_id=tenant.effective_club_id)
assert_club_feature(tenant, "ai_calls", club_id=tenant.effective_club_id) # siehe Club-Membership-Doc
# + bestehende Governance auf Objekt-Ebene
```
---
## 8. Implementierungsreihenfolge (Capabilities)
| Phase | Inhalt |
|-------|--------|
| C0 | Account-Gates (`unverified`, `verified_pending_club`) — ohne Capability-DB |
| C1 | `capabilities` + `club_role_capability_grants` seed aus §56 |
| C2 | `GET /api/me/entitlements` + Frontend-Nav |
| C3 | Enforcement: KI-Endpoints, `exercises.create`, `planning.*` |
| C4 | Restliche Router schrittweise; Audit in `ACCESS_LAYER_ENDPOINT_AUDIT.md` |
| C5 | Custom Roles (optional) — gleiche IDs |
---
## 9. Abgrenzung & Drift-Schutz
1. **Neue Nutzerfunktion**`register_capability()` in `rights_registrations/<modul>.py`, dann Endpoint mit `probe_capability`. Namenskonvention hier dokumentieren — **kein** Bulk-Seed in Migrationen.
2. **Kontingent**`register_feature()` im selben Modul; Consume über `consume_club_feature_with_usage`.
3. **Kein** paralleles `if (user.role === 'trainer')` für Sicherheit — nur UX-Fallback.
4. Capability ≠ Feature: `exercises.ai.suggest` (darf ich?) vs. `ai_calls` (wie viel übrig?).
5. Plattform-Admin-Bypass dokumentieren und auditieren (`platform_admin` sieht Mandant, nicht automatisch alle Quotas).
Siehe **`docs/working/RIGHTS_AND_FEATURES_REGISTRY.md`** (Registry-first, ersetzt Katalog-first aus 079).
---
## 10. Referenzen
| Dokument | Inhalt |
|----------|--------|
| `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` | Vereinsabo, Feature-Registry, Kontingente |
| `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | TenantContext, Governance, Stufe E |
| `MULTI_TENANCY_RBAC_ARCHITECTURE.md` | §4.6 Vereinsabo-Zielbild |
| `ACCESS_LAYER_ENDPOINT_AUDIT.md` | Endpoint-Pflege |
| Mitai `FEATURE_ENFORCEMENT.md` | 4-Phasen-Rollout-Vorbild |
---
**Changelog**
- 2026-06-06: v1 — Initial-Katalog aus Ist-Code (`club_tenancy`, Router-Inventar) + Ziel-Onboarding.

View File

@ -0,0 +1,478 @@
# Vereins-Membership & Feature-System Shinkan v1
**Status:** Konzept + M1M3 teilweise produktiv (siehe Entscheidungs-Doc §2)
**Stand:** 2026-06-06
**Bezüge:** Schwesterprojekt Mitai (`v9c_subscription_system.sql`, `FEATURE_ENFORCEMENT.md`), `CAPABILITY_CATALOG.v1.md`, `MULTI_TENANCY_RBAC_ARCHITECTURE.md` §4.6, `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, **`MEMBERSHIP_RBAC_DECISIONS_2026-06.md`**
---
## 1. Zweck
Shinkan verkauft und limitiert **nicht Einzelpersonen** (wie Mitai), sondern **Vereine**. Dieses Dokument definiert:
- das **Feature-Registry**-Muster (limitierbare Funktionen),
- das **Vereins-Abo** (`club_plans`, `club_subscriptions`),
- **Kontingente** und Enforcement,
- die **Abbildung von Mitai** und **Vermeidung von Refactoring-Schulden**.
Capabilities (Rollen: *darf ich die Funktion?*) → `CAPABILITY_CATALOG.v1.md`.
---
## 2. Grundprinzip: Zwei Achsen
```mermaid
flowchart TB
subgraph cap [Achse 1 — Capabilities]
CR[club_role_capability_grants]
PR[portal_role_capability_grants]
end
subgraph feat [Achse 2 — Features / Kontingente]
FP[club_plans]
FPL[club_plan_limits]
FS[club_subscriptions]
FU[club_feature_usage]
end
subgraph gov [Achse 3 — Governance]
GV[visibility / club_id / created_by]
end
REQ[HTTP Request] --> ACCT[Account-Lifecycle]
ACCT --> cap
cap --> gov
gov --> feat
feat --> EXEC[Ausführung + increment]
```
| Frage | System | Subjekt |
|-------|--------|---------|
| Darf Trainer X KI nutzen? | Capability `exercises.ai.suggest` | `profile_id` + `club_role` |
| Wie viele KI-Aufrufe hat Verein Y? | Feature `ai_calls` | **`club_id`** |
| Darf ich diese Übung ändern? | Governance | Objekt + Mitgliedschaft |
**Beide Achsen müssen erfüllt sein** (AND), außer dokumentierte Plattform-Ausnahmen.
---
## 3. Mitai-Mapping (was übernehmen, was nicht)
### 3.1 Übernehmen (Pattern)
| Mitai (Person) | Shinkan (Verein) | Anmerkung |
|----------------|------------------|-----------|
| `features` (TEXT-PK, Registry) | `features` (`app='shinkan'`) | Gemeinsames Muster, ggf. später Jinkendo-weit |
| `tiers` | `club_plans` | Produktdefinition |
| `tier_limits` | `club_plan_limits` | Matrix Plan × Feature |
| `user_feature_restrictions` | `club_feature_overrides` | Admin-Override pro Verein |
| `user_feature_usage` | `club_feature_usage` | Verbrauch pro Verein |
| `access_grants` | `club_access_grants` | Trial, Promo, manuelle Freischaltung |
| `check_feature_access()` | `check_club_feature_access()` | Subjekt `club_id` |
| `increment_feature_usage()` | `increment_club_feature_usage()` | Nur bei INSERT / KI-Call |
| 4-Phasen-Rollout | identisch | Log → UI → Hard-Block |
| `GET /api/features/usage` | `GET /api/clubs/{id}/entitlements` | siehe Capability-Doc §7 |
### 3.2 Nicht übernehmen
| Mitai | Shinkan-Grund |
|-------|---------------|
| `profiles.tier` als Haupt-Abo | Verein zahlt, nicht Einzeltrainer |
| `subscriptions` (Shinkan `001`, INT-Features) | Ungenutzt, Schema-Drift |
| `get_effective_tier(profile_id)` für Shinkan-Limits | Ersetzen durch `get_effective_club_plan(club_id)` |
| Profil-zentrierte Enforcement-Hooks allein | Primär `club_id`; Profil nur für Attribution |
### 3.3 Parallelität Jinkendo-Familie (später)
`CENTRAL_SUBSCRIPTION_SYSTEM.md` (Mitai): zentrales Personen-Abo über Apps.
**Zielbild ohne Refactoring:**
```
features.enforcement_subject ∈ { 'club', 'profile', 'portal' }
effektives_limit(feature) = merge(
club_plan_limit(club_id, feature), # Shinkan-Hauptquelle
profile_grant_limit(profile_id, feature) # optional Jinkendo-Bonus
)
```
Merge-Regel (Vorschlag): **Maximum** der erlaubten Kontingente, boolean = OR. Details vor Stripe festlegen.
---
## 4. Ist-Zustand Shinkan (Drift — zuerst bereinigen)
| Artefakt | Problem |
|----------|---------|
| `backend/migrations/001_auth_membership.sql` | `features.id SERIAL`, `tier_limits.tier VARCHAR` |
| `backend/auth.py` `check_feature_access()` | Erwartet Mitai-v9c-Schema (`features.id TEXT`, `tier_id`, `limit_type`, …) |
| Kein Router | Ruft `check_feature_access` auf |
| `profiles.tier` | Existiert, ohne Shinkan-Enforcement |
**Pflicht vor Phase 3 (Enforcement):** Migration `0XX_club_features_v1.sql` — v9c-kompatibles Feature-Schema + Vereins-Tabellen; alte `001`-Feature-Zeilen migrieren oder deprecaten.
---
## 5. Ziel-Schema (v1)
### 5.1 Feature-Registry (app-weit, Mitai-kompatibel)
```sql
-- Konzept — Implementierung als nummerierte Migration
CREATE TABLE features (
id TEXT PRIMARY KEY, -- z.B. 'ai_calls'
app TEXT NOT NULL DEFAULT 'shinkan',
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL, -- 'content'|'planning'|'ai'|'org'|'integration'|'platform'
limit_type TEXT NOT NULL DEFAULT 'count', -- 'count' | 'boolean'
reset_period TEXT NOT NULL DEFAULT 'never', -- 'never' | 'daily' | 'monthly'
default_limit INTEGER, -- NULL=∞, 0=aus
enforcement_subject TEXT NOT NULL DEFAULT 'club', -- 'club'|'profile'|'portal'
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
### 5.2 Vereins-Produkte & Abo
```sql
CREATE TABLE club_plans (
id TEXT PRIMARY KEY, -- 'free', 'verein_starter', 'verein_pro'
name TEXT NOT NULL,
description TEXT,
price_monthly_cents INTEGER,
price_yearly_cents INTEGER,
stripe_price_id_monthly TEXT,
stripe_price_id_yearly TEXT,
active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE club_subscriptions (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT NOT NULL REFERENCES club_plans(id),
status TEXT NOT NULL DEFAULT 'active', -- active|trial|past_due|cancelled
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ends_at TIMESTAMPTZ,
trial_ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (club_id) -- ein aktiver Plan pro Verein (v1)
);
CREATE TABLE club_plan_limits (
id SERIAL PRIMARY KEY,
plan_id TEXT NOT NULL REFERENCES club_plans(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER, -- NULL=∞, 0=deaktiviert
UNIQUE (plan_id, feature_id)
);
```
### 5.3 Overrides, Grants, Verbrauch
```sql
CREATE TABLE club_feature_overrides (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER NOT NULL,
reason TEXT,
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (club_id, feature_id)
);
CREATE TABLE club_access_grants (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT REFERENCES club_plans(id),
feature_id TEXT REFERENCES features(id), -- optional Einzel-Feature
grant_limit INTEGER,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
reason TEXT,
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL
);
CREATE TABLE club_feature_usage (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
usage_count INTEGER NOT NULL DEFAULT 0,
reset_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
UNIQUE (club_id, feature_id)
);
-- Optional: Attribution / Fairness / Audit
CREATE TABLE club_feature_usage_events (
id BIGSERIAL PRIMARY KEY,
club_id INT NOT NULL,
feature_id TEXT NOT NULL,
profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
action TEXT NOT NULL, -- 'ai_suggest', 'exercise_create', ...
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### 5.4 Capabilities (Rollen — Kurzreferenz)
Siehe `CAPABILITY_CATALOG.v1.md` für IDs. Tabellen:
```sql
CREATE TABLE capabilities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
domain TEXT NOT NULL,
min_account_state TEXT NOT NULL DEFAULT 'active_member',
linked_feature_id TEXT REFERENCES features(id), -- optional Kontingent
active BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE club_role_capability_grants (
role_code TEXT NOT NULL, -- club_admin, trainer, ...
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (role_code, capability_id)
);
CREATE TABLE portal_role_capability_grants (
portal_role TEXT NOT NULL, -- admin, superadmin
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (portal_role, capability_id)
);
```
---
## 6. Shinkan Feature-Katalog (Seed v1)
Übernahme aus `001_auth_membership.sql` + Ist-Endpoints, angereichert:
| feature_id | category | limit_type | reset_period | enforcement_subject | Default Free | Beschreibung |
|------------|----------|------------|--------------|---------------------|--------------|--------------|
| `exercises` | content | count | never | club | 100 | Anzahl Übungen im Verein (Bestand) |
| `exercise_media` | content | count | monthly | club | 20 | Medien-Uploads / Monat |
| `training_units` | planning | count | monthly | club | 40 | Geplante/durchgeführte Einheiten |
| `training_programs` | planning | count | never | club | 5 | Module + Rahmenprogramme (kombiniert v1) |
| `training_groups` | org | count | never | club | 10 | Trainingsgruppen |
| `active_members` | org | count | never | club | 25 | Aktive Mitglieder |
| `ai_calls` | ai | count | monthly | club | 0 | KI-Aufrufe (Suggest, Regenerate, Planung) |
| `ai_pipeline` | ai | boolean | never | club | 0 | Erweiterte KI-Pipelines (Batch, später) |
| `wiki_import` | integration | boolean | never | portal | 0 | MediaWiki-Import (Superadmin) |
| `data_export` | integration | boolean | never | club | 0 | Export-Funktionen (wenn eingeführt) |
**Hinweis:** Free-Defaults sind Produktentscheidung — Tabelle dient Implementierung.
### 6.1 Beispiel-Pläne (Seed)
| plan_id | ai_calls/Monat | exercises | active_members |
|---------|----------------|-----------|----------------|
| `free` | 0 | 100 | 25 |
| `verein_starter` | 30 | 500 | 80 |
| `verein_pro` | 200 | NULL (∞) | NULL |
| `pilot` | 100 | NULL | NULL |
Jeder Verein erhält bei Anlage durch Superadmin initial `club_subscriptions.plan_id = 'free'` (oder `pilot`).
---
## 7. Auflösungslogik
### 7.1 Effektiver Vereinsplan
```python
def get_effective_club_plan(cur, club_id: int) -> str:
"""
1. Aktiver club_access_grants mit plan_id (höchste Priorität, Zeitfenster)
2. club_subscriptions.status == 'active' → plan_id
3. Fallback 'free'
"""
```
### 7.2 Feature-Limit (analog Mitai `check_feature_access`)
```python
def check_club_feature_access(
cur,
club_id: int,
feature_id: str,
*,
profile_id: int | None = None, # nur für Logging / optionale Profil-Boni später
) -> dict:
"""
Priorität:
1. club_feature_overrides (club_id, feature_id)
2. club_plan_limits für get_effective_club_plan(club_id)
3. features.default_limit
Auswertung:
- limit_type boolean: limit_value == 1
- limit_type count: used < limit (club_feature_usage, reset beachten)
Returns: { allowed, limit, used, remaining, reason, reset_at }
"""
```
### 7.3 Vollständige Request-Kette
```
1. require_auth
2. assert_account_state(min_state) # unverified / verified_pending_club / active_member
3. get_tenant_context
4. assert_capability(tenant, cap_id) # Rollen-Achse
5. assert_content_governance(...) # nur bei Objekt-Endpoints
6. check_club_feature_access(club_id, feature_id)
7. … Business-Logik …
8. consume_club_feature_with_usage(…) + merge_feature_usage_into_response(payload, usage)
# Standard: zählen, JSON-Log phase=consume, feature_usage in Response
9. optional: club_feature_usage_events (profile_id, action)
```
**Response-Standard (alle Consume-Endpoints):** JSON-Feld `feature_usage` — Map `feature_id → { allowed, used, limit, remaining, reason, … }` wie `GET /me/entitlements`. Frontend: `request()` synchronisiert Entitlements automatisch (`featureUsageSync.js`); UI-Komponenten brauchen keinen Einzelcode.
### 7.4 Wer zählt als Verbrauch?
| Aktion | increment | Subjekt |
|--------|-----------|---------|
| `POST /exercises` (neu) | `exercises` | `club_id` des Objekts oder `effective_club_id` |
| Medien-Upload | `exercise_media` | Verein des Mediums |
| KI Suggest/Regenerate | `ai_calls` | `effective_club_id` |
| Mitglied hinzufügen | `active_members` | Ziel-`club_id` |
| Trainingsgruppe anlegen | `training_groups` | `club_id` |
**Mitai-Regel:** Counter **nicht** bei UPDATE/DELETE erhöhen.
---
## 8. API-Oberfläche
### 8.1 Nutzer / Vereinsadmin
```
GET /api/clubs/{club_id}/entitlements
```
Kombiniert Capabilities + Feature-Kontingente (siehe `CAPABILITY_CATALOG.v1.md` §7.1).
```
GET /api/me/entitlements?club_id=12
```
Bequemer Alias für aktiven Verein.
### 8.2 Superadmin / Plattform
| Endpoint | Zweck |
|----------|-------|
| `GET/PUT /api/admin/club-plans` | Plan-CRUD |
| `GET/PUT /api/admin/club-plan-limits` | Matrix |
| `GET/PUT /api/admin/clubs/{id}/subscription` | Verein-Abo |
| `GET/PUT /api/admin/clubs/{id}/feature-overrides` | Sonderkontingente |
| `POST /api/admin/clubs/{id}/access-grants` | Trial/Promo |
Vorbild UI: Mitai `AdminTierLimitsPage.jsx`, `AdminUserRestrictionsPage.jsx` → Vereins-Kontext.
### 8.3 Geplant: Vereinsgründung
```
POST /api/club-creation-requests # Nutzer (verified_pending_club)
GET /api/admin/club-creation-requests
POST /api/admin/club-creation-requests/{id}/approve # legt club + subscription an
```
---
## 9. Vier-Phasen-Rollout (aus Mitai)
| Phase | Shinkan-Aktivität | Nutzer sichtbar? |
|-------|-------------------|------------------|
| **0** | Schema-Migration, Seed `features` + `club_plans`, Drift `001` bereinigen | Nein |
| **1** | Account-Gates + Capability-Grants (ohne Limits) | Onboarding-Hinweise |
| **2** | `check_club_feature_access`**nur JSON-Log** (`feature_logger` analog Mitai) | Nein |
| **3** | `GET …/entitlements` + UsageBadge im UI | Ja (Kontingent-Anzeige) |
| **4** | HTTP 403 bei Limit + `increment` | Ja (Hard-Block) |
**Reihenfolge innerhalb Phase 4:** zuerst `ai_calls`, dann `exercise_media`, dann Bestands-Limits (`exercises`, `active_members`).
---
## 10. CI / Test-Isolation (Betrieb)
Unabhängig vom Membership-System — **Pflicht** wegen Prod-Vorfälle (`access_layer_it_*@test.local`):
| Regel | Umsetzung |
|-------|-----------|
| Integrationstests nie gegen Prod-DB | Eigene Test-DB oder Job-Postgres in Gitea |
| `ENVIRONMENT=production` + `ALLOW_INTEGRATION_TESTS` | Default `0`, Tests abbrechen |
| Test-Accounts | E-Mail `@test.local` oder `profiles.is_test_account` |
| Cleanup | Fixture-`finally` + Nightly-Job löscht Leichen |
`.gitea/workflows/test.yml`: pytest-backend gegen Deploy-DB **ersetzen** durch isolierte DB (eigenes Epic, parallel zu Membership).
---
## 11. Implementierungs-Roadmap (gesamt)
| Schritt | Deliverable | Membership-relevant |
|---------|-------------|-------------------|
| M0 | CI-Isolation + Prod-Cleanup-Runbook | Nein |
| M1 | Migration Feature-Schema v9c + `club_plans`/`club_subscriptions` (leer nutzbar) | **Ja** |
| M2 | `check_club_feature_access` + Seed Pläne | **Ja** |
| M3 | Account-Lifecycle + Capability-Grants | Capabilities |
| M4 | `GET /me/entitlements` | **Ja** |
| M5 | Enforcement `ai_calls` (Phase 4) | **Ja** |
| M6 | Admin Plan-Matrix UI | **Ja** |
| M7 | `club_creation_requests` | Prozess |
| M8 | Stripe / Rechnung | Später |
**Nach Produktentscheidungen 2026-06-06** (Details `MEMBERSHIP_RBAC_DECISIONS_2026-06.md` §4):
| Phase | Paket | Priorität |
|-------|--------|-----------|
| A | Onboarding-Gates vollständig (`verified_pending_club`) | **Als Nächstes** |
| B | M7 Vereinsgründung beantragen | hoch |
| C | M5 Hard-Block `ai_calls` | danach |
| D | M6 Superadmin-UI | danach |
| E | Systemrolle `co_trainer` + Frontend-Entitlements | v1 Rollen |
| F | Trainer-Member-Budgets (v2) | später |
---
## 12. Offene Produktentscheidungen
Vor M6 festlegen:
1. **Zählen `active_members`:** alle Mitglieder oder nur Rollen mit Planungsrecht?
2. **Soft-Limit vs. Hard-Stop:** Warnung bei 80 % oder sofort 403?
3. **Pilotverein:** eigener Plan `pilot` mit hohen Limits?
4. **KI-Fairness:** nur Vereinslimit oder zusätzlich Max pro Trainer/Monat?
5. **Offizielle Inhalte:** für `verified_pending_club` sichtbar oder gesperrt? → **entschieden: gesperrt** (`MEMBERSHIP_RBAC_DECISIONS_2026-06.md` §1.1)
6. **Portal `admin` vs. `superadmin`:** Wer darf Vereine anlegen? (Ziel: nur `superadmin` für Freigabe)
---
## 13. Referenzen
| Pfad | Inhalt |
|------|--------|
| `c:/dev/mitai-jinkendo/backend/migrations/v9c_subscription_system.sql` | Mitai-Schema-Vorlage |
| `c:/dev/mitai-jinkendo/.claude/docs/architecture/FEATURE_ENFORCEMENT.md` | 4-Phasen-Modell |
| `c:/dev/mitai-jinkendo/.claude/docs/technical/MEMBERSHIP_SYSTEM.md` | Mitai-Hauptdoku |
| `c:/dev/mitai-jinkendo/.claude/docs/technical/CENTRAL_SUBSCRIPTION_SYSTEM.md` | Jinkendo-Familie später |
| `CAPABILITY_CATALOG.v1.md` | Rollen & Capabilities |
| `MULTI_TENANCY_RBAC_ARCHITECTURE.md` §4.6 | Ursprüngliches Vereinsabo-Zielbild |
| `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Stufe E/F |
---
**Changelog**
- 2026-06-06: v1 — Mitai-Mapping, Ziel-Schema, Feature-Seed, Auflösungslogik, Rollout.

View File

@ -1,11 +1,12 @@
# Exercises API Specification # Exercises API Specification
**Version:** 1.5 **Version:** 1.6
**Datum:** 2026-05-08 **Datum:** 2026-05-20
**Status:** Teilweise implementiert (Liste mit Filtern + Varianten + Medienlimits + Progressionsgraphen siehe Code) **Status:** Teilweise implementiert (Liste mit Filtern + Varianten + Medienlimits + Progressionsgraphen siehe Code)
**Autor:** Claude Code **Autor:** Claude Code
**Änderungen v1.4:** Endpoints **`/exercise-progression-graphs`** inkl. Kanten, **`POST …/edges/sequence`**, **`POST …/edges/delete-batch`** — Detailtabellen siehe **`TRAINING_FRAMEWORK_SPEC.md`** §3.3 **Änderungen v1.6:** Freigabelevel-UI-Hinweis; `exercise_skills` ohne `is_primary` in Requests (Legacy-Feld wird ignoriert/forciert false); Permissions-Bereich an Ist-Code angeglichen; Intensität kanonisch `niedrig|mittel|hoch`
**Änderungen v1.5:** Medien-/Inline-Workflow aktualisiert (Modal-Picker, Drag&Drop UX im Frontend), Klarstellung zu `context` (legacy/optional), Hinweise zu Platzhaltern in Rich-Text-Feldern. **Änderungen v1.5:** Medien-/Inline-Workflow aktualisiert (Modal-Picker, Drag&Drop UX im Frontend), Klarstellung zu `context` (legacy/optional), Hinweise zu Platzhaltern in Rich-Text-Feldern.
**Änderungen v1.4:** Endpoints **`/exercise-progression-graphs`** inkl. Kanten, **`POST …/edges/sequence`**, **`POST …/edges/delete-batch`** — Detailtabellen siehe **`TRAINING_FRAMEWORK_SPEC.md`** §3.3
**Änderungen v1.3:** `GET /exercises` erweiterte Query-Parameter (`include_variants`, Multi-Filter, `ai_search`-Platzhalter); Dokumentation angepasst **Änderungen v1.3:** `GET /exercises` erweiterte Query-Parameter (`include_variants`, Multi-Filter, `ai_search`-Platzhalter); Dokumentation angepasst
**Änderungen v1.2:** KI-Assistenz Endpoints, Skill-Level-System (benannte Stufen), intensity als low/medium/high **Änderungen v1.2:** KI-Assistenz Endpoints, Skill-Level-System (benannte Stufen), intensity als low/medium/high
**Änderungen v1.1:** Exercise Blocks Endpoints, Permissions dokumentiert, age_groups korrigiert **Änderungen v1.1:** Exercise Blocks Endpoints, Permissions dokumentiert, age_groups korrigiert
@ -185,11 +186,11 @@ Lightweight-Liste; bei `include_variants=true` zusätzlich z.B.:
"skill_id": 10, "skill_id": 10,
"skill_name": "Distanzgefühl", "skill_name": "Distanzgefühl",
"skill_category": "Kumite", "skill_category": "Kumite",
"is_primary": true,
"intensity": "hoch", "intensity": "hoch",
"required_level": "grundlagen", "required_level": "grundlagen",
"target_level": "aufbau", "target_level": "aufbau",
"ai_suggested": false "ai_suggested": false,
"is_primary": false
} }
], ],
@ -307,7 +308,6 @@ Lightweight-Liste; bei `include_variants=true` zusätzlich z.B.:
"skills": [ "skills": [
{ {
"skill_id": 10, "skill_id": 10,
"is_primary": true,
"intensity": "hoch", "intensity": "hoch",
"required_level": "grundlagen", "required_level": "grundlagen",
"target_level": "aufbau" "target_level": "aufbau"
@ -578,7 +578,6 @@ Wird beim Klick auf „✨ KI-Vorschlag" im Formular aufgerufen.
"required_level": "grundlagen", "required_level": "grundlagen",
"target_level": "aufbau", "target_level": "aufbau",
"intensity": "hoch", "intensity": "hoch",
"is_primary": true,
"confidence": 0.92 "confidence": 0.92
}, },
{ {
@ -588,7 +587,6 @@ Wird beim Klick auf „✨ KI-Vorschlag" im Formular aufgerufen.
"required_level": "einsteiger", "required_level": "einsteiger",
"target_level": "grundlagen", "target_level": "grundlagen",
"intensity": "mittel", "intensity": "mittel",
"is_primary": false,
"confidence": 0.74 "confidence": 0.74
} }
] ]
@ -621,6 +619,38 @@ Trainer muss im Frontend aktiv übernehmen.
## Permissions ## Permissions
**UI-Hinweis:** Das Feld `visibility` heißt in der Oberfläche **Freigabelevel** (`exerciseGovernanceLabels.js`).
### Lesen (`GET /exercises`, `GET /exercises/{id}`)
| `visibility` | Wer darf lesen? |
|--------------|-----------------|
| `official` | Plattform-weit |
| `private` | Ersteller (`created_by`); Plattform-Admin |
| `club` | Aktive Mitglieder des Objekt-`club_id`; Plattform-Admin ohne Mitgliedschaft (Audit-Zugang) |
Implementierung: `library_content_visible_to_profile` / `exercise_visible_to_profile` in `club_tenancy.py`.
### Bearbeiten (`PUT`, Varianten-CRUD, Medien an Übung)
| Bedingung | Wer darf bearbeiten? |
|-----------|----------------------|
| Ersteller | Immer (eigene Übung) |
| Plattform-Admin | Immer |
| `visibility=club` | Zusätzlich **`can_plan_in_club`** im Objekt-Verein: `club_admin`, `trainer`, `content_editor`, `division_lead` |
Implementierung: `_assert_can_edit_exercise` in `exercises.py`. **Varianten** haben kein eigenes Owner-Feld — gleiche Prüfung wie Eltern-Übung.
### Löschen (`DELETE /exercises/{id}`)
| `visibility` | Wer darf löschen? |
|--------------|-------------------|
| `official` | Nur Plattform-Admin |
| `club` | Nur **`club_admin`** im Objekt-Verein |
| `private` | Ersteller; oder Vereins-Admin, der mit dem Ersteller einen gemeinsamen Verein teilt |
Implementierung: `_assert_can_delete_exercise` in `exercises.py`.
### Sichtbarkeits-Workflow ### Sichtbarkeits-Workflow
| Von → Nach | Wer darf das? | | Von → Nach | Wer darf das? |
@ -638,11 +668,12 @@ Trainer muss im Frontend aktiv übernehmen.
| `club → official` | Club-Admin, Super-Admin | | `club → official` | Club-Admin, Super-Admin |
| `official → club` | Super-Admin | | `official → club` | Super-Admin |
### Owner-Checks ### Owner-Checks (veraltet — siehe Tabellen oben)
- **Bearbeiten** (PUT): Nur Ersteller oder Club-Admin Die folgenden Kurzregeln sind durch die Ist-Implementierung ersetzt; nur zur historischen Einordnung:
- **Löschen** (DELETE): Nur Ersteller oder Super-Admin
- **Lesen** (`private`): Nur Ersteller - ~~Bearbeiten (PUT): Nur Ersteller oder Club-Admin~~ → siehe **Bearbeiten**-Tabelle (`can_plan_in_club`)
- ~~Löschen (DELETE): Nur Ersteller oder Super-Admin~~ → siehe **Löschen**-Tabelle
**403 Fehler-Beispiel:** **403 Fehler-Beispiel:**
```json ```json
@ -904,7 +935,8 @@ Trainer muss im Frontend aktiv übernehmen.
### Exercise Skills ### Exercise Skills
- `required_level`: enum `einsteiger | grundlagen | aufbau | fortgeschritten | experte` (optional/nullable) - `required_level`: enum `einsteiger | grundlagen | aufbau | fortgeschritten | experte` (optional/nullable)
- `target_level`: enum gleiche Werte (optional/nullable) - `target_level`: enum gleiche Werte (optional/nullable)
- `intensity`: enum `niedrig | mittel | hoch` (optional/nullable) - `intensity`: enum **`niedrig | mittel | hoch`** (optional/nullable; Default beim Speichern **`mittel`**)
- `is_primary`: **Legacy** — Spalte existiert in DB, wird bei POST/PUT **nicht ausgewertet** (immer `false` gespeichert); UI liefert/speichert kein Primär-Flag mehr; Scoring ignoriert das Feld
- `target_level` sollte >= `required_level` sein (Warnung, kein Fehler) - `target_level` sollte >= `required_level` sein (Warnung, kein Fehler)
### Exercise Block Item ### Exercise Block Item

View File

@ -99,20 +99,21 @@ Exercise Block ──── (N) Block Items ──── (1) Exercise
### 1.3 M:N Beziehungen (Primary/Secondary Pattern) ### 1.3 M:N Beziehungen (Primary/Secondary Pattern)
**Regel:** Alle Katalog-Zuordnungen nutzen M:N mit `is_primary` Flag. **Regel:** Katalog-Zuordnungen (Fokus, Stil, Zielgruppe, …) nutzen M:N mit optionalem `is_primary`-Flag.
**Betroffene Relationen:** **Betroffene Relationen (mit `is_primary`):**
- `exercise_focus_areas` (Übung ↔ Fokusbereiche) - `exercise_focus_areas` (Übung ↔ Fokusbereiche)
- `exercise_styles` (Übung ↔ Trainingsstile) - `exercise_styles` / `exercise_style_directions` (Übung ↔ Stilrichtungen)
- `exercise_training_types` (Übung ↔ Trainingsstile)
- `exercise_target_groups` (Übung ↔ Zielgruppen) - `exercise_target_groups` (Übung ↔ Zielgruppen)
- `exercise_training_characters` (Übung ↔ Trainingscharaktere)
- `exercise_skills` (Übung ↔ Fähigkeiten)
**Primary/Secondary Semantik:** **Ausnahme — `exercise_skills`:** Kein Primär-Flag in UI/API mehr; stattdessen **`intensity`** (`niedrig` \| `mittel` \| `hoch`, Default `mittel`). Spalte `is_primary` bleibt Legacy (Backend speichert immer `false`).
**Primary/Secondary Semantik (Katalog-Dimensionen):**
- **Primary:** Hauptzuordnung, entscheidend für Filter/Suche - **Primary:** Hauptzuordnung, entscheidend für Filter/Suche
- **Secondary:** Nebenzuordnung, zusätzlicher Kontext - **Secondary:** Nebenzuordnung, zusätzlicher Kontext
- **Regel:** Genau EINE Primary-Zuordnung pro Dimension - **Regel:** Genau EINE Primary-Zuordnung pro Dimension (wo UI das noch anbietet)
- **UI:** Primary wird visuell hervorgehoben (z.B. fett, farbig) - **UI:** Primary wird visuell hervorgehoben (z. B. fett, farbig) — Fähigkeiten: Intensitäts-Segmente statt Primary
**Legacy-Felder (DEPRECATED):** **Legacy-Felder (DEPRECATED):**
- `exercises.focus_area` → Ignorieren, nutze `exercise_focus_areas` - `exercises.focus_area` → Ignorieren, nutze `exercise_focus_areas`

View File

@ -1,9 +1,10 @@
# Frontend Routing & Navigation Specification # Frontend Routing & Navigation Specification
**Version:** 1.2 **Version:** 1.3
**Datum:** 2026-04-30 **Datum:** 2026-05-20
**Status:** DRAFT - Awaiting Review **Status:** DRAFT - Awaiting Review
**Autor:** Claude Code **Autor:** Claude Code
**Änderungen v1.3:** Übungsformular Tab-Navigation unter `/exercises/:id/edit` (Stammdaten … Medien & Mehr); Freigabelevel als UI-Begriff
**Änderungen v1.2:** Übersicht **Übungen**: Tabs Liste \| Progressionsgraphen auf `/exercises`; Progressions-Editor ohne neue Routen (Panel + Formularblock unter `/exercises/:id/edit`) **Änderungen v1.2:** Übersicht **Übungen**: Tabs Liste \| Progressionsgraphen auf `/exercises`; Progressions-Editor ohne neue Routen (Panel + Formularblock unter `/exercises/:id/edit`)
**Änderungen v1.1:** Übungsvarianten-Bearbeitung nur unter `/exercises/:id/edit` (keine VariantFormPage-Routen) **Änderungen v1.1:** Übungsvarianten-Bearbeitung nur unter `/exercises/:id/edit` (keine VariantFormPage-Routen)
@ -17,7 +18,7 @@
/exercises → ExercisesListPage — Tabs: **Liste** \| **Progressionsgraphen** (`ExerciseProgressionGraphPanel`) /exercises → ExercisesListPage — Tabs: **Liste** \| **Progressionsgraphen** (`ExerciseProgressionGraphPanel`)
/exercises/new → ExerciseFormPage (Create) /exercises/new → ExerciseFormPage (Create)
/exercises/{id} → ExerciseDetailPage (Accordion-Layout) /exercises/{id} → ExerciseDetailPage (Accordion-Layout)
/exercises/{id}/edit → ExerciseFormPage (Edit inkl. Varianten-Editor inline + Block Progressionsgraph) /exercises/{id}/edit → ExerciseFormPage (Edit: Registerkarten + Varianten inline + Progressionsgraph)
/exercise-blocks → ExerciseBlocksListPage (Meine Blocks) /exercise-blocks → ExerciseBlocksListPage (Meine Blocks)
/exercise-blocks/new → ExerciseBlockFormPage (Create) /exercise-blocks/new → ExerciseBlockFormPage (Create)
@ -35,6 +36,25 @@
- Pagination: `/exercises?limit=50&offset=100` - Pagination: `/exercises?limit=50&offset=100`
- Sortierung: `/exercises?sort=created_at&order=desc` - Sortierung: `/exercises?sort=created_at&order=desc`
### 1.2 Übungsformular Registerkarten (`/exercises/new`, `/exercises/:id/edit`)
**Implementierung:** `ExerciseFormPageRoot.jsx` + `ExerciseFormLayout.jsx` (`ExerciseFormTabBar`, `ExerciseFormPanel`).
| Tab-ID | Label | Verfügbarkeit |
|--------|-------|---------------|
| `stammdaten` | Stammdaten | immer |
| `anleitung` | Anleitung | immer |
| `einordnung` | Einordnung | immer |
| `kombination` | Kombination | nur `exercise_kind=combination` |
| `varianten` | Varianten | Edit-Modus; nicht bei Kombination; disabled bei Neuanlage |
| `medien` | Medien & Mehr | Edit-Modus; disabled bei Neuanlage |
**UX-Regeln:**
- Nur ein Panel sichtbar (`activeFormTab`); Navigation über `PageSectionNav`.
- **Freigabelevel** (Feld `visibility`) in Stammdaten — Konstante `EXERCISE_VISIBILITY_FIELD_LABEL`.
- Varianten-Änderungen werden mit **Speichern** in der Aktionsleiste persistiert (`persistPendingVariantChanges`); Button „Variante anlegen“ optional sofort.
- Kein URL-Hash pro Tab (Tab-State nur lokal).
--- ---
## 2. Navigation-Patterns ## 2. Navigation-Patterns
@ -673,7 +693,7 @@ function App() {
--- ---
**Version:** 1.2 **Version:** 1.3
**Letzte Änderung:** 2026-04-30 **Letzte Änderung:** 2026-05-20
**Status:** REVIEWED - Pending Implementation **Status:** REVIEWED - Pending Implementation
**Review-Änderungen:** Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher) **Review-Änderungen:** Formular-Registerkarten; Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)

View File

@ -7,11 +7,16 @@
**Änderungen v1.1:** Prompts sind nicht hardcoded sie werden aus der DB geladen (AI_PROMPT_SYSTEM_SPEC.md) **Änderungen v1.1:** Prompts sind nicht hardcoded sie werden aus der DB geladen (AI_PROMPT_SYSTEM_SPEC.md)
**Verwandte Specs:** AI_PROMPT_SYSTEM_SPEC.md (Prompt-DB + Platzhalter), SKILLS_MATRIX_SPEC.md (Fähigkeitsmatrix) **Verwandte Specs:** AI_PROMPT_SYSTEM_SPEC.md (Prompt-DB + Platzhalter), SKILLS_MATRIX_SPEC.md (Fähigkeitsmatrix)
**Übergeordnete Produkt-Vision** (breiter Scope: Zielausbau, bereichsweise vs. Gesamtüberarbeitung, Varianten, Planungs-/Nachbereitungskontext, Admin-Masse):
`functional/AI_EXERCISE_ASSISTANT_VISION.md`
--- ---
## 1. Übersicht ## 1. Übersicht
Zwei KI-gestützte Assistenzfunktionen beim Anlegen und Bearbeiten von Übungen: KI-gestützte Assistenzfunktionen beim Anlegen und Bearbeiten von Übungen (Mindestpaket dieser Spec):
**Hinweis:** Die beiden folgenden Zeilen entsprechen **P0** der Phasierung in **`AI_EXERCISE_ASSISTANT_VISION.md`**; spätere Funkteile sind dort beschrieben.
| Funktion | Ziel | | Funktion | Ziel |
|---------|------| |---------|------|
@ -155,7 +160,38 @@ KI gibt Vorschläge
Liefert KI-Vorschläge auf Basis von Eingabe-Text, **bevor** die Übung gespeichert wurde. Liefert KI-Vorschläge auf Basis von Eingabe-Text, **bevor** die Übung gespeichert wurde.
Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen. Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
**Request Body:** **Required Fields:** mindestens `goal` ODER `execution`
**Optional Skill-Katalogpriorisierung (Stand 068):**
```json
{
"focus_areas_context": [
{ "focus_area_id": 3, "is_primary": true },
{ "focus_area_id": 1, "is_primary": false }
],
"focus_area_hint": "Karate, Kumite…"
}
```
- `focus_areas_context`: IDs aus Stammdatum **Fokusbereiche**; Primär soll zuerst stehen (`is_primary`). Ohne Feld oder leere Liste gilt das DB-Profil **`is_default`** (`ai_skill_retrieval_profiles`).
- `focus_area_hint`: bleibt lesbarer Text für den Prompt (bestehende Prompts).
**Minimal-Beispiel (Mit Fokus für Retrieval):**
```json
{
"title": "Maai - Distanzübung",
"goal": "…",
"execution": "…",
"focus_areas_context": [ { "focus_area_id": 1, "is_primary": true } ]
}
```
**Minimal-Beispiel ( ohne Fokus — nur Texts):**
```json ```json
{ {
"title": "Maai - Distanzübung", "title": "Maai - Distanzübung",
@ -164,8 +200,6 @@ Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
} }
``` ```
**Required Fields:** mindestens `goal` ODER `execution` (je länger, desto besser)
**Response:** `200 OK` **Response:** `200 OK`
```json ```json
{ {
@ -182,7 +216,6 @@ Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
"required_level": "grundlagen", "required_level": "grundlagen",
"target_level": "aufbau", "target_level": "aufbau",
"intensity": "hoch", "intensity": "hoch",
"is_primary": true,
"confidence": 0.92 "confidence": 0.92
}, },
{ {
@ -192,7 +225,6 @@ Wird beim Klick auf „KI-Vorschlag" im Formular aufgerufen.
"required_level": "einsteiger", "required_level": "einsteiger",
"target_level": "grundlagen", "target_level": "grundlagen",
"intensity": "mittel", "intensity": "mittel",
"is_primary": false,
"confidence": 0.74 "confidence": 0.74
} }
] ]

View File

@ -0,0 +1,243 @@
# Membership, RBAC & Kontingente — Produktentscheidungen
**Status:** Verbindlich (Zielbild & Roadmap-Priorisierung)
**Stand:** 2026-06-06
**Bezüge:** `CAPABILITY_CATALOG.v1.md`, `CLUB_MEMBERSHIP_AND_FEATURES.v1.md`, `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`
Dieses Dokument hält **getroffene Produktentscheidungen** fest (Session 2026-06-06) und ergänzt die v1-Konzept-Specs um Umsetzungsrichtung. Technischer Implementierungsstand: Abschnitt 2.
---
## 1. Getroffene Entscheidungen
### 1.1 Onboarding: `verified_pending_club`
Nutzer **ohne aktive Vereinsmitgliedschaft** (E-Mail verifiziert) dürfen **nur**:
| Erlaubt | Nicht erlaubt (Zielbild) |
|---------|---------------------------|
| Konto / Einstellungen | Übungen, Planung, KI, Medien |
| Vereinsverzeichnis lesen | Vereinsinterne Inhalte (`club`), private Fremdinhalte |
| **Beitrittsantrag** an bestehenden Verein | Vollzugriff auf Bibliothek / offizielle Inhalte (Lesen) — **bewusst gesperrt** bis Mitgliedschaft |
| **Vereinsgründung beantragen** (Prozess M7, Superadmin-Freigabe) | |
**Kein** „Bibliothek durchstöbern“ für Bewerber — reduziert Datenexposition und vereinfacht UX („erst Verein, dann Arbeit“).
Technischer Zustand: `account_state = verified_pending_club` (siehe `CAPABILITY_CATALOG.v1.md` §3).
---
### 1.2 Rollenmodell: Risikoarm statt Big-Bang
**Zielbild (langfristig):**
- **Fest:** nur `superadmin` (Plattform) als nicht konfigurierbare Systemrolle.
- **Dynamisch konfigurierbar:** alle Vereinsrollen und deren Capability-Bundles (später `club_custom_roles`).
- Optional: `admin` (Plattform) als abgeschwächter Portal-Admin bleibt vorerst bestehen (Ist-Code).
**Entscheidung v1 (risikoarm):**
| Maßnahme | Jetzt | Später |
|----------|-------|--------|
| Alte Helfer (`can_plan_in_club`, `if (club_admin)` in JSX) | **Behalten** — weiter produktiv | Schrittweise durch `entitlements` ersetzen |
| Neue Endpoints / Features | Nur über **Capability-IDs** + Audit | — |
| Neue Vereinsrollen | Als **Systemrollen** ergänzen (z.B. `co_trainer`) | Custom Roles UI |
| `club_custom_roles` | **Nicht** in v1 | v2 Epic |
**Begründung:** Backend und Frontend haben hunderte Verdrahtungen auf `trainer` / `club_admin` / Plattform-Rollen. Parallelbetrieb Capability-System + Legacy-Helfer ist sicherer als einmaliges Aufbrechen.
**Co-Trainer (geplant als Systemrolle):** weniger Capabilities als `trainer` (z.B. kein `planning.*`, kein `exercises.create`) — Umsetzung nach Onboarding-Gates + Entitlements-Rollout, nicht vorher.
---
### 1.3 Vereins-Kontingente (Membership-Pakete)
**Jetzt:** Schema und Anzeige vorbereiten; **keine** detaillierte Paket-Logik (z.B. „3 Trainer + 10 Co-Trainer“) implementieren.
| Vorbereitet (DB/Module) | Bewusst zurückgestellt |
|-------------------------|-------------------------|
| `features`, `club_plans`, `club_subscriptions` | Eigene Feature-IDs `trainer_seats` / `co_trainer_seats` |
| Bestands-Limits (`exercises`, `training_groups`, `ai_calls`, …) | Zählregel „nur planungsberechtigte Mitglieder“ vs. alle Mitglieder |
| `GET /me/entitlements` Feature-Teil | Stripe / Rechnung (M8) |
**Prinzip:** Neue Kontingent-Typen = neue `features`-Zeile + Plan-Limits + optional Capability-`linked_feature_id` — ohne Schema-Bruch.
---
### 1.4 Trainer-Budget innerhalb Vereins-Kontingent (v2)
**Anforderung:** Vereins-KI-Kontingent liegt beim Verein; **Vereinsadmin** kann pro Trainer ein **Sub-Budget** vergeben (Fairness, „Kontingent-Fresser“).
**Entscheidung:**
- v1: nur **Vereins-Ebene** (`club_plan_limits`, `club_feature_usage`).
- v2: neue Tabellen (Skizze):
```sql
-- Skizze — noch nicht migriert
club_member_feature_budgets (club_id, profile_id, feature_id, limit_value, …)
club_member_feature_usage (club_id, profile_id, feature_id, usage_count, reset_at, …)
```
**Prüf-Kette v2:** Capability → Mitglieds-Budget (falls gesetzt, `profile_id` aus Session) → Vereins-Kontingent.
**Fairness-Modell (offen, Tendenz):** harte Sub-Budgets (Modell A) — Trainer darf sein Budget nicht überschreiten, auch wenn Verein noch Rest hat.
**Roadmap:** Phase 5b / Meilenstein **M9** in `docs/working/RBAC_ENFORCEMENT_ROADMAP.md` — Vereinsadmin-UI zur Verteilung, Entitlements mit persönlichem + Vereins-Rest, Auswertung je Person.
---
### 1.5 Enforcement-Phasen (unverändert, bestätigt)
| Phase | Verhalten | Nutzer sichtbar |
|-------|-----------|-----------------|
| 2 (M2/M3) | JSON-Log, kein Block | Nein (außer Logs) |
| 3 (M4) | `GET /me/entitlements` + Badge | Kontingent-Anzeige |
| 4 (M5+) | HTTP 403 + `increment` | Hard-Block |
Env-Schalter: `ACCOUNT_GATE_ENFORCE` (Default `1`, Endpoint-Helfer), `ACCOUNT_GATE_API_ENFORCE` (Default `1`, API-Middleware Phase A), `CAPABILITY_ENFORCE` / `CLUB_FEATURE_ENFORCE` (Default `0`).
---
## 2. Implementierungsstand (Ist, Codebase)
**DB-Schema:** `20260606083` · App **0.8.199** (`backend/version.py`)
**Roadmap (detailliert):** `docs/working/RBAC_ENFORCEMENT_ROADMAP.md`
### M1 — Feature-Schema v9c ✅
| Deliverable | Status |
|-------------|--------|
| Migration `078_club_features_and_plans.sql` | ✅ |
| Legacy `001` archiviert | ✅ |
| `club_plans`, `club_subscriptions`, Usage-Tabellen | ✅ |
| Seed Features + Pläne (`free`, …) | ✅ |
| `club_features.py`: `check_club_feature_access`, `get_effective_club_plan` | ✅ |
| Backfill Vereine → Plan `free` | ✅ |
### M2 — Feature-Probe (Log only) ✅
| Deliverable | Status |
|-------------|--------|
| `club_feature_logger.py``club-feature-usage.log` | ✅ |
| `probe_club_feature_access()` | ✅ |
| Hooks: KI-Endpoints, `POST /exercises`, Medien-Upload, Planungs-KI | ✅ |
| Consume-Standard + `feature_usage` in Response (`ai_calls`) | ✅ |
| `CLUB_FEATURE_ENFORCE=0` (Default) | ✅ |
### M3 — Account-Lifecycle + Capability-Grants ⚠️ teilweise
| Deliverable | Status | Lücke |
|-------------|--------|-------|
| Migration `079_capabilities.sql` + Seed | ✅ | — |
| `account_lifecycle.py`, `resolve_account_state` | ✅ | — |
| `capabilities.py`, `check_capability`, `probe_capability` | ✅ | — |
| `TenantContext.account_state` | ✅ | — |
| `GET /profiles/me``account_state`, `club_roles` | ✅ | — |
| Account-Gates auf **Schreib-/KI-Endpoints** | ✅ | Lesepfade für Bewerber noch offen |
| `CAPABILITY_ENFORCE=0` (nur Log) | ✅ | — |
| Onboarding UX: nur Bewerbung/Gründung | ✅ | Phase A: API-Middleware + `/onboarding` + reduzierte Nav |
| `club_creation_requests` (M7) | ✅ Basis | Capabilities + Admin-Freigabe |
| Quota-Bypass via Capability-Grants (083) | ✅ | kein paralleles Exemption-Schema |
| Custom Roles / Co-Trainer | ❌ | bewusst v2 |
| Legacy-Helfer entfernt | ❌ | bewusst parallel |
### M4 — Anzeige ✅ teilweise
| Deliverable | Status |
|-------------|--------|
| `GET /api/me/entitlements` | ✅ |
| `EntitlementsContext`, `hasCapability()` | ✅ (UI nutzt noch kaum) |
| `FeatureUsageBadge` | ✅ nur KI im Übungsformular |
| `featureUsageSync` in `request()` | ✅ |
### M5 — Hard-Block + vollständiger Verbrauch ⚠️
| Deliverable | Status |
|-------------|--------|
| `consume_club_feature_with_usage` Standard | ✅ `ai_calls` |
| `CLUB_FEATURE_ENFORCE=1` produktiv | ❌ Default 0 |
| Consume `exercises`, `exercise_media`, … | ❌ |
### M6 — Admin UI Rollen & Rechte ⚠️
| Deliverable | Status |
|-------------|--------|
| `/admin/rights` Capability-Matrix (Portal + Verein) | ✅ |
| Klartext zuerst, Enforcement-Badge | ✅ 2026-06-07 |
| Kontingent-Bypass + Vereinspläne (Seed) | ✅ |
| Neue Pläne / Rollen anlegen (CRUD) | ❌ |
### Bewusst zurückgestellt
| ID | Inhalt |
|----|--------|
| M0 | CI-Isolation / Test-DB |
| M8 | Stripe |
| v2 | Trainer-Budgets, Custom Roles |
---
## 3. Architektur-Zielbild (kompakt)
```
Request
→ require_auth
→ account_state (Gate)
→ TenantContext
→ assert_capability (Rolle / Funktion)
→ check_club_feature_access (Vereins-Kontingent)
→ [v2] member_feature_budget (Trainer-Budget)
→ Governance (Objekt)
```
**Drei Achsen:** Account-Lifecycle · Capabilities · Features (Kontingente). Governance bleibt vierte Prüfung.
---
## 4. Empfohlene Roadmap (nach Entscheidungen)
| Phase | Paket | Warum zuerst |
|-------|--------|--------------|
| **A** | **Onboarding-Gates vollständig** | ✅ umgesetzt (API + Frontend `/onboarding`) |
| **B** | **M7 Vereinsgründung beantragen** | **Als Nächstes** — zweiter Pfad für `verified_pending_club` |
| **C** | **M5 Hard-Block `ai_calls`** | Free-Plan `0` wird real; Badge (M4) liefert Erklärung |
| **D** | **M6 voll** | Pläne-CRUD, Rollen-CRUD | ⚠️ Matrix da |
| **E** | Entitlements im Frontend (`hasCapability`) | Entscheidung 1.2 risikoarm |
| **F** | **M9 Kontingent-Verteilung** — Vereinsadmin vergibt Sub-Budgets pro Person (`profile_id`); Prüfung + Consume personenbezogen; UI Vereinsorga | Entscheidung 1.4, Roadmap Phase 5b |
| **G** | `co_trainer` + Custom Roles (v2) | Entscheidung 1.2 |
M0 parallel, nicht blockierend.
---
## 5. Offene Punkte (vor M6 / v2)
1. Fairness Modell A/B/C für Trainer-Budget (Tendenz: A).
2. Ob `admin` (Portal) langfristig neben `superadmin` bleibt.
3. Ob offizielle Inhalte für Bewerber **nie** lesbar bleiben (aktuell: ja).
---
## 6. Referenzen
| Pfad | Inhalt |
|------|--------|
| `CAPABILITY_CATALOG.v1.md` | Capability-IDs, Account-States |
| `CLUB_MEMBERSHIP_AND_FEATURES.v1.md` | Feature-Registry, Kontingente |
| `backend/club_features.py` | Vereins-Features |
| `backend/capabilities.py` | Capability-Auflösung |
| `backend/account_lifecycle.py` | Account-Gates |
## 7. Superadmin im Verein (FAQ)
Siehe **`docs/working/RBAC_ENFORCEMENT_ROADMAP.md` §4**: Plattform-Admin (`admin`, `superadmin`) erhält **Capability-Bypass** für Vereins-Funktionen ohne `club_admin`-Mitgliedschaft. Mandant über aktiven Verein wählen; Kontingente via Bypass. Einzelne Legacy-Pfade (z.B. Löschen `visibility=club`) sind noch nicht vereinheitlicht — Ziel Phase 3.
---
**Changelog**
- 2026-06-06: Initial — Entscheidungen Onboarding, Rollen-Risiko, Kontingente, Trainer-Budget v2; Ist-Stand M1M3; Roadmap AF.
- 2026-06-06: Phase A — `account_onboarding_gate.py`, Frontend `/onboarding`, reduzierte Navigation.
- 2026-06-07: M4M6 Ist-Stand, Roadmap-Verweis, Superadmin-FAQ; Admin-Matrix UX + Enforcement-Audit.
- 2026-06-08: Roadmap Phase 5b / M9 — Vereinsadmin-Kontingentverteilung pro Person; Enforce Dev verifiziert (0.8.202).

View File

@ -227,7 +227,9 @@ Ziel: **vereinszentrierte** Vertrags- und Limitlogik, analog zur bestehenden Tie
## 8. Verwandtes Dokument ## 8. Verwandtes Dokument
- **`ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`** verbindliche Umsetzungsstufen AF, einheitliche Zugriffsschicht, Scope-Erweiterung (`division`, später Community), Capability-Vorbereitung ohne Custom-Rollen-UI; Vereinsabo explizit zurückgestellt. - **`ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`** verbindliche Umsetzungsstufen AF, einheitliche Zugriffsschicht, Scope-Erweiterung (`division`, später Community), Capability-Vorbereitung ohne Custom-Rollen-UI; Vereinsabo explizit zurückgestellt.
- **`CAPABILITY_CATALOG.v1.md`** Rollen, Capability-IDs, Account-Lifecycle, Endpoint-Mapping.
- **`CLUB_MEMBERSHIP_AND_FEATURES.v1.md`** Vereinsabo, Feature-Registry (Mitai-v9c-Pattern), Kontingente.
--- ---
**Letzte Aktualisierung:** 2026-05-05 **Letzte Aktualisierung:** 2026-06-06

View File

@ -0,0 +1,144 @@
# Navigation — Return-Kontext (Rücksprung)
**Stand:** 2026-05-20
**Status:** Spezifikation + Phase 12 umgesetzt
**Ziel:** In der PWA (ohne Browser-Back) zuverlässig an den fachlichen Ausgangspunkt zurückkehren — inkl. sinnvollem Label und optional UI-State.
---
## Problem
Viele Flows navigieren von Kontext A zu Editor/Detail B (z.B. Übungsliste → Modulbearbeitung). Die Zielseite kennt A nicht und bietet nur einen **fest verdrahteten** Zurück-Link (z.B. immer „Modul-Bibliothek“). In der installierten PWA fehlt zusätzlich die Browser-Chrome.
Betroffen u.a.:
- Übungsliste → Modul anlegen/bearbeiten
- Planung → Einheiten-Editor (teilweise gelöst via `planningReturn`)
- Modals mit Speichern + Redirect auf Vollseite
---
## Strategie (Hybrid)
| Mechanismus | Wann |
|-------------|------|
| **Expliziter Return-Kontext** (`appReturn` in Router-State) | Seitenwechsel, bei denen das Ziel einen fachlichen Rücksprung anbieten soll |
| **History-Back** (`navigate(-1)`) | Fallback, wenn kein Kontext gesetzt ist und History-Eintrag existiert |
| **Default-Pfad** | Fallback der Zielseite (z.B. Modul-Bibliothek) |
| **Modal schließen** | Overlays/Peek — kein Routing-Return |
**Nicht** als alleinige Lösung: reines Browser-Back (History durch `replace`, Deep Links, Reload unzuverlässig).
---
## Datenmodell
Router-State-Schlüssel: **`appReturn`**
```javascript
{
v: 1, // Schema-Version
path: '/exercises', // Ziel-URL (inkl. Query, falls nötig)
label: 'Zurück zur Übungsliste', // Anzeige im UI (vollständiger Satz)
kind: 'exerciseList', // optional: Typ für erweiterte Wiederherstellung
payload: { ... } // optional: kind-spezifische Daten
}
```
### `kind`-Werte (erweiterbar)
| kind | payload | path-Ableitung |
|------|---------|----------------|
| `exerciseList` | — | `/exercises` (Filter/Auswahl via sessionStorage) |
| `planningHub` | `buildPlanningHubReturnState(...)` | `planningHubPathFromReturnState(payload)` |
| `trainingModulesList` | — | `/planning/training-modules` |
| `planTemplatesList` | — | `/planning/plan-templates` |
| `frameworkProgramsList` | — | `/planning/framework-programs` |
| `settings` | — | `/settings` |
| `dashboard` | — | `/` |
| `mediaLibrary` | — | `/media` |
| `trainingRun` | `{ unitId }` | `/planning/run/:unitId` |
| `currentLocation` | — | aktuelle Route (z.B. Einheiten-Editor) |
| (frei) | — | `path` direkt gesetzt |
### Legacy-Kompatibilität
Bestehendes Feld **`planningReturn`** (Planung ↔ Einheiten-Editor) wird beim Lesen in `appReturn` **bridged** — keine Big-Bang-Migration nötig.
---
## API (Frontend)
Zentrale Datei: `frontend/src/utils/navReturnContext.js`
| Funktion | Zweck |
|----------|--------|
| `buildNavReturnContext({ path, label, kind?, payload? })` | Kontext-Objekt erzeugen |
| `buildExercisesListReturnContext()` | Standard-Rückkehr Übungsliste |
| `buildPlanningHubReturnContext(hubState)` | Planungs-Hub inkl. Filter-Query |
| `buildTrainingModulesListReturnContext()` | Modul-Bibliothek |
| `readNavReturnFromLocation(location)` | Kontext aus `location.state` (+ Legacy) |
| `resolveNavReturnTarget(location, fallback)` | `{ path, label }` für UI |
| `goNavReturn(navigate, location, fallback?)` | Programmatischer Rücksprung (priorisiert: Kontext → History → Fallback) |
| `navigateWithAppReturn(navigate, to, returnContext, options?)` | Navigation mit gesetztem `appReturn` |
| `preserveAppReturnOnNavigate(navigate, location, to, options?)` | Weiterleiten, bestehenden Kontext behalten (z.B. nach `replace`) |
UI-Komponente: **`PageReturnButton`** — app-typischer Zurück-Schalter (Button mit Pfeil, kein Router-Link).
Links **zum** Ziel: **`NavStateLink`** mit `returnContext` der Quellseite.
### Editor-Aktionen
Auf Vollseiten-Editoren mit **`FormActionBar`** (`placement="bottom"`) oder **`PageFormEditorChrome`**:
- **Kein** separater Zurück-Link/Button oben (wirkt in der App redundant)
- **Abbrechen**`goBack()` / `goNavReturn(...)` (Einsprungspunkt)
- **Speichern & Schließen** → nach erfolgreichem Save ebenfalls `goBack()`
- Sticky Action Bar unten nutzen
**PageReturnButton** nur auf **Leseseiten** ohne Editor-Leiste (z.B. Übungsdetail, Einstellungen-Unterseiten, Trainingsablauf).
---
## Regeln für Entwickler
1. **Jede Navigation** von Kontext A zu Editor B, wo der Nutzer „weitermachen“ soll, setzt `appReturn` (oder nutzt `navigateWithAppReturn`).
2. **Zielseite** zeigt `PageReturnButton` mit sinnvollem **Default-Fallback** (Bibliothek/Hub).
3. **Nach Create + `replace: true`:** Return-Kontext mit `preserveAppReturnOnNavigate` erhalten.
4. **Modals:** Schließen reicht; Redirect nach Speichern = Seiten-Navigation → Return setzen.
5. **Kein Return-Kontext** in `location.state` für interne Bibliothek → Detail → Bearbeiten, wenn Herkunft = offensichtliche Elternliste (Default-Fallback genügt).
6. **UI-State** (Filter, Auswahl): weiter über bestehende Session-Mechanismen (z.B. `exerciseListSessionState`), nicht im Return-Payload duplizieren, außer kind erfordert Query-Reconstruction (Planung).
---
## Umsetzungsstand
### Phase 1 (Pilot)
- [x] Spec + Utility + Tests
- [x] `PageReturnButton` (ersetzt Link-Variante)
- [x] Übungsliste → Modul speichern → Modul-Editor
- [x] Planung: `SaveExercisesAsModuleModal` leitet Return-Kontext weiter
- [x] `TrainingUnitEditPage`: `goBack` über `goNavReturn` (Legacy-bridge)
### Phase 2 (Flows verbinden)
- [x] Listen → Editoren: Übungen, Module, Vorlagen, Rahmenprogramme
- [x] Dashboard → Übung bearbeiten / Trainingsablauf / Einheit bearbeiten
- [x] Einstellungen-Unterseiten (Rechtliches, Systeminfo)
- [x] Trainingsablauf + Coach-Modus (`trainingRun`, Planungs-Fallback)
- [x] Medienbibliothek → verknüpfte Übungen/Einheiten
- [x] `ExercisePeekModal` → Vollseite mit Return
- [x] Editoren: Abbrechen + Speichern & Schließen → Einsprungspunkt
### Optional (später)
- Globaler Zurück-Button in App-Chrome (Mobile)
- Nach Speichern: explizite Aktion „Zurück zum Ausgang“ im Toast
---
## Referenzen
- Bestehend: `frontend/src/utils/planningUnitRoutes.js` (`planningReturn`)
- Session Übungsliste: `frontend/src/utils/exerciseListSessionState.js`
- PWA-Kontext: `docs/FACHLICHE_NUTZERFUNKTIONEN.md`, App-Shell in `App.jsx`

View File

@ -1,23 +1,26 @@
# Parallele Trainingsstreams — Technische Spezifikation (Umsetzung) # Parallele Trainingsstreams — Technische Spezifikation (Umsetzung)
**Status:** Entwurf · **Stand:** 2026-05-14 **Status:** Umsetzung **Phase 1 (teils)** · **Stand:** 2026-05-14
**Fachgrundlage:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md` **Fachgrundlage:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`
Dieses Dokument beschreibt die **Umsetzung** auf Basis der **aktuellen Codebasis** (Stand Analyse 2026-05-14): eine `training_unit` mit **`training_unit_sections`** und **`training_unit_section_items`** (Übung/Notiz, optional `planning_method_profile` für Kombinationsübungen, Migration **057**); Rahmen-**Blueprint**-Einheiten mit `framework_slot_id` (**037**); Leitung **`lead_trainer_profile_id`** (**038**); Co-Trainer **`assistant_trainer_profile_ids`** JSONB (**042**); Durchführung **`TrainingUnitRunPage`** (sequentiell über Sektionen). Dieses Dokument beschreibt die **Umsetzung** auf Basis der **aktuellen Codebasis** (Stand 2026-05-14): **`training_unit_phases` / `training_unit_parallel_streams`** (Migration **063**) und **`training_unit_sections`** mit Phasen-/Stream-Bezug; **`training_unit_section_items`** (Übung/Notiz, optional `planning_method_profile` für Kombinationsübungen, Migration **057**); Rahmen-**Blueprint**-Einheiten mit `framework_slot_id` (**037**); Leitung **`lead_trainer_profile_id`** (**038**); Co-Trainer **`assistant_trainer_profile_ids`** JSONB (**042**); Durchführung und Coaching über **`TrainingUnitRunPage`**, **`TrainingCoachPage`** und **`trainingPlanUtils.js`**.
--- ---
## 1. Ist-Stand (relevant) ## 1. Ist-Stand (Code, 2026-05-14)
| Bereich | Aktuell | | Bereich | Aktuell |
|---------|---------| |---------|---------|
| Planstruktur | **Eine** lineare Liste `training_unit_sections` je `training_unit_id`; Items in `training_unit_section_items`. | | **Schema** | Migration **063:** `training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` **oder** `parallel_stream_id`. |
| Rahmenprogramm | `training_framework_slots` verweisen auf **Blueprint**-`training_units` — Slots = **Serien-Spalten**, nicht simultane Breakouts in **einer** Halle. | | **API** | `GET /api/training-units/:id`**`phases`** (verschachtelt) + flache **`sections`**. `PUT/POST` mit **`phases`** für Breakout-Einheiten (**0.8.138**); höchstens eines von `phases`, `sections`, `exercises` pro Request (Planning-Router). Legacy-PUT mit nur `sections` erzeugt/ergänzt Ganzgruppen-Phase. |
| Kombinationsübung | Ein **Item** kann Kombi sein; `planning_method_profile` = Snapshot; Coaching-UI teilweise (`CombinationPlanBracket` in Run/Peek). | | **Planung (UI)** | Breakout-Panel: Ganzgruppen-/parallele Phasen, Streams; Speichern phasenbasiert (`trainingUnitSectionsForm.js`, `TrainingPlanningPage`). |
| Trainer-Zuweisung | `lead_trainer_profile_id`, `assistant_trainer_profile_ids` am **`training_units`**-Kopf; **keine** Zuordnung zu „welcher parallelen Spur“. | | **Durchführung** | `TrainingUnitRunPage.jsx` + `trainingPlanUtils.js` (`sectionsWithPlanLocForDisplay`, `buildPlanRunViewModelFromSections`) — Phasenfolge in „Plan & Ablauf“. |
| Run-Modus | `TrainingUnitRunPage`: sortierte Sektionen/Items, Checkliste, Fortschritt in `sessionStorage` pro Einheit. | | **Coaching** | `TrainingCoachPage.jsx` + `flattenPlanTimeline`, Stream-Picks, Rejoin vor Ganzgruppe/nächstem Split (`coachShouldPromptSplitRejoinTransition`), Nachbereitung mit `buildCoachSavePlanPayload`, danach Navigation zu `/planning/run/:id`. |
| **Kombinationsübung** | Unverändert je Item; `planning_method_profile`, Coach-Kombi-Stufe A. |
| **Trainer-Zuweisung** | `lead_trainer_profile_id`, `assistant_trainer_profile_ids` am Einheitskopf; **Stream-**`assigned_trainer_profile_ids` im Schema — UI/Policy noch nicht vollständig (siehe **§8 offen**). |
| **Rahmenprogramm** | Blueprint-`training_units` können dieselbe Phasenstruktur tragen; Kopie aus Slot (`from-framework-slot`, **0.8.138**). |
**Konsequenz:** Parallele Streams erfordern ein **erweitertes konzeptionelles „Gefäß“** unterhalb der Einheit (Phasen und/oder Streams) und eine **Verknüpfung** bestehender Sektionen mit diesem Gefäß — oder eine **Migration** zu einem neuen Pflicht-Container (siehe §3). **Hinweis:** Die frühere Planungsvariante „nur lineare `training_unit_sections` ohne Phasen“ gilt weiter für Alt-Daten; Migration **063** ordnet Bestand einer Default-Ganzgruppenphase zu.
--- ---
@ -37,9 +40,11 @@ training_unit (Kalender-Einheit)
## 3. Datenmodell — Optionen ## 3. Datenmodell — Optionen
**Ist (063):** Die unten skizzierte **empfohlene** Normalform ist unter den genannten Tabellennamen produktiv; die Abschnitte 3.1/3.2 bleiben zur Einordnung erhalten.
### 3.1 Empfohlen: explizite Phasen + Streams (normalisiert) ### 3.1 Empfohlen: explizite Phasen + Streams (normalisiert)
Neue Tabellen (Namen bei Implementierung final festlegen): Die Tabellen sind **umgesetzt** (Namen final):
| Tabelle | Zweck | | Tabelle | Zweck |
|---------|--------| |---------|--------|
@ -109,15 +114,15 @@ Nur **`training_unit_parallel_streams`** + `parallel_stream_id` auf Sektionen; P
--- ---
## 8. Implementierungsphasen (Vorschlag) ## 8. Implementierungsphasen (Abgleich)
| Phase | Inhalt | | Phase | Inhalt | Stand 2026-05-14 |
|-------|--------| |-------|--------|------------------|
| **P1** | Schema Phasen + Streams; Migration; GET/PATCH Einheit verschachtelt; Planungs-UI; Run-UI mit Stream-Tabs | | **P1** | Schema Phasen + Streams; Migration **063**; GET/PUT verschachtelt; Planungs-UI; Run + Coach phasenbasiert | **Teilweise erledigt** — Run-UI nutzt Phasen-Timeline in der Anzeige; **Stream-Tabs** optional noch zu vereinheitlichen (§5.2) |
| **P2** | Trainer-Zuordnung pro Stream + effektive Anzeige; Vorlagen erweitert | | **P2** | Trainer-Zuordnung pro Stream + effektive Anzeige; Vorlagen erweitert | **Offen** |
| **P3** | Synchroner Hallen-Takt / Rotationsmatrix (falls fachlich freigegeben) | | **P3** | Synchroner Hallen-Takt / Rotationsmatrix (falls fachlich freigegeben) | **Offen** |
--- **Offene Punkte (kurz):** siehe **`docs/HANDOVER.md`** Tabelle „Coaching & Breakout“.
## 9. Verwandte Dokumente ## 9. Verwandte Dokumente
@ -127,4 +132,5 @@ Nur **`training_unit_parallel_streams`** + `parallel_stream_id` auf Sektionen; P
| `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slot vs. Parallelität | | `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slot vs. Parallelität |
| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombi, `planning_method_profile` | | `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombi, `planning_method_profile` |
| `technical/DATABASE_SCHEMA.md`, `backend/migrations/` | DDL-Historie | | `technical/DATABASE_SCHEMA.md`, `backend/migrations/` | DDL-Historie |
| `frontend/src/pages/TrainingPlanningPage.jsx`, `TrainingUnitRunPage.jsx`, `TrainingFrameworkProgramEditPage.jsx` | Ist-UI | | `TrainingPlanningPage.jsx`, `TrainingUnitRunPage.jsx`, `TrainingFrameworkProgramEditPage.jsx` | Planung, Durchführung, Rahmen |
| `frontend/src/utils/trainingPlanUtils.js`, `TrainingCoachPage.jsx` | Phasen-Timeline, Rejoin, Coach-Speichern |

View File

@ -0,0 +1,184 @@
# Gewichtetes Fähigkeiten-Scoring (Phase 3)
**Stand:** 2026-05-20
**Status:** Variante A (regelbasiert) umgesetzt — **v1.3** (Peer-Kontext getrennt + Listen-Filter)
**Modul:** `backend/skill_scoring.py`, Router `backend/routers/skill_profiles.py`
## Ziel
Trainer wählen **Schwerpunkt-Fähigkeiten** und finden passende **Bausteine** für die Trainingsplanung:
- **Trainingsmodule** — wiederverwendbare Übungsfolgen
- **Rahmenprogramme** — Programme mit Zielen und Session-Slots
- **Regressionspfade** (Progressionsgraphen) — Übungsketten
Das Scoring beantwortet: *Wie stark trainiert dieser Baustein eine Fähigkeit?* und *Wie stark ist er im Vergleich zu anderen **sichtbaren** Bausteinen **desselben Typs**?*
## Fachliche Kernregel: Peer-Kontext (nicht vermischen)
| Planungs-Artefakt | Vergleichsgruppe (`universal_percent`) |
|-------------------|----------------------------------------|
| Trainingsmodul | nur andere **sichtbare Module** |
| Rahmenprogramm | nur andere **sichtbare Rahmenprogramme** |
| Regressionspfad | nur andere **sichtbare Pfade** |
**Nicht** verglichen werden:
- Module vs. Rahmenprogramme vs. Pfade (kein Mix)
- Artefakte anderer Vereine, auf die der Nutzer keinen Planungszugriff hat
**Sichtbarkeit:** `library_content_visibility_sql` — private, vereinsinterne und offizielle Inhalte gemäß Mandant/Rolle, analog zu anderen Bibliothekslisten.
## Datenquellen
| Artefakt | Übungen aus |
|----------|-------------|
| Rahmenprogramm (gesamt) | Alle Blueprint-`training_units` der Slots → `training_unit_section_items` |
| Rahmenprogramm (pro Slot) | Blueprint einer Session |
| Trainingsmodul | `training_module_items` (nur `item_type = exercise`) |
| Progressionsgraph | `from_exercise_id` + `to_exercise_id` je Kante (Vorkommen zählt) |
Fähigkeiten je Übung: `exercise_skills``skills` (nur `status = active`).
## Gewichtungsformel (v1.1 / v1.2)
Pro **Übungsvorkommen** (eine Zeile im Ablauf / Modul / Kanten-Endpunkt):
1. **Basis-Minuten** = `planned_duration_min` der Position, sonst Default (Einheit/Modul: 8 Min, Graph: 10 Min).
2. Pro verknüpfte Fähigkeit der Übung:
- `Beitrag = Basis-Minuten × Anzahl Vorkommen × Link-Faktor`
- **Link-Faktor** = Intensität × Stufen-Faktor
### Intensität (Nutzeneinschätzung, UI-Feld)
| Wert | Faktor |
|------|--------|
| niedrig | 0,85 |
| mittel / leer | 1,0 |
| hoch | 1,2 |
### Stufen-Spanne (`required_level` → `target_level`, UI „von/bis“)
Kanonische Slugs: basis … optimierung (15). Fehlen beide: Faktor 1,0.
- **Spanne** = Anzahl Stufen von „von“ bis „bis“ (15)
- **Mittelpunkt** = durchschnittliche Stufe
- Faktor ≈ `(0,92 + 0,04 × Spanne) × (0,95 + 0,025 × Mittelpunkt)` → typisch 0,961,20
### Bewusst nicht im Scoring
| Feld | Grund |
|------|--------|
| `is_primary` | Perspektivabhängig; bleibt in Übungs-UI, fließt nicht ins Profil ein |
| `development_contribution` | Legacy-DB-Feld, in UI nicht gepflegt |
## Aggregierte Metriken
| Feld | Bedeutung |
|------|-----------|
| `weight` / `score` | Absolutes **Trainingsgewicht** (gewichtete Minuten) — über alle Fähigkeiten eines Artefakts vergleichbar |
| `share_percent` | Anteil am `total_weight` **innerhalb dieses Artefakts** (summiert 100 %) — sekundär |
| `by_main_category[]` | Je Unterkategorie `top_skill` (stärkste Fähigkeit nach Gewicht) |
| `universal_percent` | Anteil am **Maximum derselben Fähigkeit im Peer-Kontext** (max. 100 %) |
| `is_club_best_for_skill` | Stärkster sichtbarer Peer für diese Fähigkeit (★ in UI) |
| `club_best` | Referenz-Peer (Titel, Typ, Gewicht) — **Legacy-Name**, fachlich Peer-Best |
### Berechnung `universal_percent`
```
effective_ref = max(max_weight_in_peer_corpus(skill_id), eigenes_gewicht)
universal_percent = min(100, weight / effective_ref × 100)
```
Corpus je Typ: `compute_planning_corpus_by_type()` scannt sichtbare Artefakte getrennt nach `framework_program`, `training_module`, `progression_graph`.
Discovery-Sortierung nutzt **`match_score`** (= Summe absoluter Gewichte der gewählten Fähigkeiten), nicht den Plan-internen Anteil. Discovery verwendet typ-getrennte Referenz (`fw_ref`, `mod_ref`, `graph_ref`).
## API
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| GET | `/api/training-framework-programs/{id}/skill-profile` | `overall` + `slots[]` mit je `profile`; `reference_scale` (Peer-Kontext Rahmenprogramme) |
| GET | `/api/training-modules/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Module) |
| GET | `/api/exercise-progression-graphs/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Pfade) |
| POST | `/api/skill-profiles/batch-summaries` | Kompakte Profile für Listen; Body: `frameworkProgramIds`, `trainingModuleIds`, …; Response: `summaries`, `reference_scale_by_type`, `club_best_by_skill` |
| GET | `/api/skill-discovery/suggestions?skill_ids=1,2,3` | Ranking sichtbarer Artefakte; Query `types`, `limit` |
Zugriff: `get_tenant_context` + `library_content_visibility_sql` wie Parent-Artefakt.
### `reference_scale` / `reference_scale_by_type`
```json
{
"scope": "planning_peer",
"artifact_type": "training_module",
"artifacts_scanned": 12,
"skills_in_corpus": 34,
"description": "Prozent = Anteil am stärksten sichtbaren Eintrag unter Trainingsmodulen …"
}
```
## UI
### Bearbeitung (Vollprofil)
| Ort | Panel | `artifactType` |
|-----|-------|----------------|
| Rahmenprogramm bearbeiten | Fähigkeiten-Schwerpunkte (+ Sessions) | `framework_program` |
| Trainingsmodul bearbeiten | Fähigkeiten im Modul | `training_module` |
| Progressionsgraph | Fähigkeiten entlang des Pfads | `progression_graph` |
Anzeige: Top je Kategorie (Editor) oder alle Fähigkeiten (Modal). Hinweise nennen Peer-Kontext explizit (z. B. „72 % Rahmenpr.“).
### Listen & Filter (UX wie Übungsliste)
| Liste | Filter |
|-------|--------|
| Rahmenprogramme (`/planning/framework-programs`) | Suche, Katalog (Fokus/Trainingsart/Zielgruppe), Session-Dauer, **Fähigkeiten** (`SkillTreeMultiSelect`), Mindest-% im Peer-Kontext, Sortierung nach Stärke |
| Trainingsmodule (`/planning/training-modules`) | Suche, **Fähigkeiten** (+ Min-%, Sortierung) |
- **Filter-Button** mit Badge, entfernbare **Chips**, Einstellungen im **Modal** (`PlanningArtifactFilterModal`)
- KPI-Kacheln: Top-Fähigkeit **je Unterkategorie** mit Score + Peer-%
- Vollprofil-Modal: `SkillProfileFullModal` mit `displayMode=full`
Profil wird nach Speichern neu geladen (`skillProfileTick` in Editoren).
### Discovery
**Fähigkeiten-Seite → Planungs-Vorschläge:** Multi-Select + API `/api/skill-discovery/suggestions` (optional Filter `types`).
## Frontend-Module (Auswahl)
| Pfad | Rolle |
|------|--------|
| `frontend/src/components/planning/PlanningArtifactFilterModal.jsx` | Filter-Modal |
| `frontend/src/components/planning/PlanningSkillFilterSection.jsx` | Fähigkeiten-Block im Modal |
| `frontend/src/utils/planningArtifactFilterChips.js` | Chip-Labels + Entfernen |
| `frontend/src/utils/frameworkProgramListHelpers.js` | Client-Filter Rahmenprogramme |
| `frontend/src/utils/trainingModuleListHelpers.js` | Client-Filter Module |
| `frontend/src/components/skills/SkillProfileCompact.jsx` | KPI-Kacheln in Listen |
| `frontend/src/components/SkillTreeMultiSelect.jsx` | Baumauswahl (Portal-Dropdown in Modals) |
## Grenzen / später
- Kein DB-Cache (`skill_profile_json`) — on-the-fly; bei >50 Artefakten pro Typ serverseitiger Index/Caching
- Entwicklungsziele am Rahmenkopf bleiben Freitext (kein Scoring)
- KI-Zusammenfassung (Variante B) nicht Teil von v1.0
- Trainings**einheiten** (Kalender) optional als nächste Erweiterung
- Filter-Persistenz („Als Standard speichern“) wie bei Übungen — noch nicht für Planungslisten
- Fähigkeiten-Filter im Dialog **Planung → Rahmen übernehmen** — Katalog ja, Skill-Filter optional nachziehen
- API-Feldnamen `club_*` / `skillMinClubPercent` — technische Altlast, semantisch Peer-Kontext
## Tests
- `backend/tests/test_skill_scoring.py` — Multiplikator, Aggregation, Match-Score, Cap 100 %
- **Offen:** dedizierte Tests für `compute_planning_corpus_by_type` (Typ-Trennung)
## Verweise
| Dokument | Inhalt |
|----------|--------|
| `functional/DOMAIN_MODEL.md` | Domänenabschnitt Planungs-Fähigkeiten-Profil |
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick Listen/Filter |
| `docs/HANDOVER.md` | Handover-Abschnitt Phase 3 |
| `technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Sichtbarkeit / Mandant |

View File

@ -16,6 +16,7 @@
| `functional/DOMAIN_MODEL.md` | Fachliche Begriffe; Kurzverweis auf Progressionsgraph ergänzt. | | `functional/DOMAIN_MODEL.md` | Fachliche Begriffe; Kurzverweis auf Progressionsgraph ergänzt. |
| `TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` | **Was** und **warum** (Bibliothek vs. Instanz, Governance, CURRTabelle). | | `TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` | **Was** und **warum** (Bibliothek vs. Instanz, Governance, CURRTabelle). |
| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | **Parallele Streams / Breakout innerhalb einer Einheit** — orthogonale Domäne zu **RahmenSlots** (SerienSessions). | | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | **Parallele Streams / Breakout innerhalb einer Einheit** — orthogonale Domäne zu **RahmenSlots** (SerienSessions). |
| `technical/SKILL_SCORING_SPEC.md` | **Fähigkeiten-Profil** der RahmenSlots / Module / Pfade; Listen-Filter und PeerVergleich (nur gleicher Artefakttyp). |
**Konsequenz:** Diese Datei bleibt der **technische Arbeitspool** für Rahmenprogramm Stufe 12. Abschnitt **§4** beschreibt explizit den **aktuellen Produktfreigabe-Umfang** und **bekannte Lücken** (damit Trainingsplanung weiter gebaut werden kann ohne falscher Erwartung an „AlternativePakete“ in der UI). **Konsequenz:** Diese Datei bleibt der **technische Arbeitspool** für Rahmenprogramm Stufe 12. Abschnitt **§4** beschreibt explizit den **aktuellen Produktfreigabe-Umfang** und **bekannte Lücken** (damit Trainingsplanung weiter gebaut werden kann ohne falscher Erwartung an „AlternativePakete“ in der UI).

View File

@ -13,6 +13,7 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe AC.
| exercises | `PATCH /api/exercises/bulk-metadata` | ja | `get_tenant_context` | ja | Liste: UI-Mehrfachwahl; bis 500 IDs; nur Ersteller oder Plattform-Admin | | exercises | `PATCH /api/exercises/bulk-metadata` | ja | `get_tenant_context` | ja | Liste: UI-Mehrfachwahl; bis 500 IDs; nur Ersteller oder Plattform-Admin |
| exercises | `GET .../media/{mid}/file` | ja | `get_tenant_context_flexible` | ja (wie Übung lesen) | Datei oder `?ssetoken`; kein anonymes `/media/` ohne ALLOW_PUBLIC_MEDIA_STATIC | | exercises | `GET .../media/{mid}/file` | ja | `get_tenant_context_flexible` | ja (wie Übung lesen) | Datei oder `?ssetoken`; kein anonymes `/media/` ohne ALLOW_PUBLIC_MEDIA_STATIC |
| exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | PUT Einzelübung: bei Sichtbarkeit `official` Medien-§4.2 (422: Lifecycle/Promotion/Copyright) | | exercises | übrige geschützte `/api/exercises*` | ja | `get_tenant_context` | ja | PUT Einzelübung: bei Sichtbarkeit `official` Medien-§4.2 (422: Lifecycle/Promotion/Copyright) |
| exercises | POST `/api/exercises/ai/suggest`, POST `/api/exercises/{id}/ai/regenerate` | ja | `get_tenant_context` | nein | Nur Vorschlags-JSON; keine DB-Schreibung; OpenRouter — suggest optional `focus_areas_context` für Retrieval-Profile |
| exercise_progression_graphs | `/api/exercise-progression-graphs*` | ja | `get_tenant_context` | Liste wie Bibliothek; Schreiben Ersteller/Plattform-Admin | Kanten: Lesen wenn Graph lesbar | | exercise_progression_graphs | `/api/exercise-progression-graphs*` | ja | `get_tenant_context` | Liste wie Bibliothek; Schreiben Ersteller/Plattform-Admin | Kanten: Lesen wenn Graph lesbar |
| training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id | | training_planning | alle geschützten Endpoints | ja | `get_tenant_context` | ja | Vorlagen-Liste wie Übungen; POST Vorlage Default club_id |
| dashboard | `GET /api/dashboard/kpis` | ja | `get_tenant_context` | wie `GET /api/exercises` + `GET /api/training-units` | Aggregat für Dashboard-Kurzüberblick (ein Roundtrip) | | dashboard | `GET /api/dashboard/kpis` | ja | `get_tenant_context` | wie `GET /api/exercises` + `GET /api/training-units` | Aggregat für Dashboard-Kurzüberblick (ein Roundtrip) |
@ -32,18 +33,28 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe AC.
| skills | `/api/skills*` | nein (global) | `require_auth` | je Endpoint | EXEMPT | | skills | `/api/skills*` | nein (global) | `require_auth` | je Endpoint | EXEMPT |
| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin für Schreiben; `GET …/{id}` nur Portal-Admin | EXEMPT | | maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin für Schreiben; `GET …/{id}` nur Portal-Admin | EXEMPT |
| matrix_stack_bundle | Export/Import Bundles | Plattform/Test | `require_auth` | Admin | EXEMPT | | matrix_stack_bundle | Export/Import Bundles | Plattform/Test | `require_auth` | Admin | EXEMPT |
| matrix_editor | `/api/admin/matrix-editor/*` (Export/Import Editor-Bundle) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; globale Fähigkeitsmatrix ohne Mandantenkontext |
| import_wiki / import_wiki_admin | Wiki-Import | Werkzeug | `require_auth`/Admin | Admin | EXEMPT | | import_wiki / import_wiki_admin | Wiki-Import | Werkzeug | `require_auth`/Admin | Admin | EXEMPT |
| ai_skill_retrieval_admin | `/api/admin/ai-skill-retrieval-profiles*` (CRUD) | Plattform | `require_auth` | nur `superadmin`; JSON `config` | EXEMPT wie `admin_users`; kein Vereinsbezug |
| ai_prompts_admin | `/api/admin/ai-prompts*` (Liste, Detail, PUT, Preview, Reset) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; globale `ai_prompts` ohne Mandantenkontext |
| exercise_enrichment_admin | `/api/admin/exercise-enrichment/*` (Kandidaten, Preview, Apply) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; plattformweite Übungsliste + Skill-Schreibung; kein TenantContext |
| admin_user_content | `/api/admin/user-content/*` (Meta, Nutzer-Summary, Items, PATCH, DELETE) | Plattform | `require_auth` | nur `superadmin` | EXEMPT; Moderation nutzerangelegter Inhalte inkl. privat; kein TenantContext |
**Legende:** Router auf der EXEMPT-Liste des Scripts sind globale oder Auth-only-Pfade; sobald ein Router Vereinsdaten oder Bibliotheks-Sichtbarkeit erhält, EXEMPT entfernen und `get_tenant_context` einführen. **Legende:** Router auf der EXEMPT-Liste des Scripts sind globale oder Auth-only-Pfade; sobald ein Router Vereinsdaten oder Bibliotheks-Sichtbarkeit erhält, EXEMPT entfernen und `get_tenant_context` einführen.
**Pflege / Drift:** Änderungen an Mandanten, Governance (`visibility`/`club_id`) oder neuen inhaltsbezogenen Endpoints → eine Zeile in dieser Tabelle anpassen und `PRODUCTION_READINESS_AUDIT_2026-05.md` prüfen. **Pflege / Drift:** Änderungen an Mandanten, Governance (`visibility`/`club_id`) oder neuen inhaltsbezogenen Endpoints → eine Zeile in dieser Tabelle anpassen und `PRODUCTION_READINESS_AUDIT_2026-05.md` prüfen.
Letzte Änderung: 2026-05-13 — `GET /api/dashboard/kpis` (Kurzüberblick-Aggregat). Letzte Änderung: 2026-06-06 — Superadmin `/api/admin/user-content/*` (Nutzer-Inhalte Moderation).
--- ---
### Changelog (Fortführung) ### Changelog (Fortführung)
- **2026-05-23:** Superadmin-API `exercise_enrichment_admin` (Batch-Übungs-Anreicherung KI) dokumentiert.
- **2026-05-30:** Superadmin-API `ai_prompts_admin` (`/api/admin/ai-prompts*`) dokumentiert.
- **2026-05-29:** Superadmin-API `ai_skill_retrieval_admin` (Retrieval-Profile) dokumentiert.
- **2026-05-22:** Übungs-KI-Endpunkte (Suggest/Regenerate) dokumentiert.
- **2026-05-13:** Dashboard-KPI-Endpunkt dokumentiert. - **2026-05-13:** Dashboard-KPI-Endpunkt dokumentiert.
- **2026-05-07:** Legacy `GET/PUT /api/profile` auf Session-Profil gehärtet; OpenAPI/Health-Ready Produktionsdefaults; Security-Release-Tests + CI-Schritt `security_release_checks.py` — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. - **2026-05-07:** Legacy `GET/PUT /api/profile` auf Session-Profil gehärtet; OpenAPI/Health-Ready Produktionsdefaults; Security-Release-Tests + CI-Schritt `security_release_checks.py` — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`.
- **2026-05-07 (Phase 3):** CSP SPA (nginx); API `nosniff`-Middleware — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. - **2026-05-07 (Phase 3):** CSP SPA (nginx); API `nosniff`-Middleware — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`.

View File

@ -0,0 +1,67 @@
# Umsetzungsplan KI bei Übungen (stufenweise, Driftschutz)
**Version:** 0.2
**Datum:** 2026-05-29
**Bezüge:** `functional/AI_EXERCISE_ASSISTANT_VISION.md` · **`working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`** · `technical/KI_FEATURES_SPEC.md` · `technical/AI_PROMPT_SYSTEM_SPEC.md` · `technical/AI_TRAINING_PLANNING_CONCEPT.md` (§1.1 Ist-Stand)
---
## 1. Drift vermeiden verbindliche Regeln
1. **Spec vor Code:** Request/Response-Felder und Statuscodes an `KI_FEATURES_SPEC.md` ausrichten; Abweichungen zuerst Spec oder dieses Dokument anpassen.
2. **Prompts in der DB:** Keine produktionskritischen Prompt-Langtexte nur im Code; Defaults per **Migration** in `ai_prompts`, Anpassung durch Admins über vorgesehene Oberfläche (später) oder SQL.
3. **Skill-Retrieval-Profile:** Gewichte/Quotes in **`ai_skill_retrieval_profiles.config`** — Spezifikation `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`; kein zweites gleichzeitiges Truth-Repo im Sourcecode außer defensiver Fallback `_FALLBACK_RETRIEVAL_CONFIG` in `exercise_ai.py`.
4. **Stufen-Slugs & Intensität:** Nur **kanonische** Werte wie in `exercises.py` (`basis` … `optimierung`, `niedrig|mittel|hoch`); LLM-Ausgaben **normalisieren**, ungültige `skill_id` verwerfen.
5. **Kein stiller DB-Write:** KI liefert **Vorschläge**; Persistenz nur über bestehende **PUT/POST exercises** inkl. Trainer-Aktion (und optional `summary_ai_generated` / `ai_suggested` wie Spec).
6. **Mandant:** Übungsbezogene KI-Endpunkte nutzen `Depends(get_tenant_context)`; keine Ausnahme ohne Eintrag in `ACCESS_LAYER_ENDPOINT_AUDIT.md`.
7. **Schema:** Neue DB-Objekte nur nummerierte Migration **`backend/migrations/`** (aktuell bis **068**) und `DB_SCHEMA_VERSION` anheben.
---
## 2. Stufen (Releases)
| Stufe | Inhalt | Exit-Kriterium |
|-------|--------|------------------|
| **S0** | Dieses Dokument + Verweise konsistent | Review abgehakt |
| **S1** | Migration `ai_prompts` + Defaults `exercise_summary`, `exercise_skill_suggestions`; `exercises.summary_ai_generated` | Migrierte DB, App startet |
| **S2** | `httpx`-Client OpenRouter; Modul lädt Prompt, ersetzt Platzhalter, parst Antwort | Unit-/Smoke: 503 ohne Key |
| **S3** | `POST /api/exercises/ai/suggest`, `POST /api/exercises/{id}/ai/regenerate` | OpenAPI/Handtest mit Key |
| **S4** | Frontend: KI-Vorschlag, **Änderungsdialog** (Vorschau, Kurzfassung wählbar, Fähigkeiten pro Zeile an-/abwählbar), dann Übernahme ins Formular | Manuelle UX-Prüfung |
| **S4b** | **Skill-Retrieval:** Migration **`ai_skill_retrieval_profiles`**, `focus_areas_context` am **`POST …/ai/suggest`**, `exercise_ai` kontextbezogener Katalog (Gewichte, Caps, Keyword-Patches) | Migration 068 angelegt; Smoke mit Gewaltschutz / ohne Fokus |
| **S5** | (später) Auto-Fallback beim Speichern laut `KI_FEATURES_SPEC` §7 | Feature-Flag / Config |
| **S6** | (später) Zielausbau, Anleitung-only, Varianten, Admin-Masse laut Vision | Separate Epics |
**Aktueller Implementierungsstand:** **S4 + S4b** im Code (`exercise_ai` + Formular übermittelt `focus_areas_context`).
---
## 3. Implementierungs-Checkliste (Technik)
- [ ] `OPENROUTER_API_KEY` / `OPENROUTER_MODEL` in `.env.example` dokumentiert (bereits teils vorhanden prüfen).
- [ ] Fehlerbilder: `400` zu wenig Inhalt, `503` KI nicht konfiguriert, `502` Upstream-Fehler mit kurzer Message.
- [ ] Logging: **keine** vollständigen Prompts mit personenbezogenen Daten in Prod-Logs (optional DEBUG).
- [ ] Optional: Rate-Limit KI-Endpunkte (`slowapi`) nach Bedarf.
- [ ] `MODULE_VERSIONS["exercises"]` / Changelog bei API-Erweiterung setzen.
---
## 4. Changelog dieses Plans
- **2026-05-22:** Initial; S1S4 als erster Umsetzungspfad.
- **2026-05-22:** S1S4 im Code umgesetzt (Migration 067, `exercise_ai` + Router, Übungsformular); S5 weiter offen.
- **2026-05-29:** **S4b:** Migration **068**, `ai_skill_retrieval_profiles`; suggest `focus_areas_context`; Frontend sendet gesetzte Fokusbereiche; Spec `working/AI_SKILL_RETRIEVAL_PROFILES_SPEC.md`.
---
## 5. Umsetzungsstand (Zwischencheckpoint)
**Erledigt (2026-05-22):** Migration **`067_ai_prompts_exercise_assistant`**, **`openrouter_chat`**, **`exercise_ai`**, **`POST /api/exercises/ai/suggest`** und **`POST /api/exercises/{id}/ai/regenerate`**, Formular-Schaltflächen (Kurzfassung / Fähigkeiten / kombiniert).
**Erledigt (2026-05-29):** Migration **`068`** / Profil **`ai_skill_retrieval_profiles`** (Standard + Profil Gewaltschutz wenn `focus_areas.name` vorhanden); **`exercise_ai`** — Score/Kategorie-Zapfen/Text-Overlap/Keyword-Zuschläge; **API:** `ExerciseAiSuggestBody.focus_areas_context`; **Regenerate** nutzt DB-Fokuszeilen.
**Nacharbeit S4 UX:** Übernahmedialog **`ExerciseFormPageRoot`**: keine sofortige Überschreibung; Kurzfassung mit Vergleich + Checkbox; Fähigkeiten mit Neu/Aktualisierung, Checkboxen, „Alle auswählen/abwählen“; **`Escape`** schließt; KI-Schaltflächen blockiert solange Dialog offen.
**Offen nächste Schritte Pflege/Umsetzung:** weitere Retrieval-Profile (z.B. Karate-/Fitness-Schwerpunkt) per SQL später Admin-UI; optionales Feld **`skills.ai_context`** Kurzbeschreibung für KI; automatische KI beim Speichern (**S5**); Prompt-/Profil-Admin-UI ohne SQL; Rate-Limits.
**Bewusst noch nicht (`summary_ai_generated`):** zurücksetzen bei manueller Kurzfassung im UI; Admin-Pflege `ai_skill_retrieval_profiles`.

View File

@ -0,0 +1,124 @@
# Mehrstufige KI für Trainingsplanung Architektur-Vorschau (Anti-Refactoring)
**Version:** 0.1
**Datum:** 2026-05-22
**Status:** Planungs-/Architektur-Arbeitspapier (keine Implementierungspflicht)
**Ziel:** Für die **spätere** Planungs-KI bereits **Schnittstellen und Schichten** vorzeichnen, damit die **kleinere, starre** Übungs-KI nicht zur impliziten Vorlage für einen viel größeren Kopf wird — **ohne** jetzt eine Mitai-artige Workflow-Engine zu bauen.
**Update 2026-06-07:** Progressionsgraph startet **Phase F** (`planning_progression_roadmap.py`) — Roadmap-first, Workflow-lite. Siehe **`PLANNING_PROGRESSION_ROADMAP_SPEC.md`** und **`docs/architecture/PLANNING_KI_ROADMAP.md`**. Gruppenanalyse bleibt in der **Trainingsplanungs-Pipeline** (§3 S0S4), nicht im Graphen.
**Bezüge:** `technical/AI_TRAINING_PLANNING_CONCEPT.md` · `functional/AI_EXERCISE_ASSISTANT_VISION.md` · `technical/SKILL_SCORING_SPEC.md` · `functional/TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` (CURR-003) · Schwesterprojekt Mitai: `c:/dev/mitai-jinkendo` (Referenz: `prompt_executor`, `placeholder_resolver`, `workflow_*`**nicht** Pflicht-Port).
---
## 1. Zwei getrennte Produktlinien (bewusst entkoppelt)
| Linie | Rolle | Orchestrator |
|--------|--------|----------------|
| **Übungs-KI** | wenige Eingaben → Kurzfassung / Skills; **starrer** Ablauf (12 Calls), kleines Kontextfenster | z.B. `exercise_ai.py` (heute) |
| **Planungs-KI** | Gruppe, Zeit, Ziele, Historie, Katalogausschnitt, Phasen/Streams → **strukturierte Planelemente** | **eigenes** Modul + **mehrstufig** (siehe §3) |
**Regel:** Shared Library nur auf **niedriger Ebene** (`openrouter_chat`-Art: HTTP, Timeouts, Modellname, Fehler-Mapping) und **gemeinsame Prompt-Tabelle** `ai_prompts`. **Keine** Vermischung der Geschäftslogik „Übung erstellen“ mit „Einheit füllen“, um später keine Abhängigkeiten reißen zu müssen.
---
## 2. Konzeptioneller „Planungs-Graph“ (Daten, nicht zwingend Graph-DB)
Für die Planungs-KI ist ein **Graph als Denkmodell** hilfreich — technisch reicht meist **PostgreSQL + bestehende FKs** (+ optional `exercise_progression_graphs`):
**Knoten-Typen (Auszug):** `training_groups`, `training_units`, `training_unit_sections` / Items, `exercises`, `skills`, `training_framework_programs` / Slots / Goals, ggf. Nachbearbeitungs-/Debrief-Metadaten.
**Kanten-Typen (Auszug):**
- **Zeitliche Folge:** Einheiten einer Gruppe nach `planned_date` / Reihenfolge
- **Inhalt:** Section-Item → `exercise_id` (± Variante)
- **Ziele:** Slot-/Framework-Ziele, Kopf-Notizen, Trainer-Zieltexte
- **Progression:** Kanten aus `exercise_progression_graphs` (optional erweitern um „empfohlene Folge im Gruppenkontext“, bleibt Spekulationsfeld)
- **Skills:** bereits über `exercise_skills`; aggregiert über `skill_scoring`-Pfad
**Wichtig:** Für KI **nicht** einen Riesen-Graphen serialisieren, sondern **Projektionen** („letzte *N* Einheiten“, „Nachbarn im Progressionsgraph zu zuletzt verwendeten Übungen“, „Skill-Gap Heuristik“).
---
## 3. Mehrstufiger Prozess (Pflichtidee für Planungs-KI)
Statt einem Prompt „mach den ganzen Plan“ mehrere **Schritte mit kleinen, validierbaren Outputs**:
| Stufe | Beispiel-Aufgabe | Deterministisch möglich? | Typischer LLM-Einsatz |
|-------|-------------------|--------------------------|------------------------|
| **S0** | Governance + Filter + Historie + Slot-Ziele zusammenstellen | Ja (SQL/API) | Nein |
| **S1** | Kandidaten-Übungen auf TopK schrumpfen (Skills, Volltext, Score, Wiederholungsstrafe) | Teilweise | Optional Ranking |
| **S2** | Reihenfolge je Section / Phase unter Constraints (Aufwärmen, Graphen-Nachbarn) | Teilweise | Ja (auf kleiner Liste) |
| **S3** | Zeiten auf Section/Item vorschlagen oder Plausibilisieren | Teilweise | Ja |
| **S4** | Trainer-sprachliche Kurzbegründung / Alternativen | Nein | Ja |
**Zwischen jeder Stufe:** starkes **Schema / Validierung** (z.B. nur erlaubte `exercise_id`s, nur erlaubte Slot-Struktur zu Phasen/Streams). So bleibt das System auch bei Modell-Fehlern stabil.
---
## 4. Schnittstellen-Vorsorge im Code (ohne Big-Bang)
Minimal-Ausbaustufe später, die Refactoring vermeidet:
1. **`PlanningContextPack` (internes DTO)** — reines Python-`dict`/`dataclass` oder Pydantic: aggregierte, **tokenbewusst gekürzte** Ansicht (Gruppe, nächste Einheit-Ziele, Historie-IDs, TopK-Kandidaten, Constraints).
2. **`planning_ai_steps` als rein **funktionale** Pipeline** — jede Stufe `(context) → context` oder `(context) → partial_suggestion`; keine globale „Prompt-String-Bastelei“ überall im Router.
3. **Prompt-Slugs pro Stufe** in `ai_prompts` (analog Übung), z.B. `planning_rank_section_items`, `planning_explain_sequence`, mit **eigenem** Platzhalter-Katalog (nicht `{{skills_catalog}}` aus Übungen recyclen).
4. **Router** `training_planning.py` (oder neuer `planning_ai.py`): nur **dünne** HTTP-Schicht, ruft Orchestrator.
Optional **später**, wenn nötig: zweite Tabelle `ai_prompt_chains` oder externe Workflow-Definition — **erst** wenn 34 feste Stufen nicht mehr reichen. Mitai-Workflow-Engine dann **bewusste** Option, kein Default.
---
## 5. Kontextfenster und „Kaskade“
**Kerngedanke:** Je Stufe nur **neue** Information hinzufügen, die vorherige Stufen **ersetzen** oder **verdichten**, nicht duplizieren.
Beispiel:
- Stufe A (LLM oder Heuristik): „Priorisierte Skill-Ziele für diese Session“ (kurz)
- Stufe B: Top40 Übungen mit **einer** Zeile pro Übung
- Stufe C: Reihenfolge für 8 IDs + 2-Satz-Begründung
So bleibt dieselbe fachliche Tiefe erreichbar ohne Kontext-Explosion.
---
## 6. Schnittstellen zu bereits vorhandenen Bausteinen
- **`skill_profiles` / `skill-discovery`:** liefern **deterministische** Ziel-/Profil-Signale für S0/S1 (`SKILL_SCORING_SPEC.md`).
- **`training_planning_prefs`:** weiche Constraints (Tone, Dauer, Split-Vorlieben).
- **`exercise_progression_graphs`:** lokale Nachbarschaft um „zuletzt verwendet“.
- **Mitai-Referenz:** Platzhalter-Katalog + Preview-API als **Inspiration** für Admin-UX; Workflow-Graph nur wenn Shinkan **wirklich** viele verzweigte Pipelines braucht.
---
## 7. Was wir **nicht** jetzt tun müssen
- Keine zweite Graph-Datenbank nur für KI.
- Keine Workflow-UI-Kopie aus Mitai.
- Keine Vereinheitlichung der Übungs-KI mit Planungs-KI über einen „Mega-Orchestrator“.
---
## 8. Kurz-Checkliste „Refactoring vermeiden“ vor erster Planungs-KI-Zeile Code
- [ ] Eigenes Modulbaum-„Root“ für Planung (nicht `exercise_ai` erweitern).
- [ ] Prompt-Slugs mit **Planungs-**Präfix und **eigenem** Platzhalter-Set dokumentieren.
- [ ] Outputs pro Stufe **JSON-Schema** oder Pydantic validieren.
- [ ] Kandidatenlisten **immer** serverseitig auf erlaubte IDs begrenzen.
---
## 9. Progressionsgraph vs. Trainingsplanung (2026-06-07)
| Pipeline | Kontext | Orchestrator |
|----------|---------|--------------|
| **Progressionsgraph (F)** | Zieltext, N Steps, Semantic Brief | `planning_progression_roadmap.py` |
| **Trainingsplanung (G, später)** | Gruppe, Historie, Rahmen, Zeit | `planning_ai_steps` + ggf. Mitai Workflow |
---
## 10. Changelog
- **2026-06-07:** Verweis Phase F Roadmap-first; Abgrenzung Graphen/Planung.
- **2026-05-22:** Erstfassung als Vorschau-Dokument für mehrstufige Planungs-KI.

View File

@ -0,0 +1,121 @@
# KI Skill-Retrieval-Profile (`ai_skill_retrieval_profiles`)
**Version:** 0.1
**Datum:** 2026-05-29
**Status:** Umsetzung gestartet (Migration **068**)
**Ziel:** Für `POST /api/exercises/ai/suggest` (Skill-Katalogauszug) **Gewichte und Quoten** steuerbar machen:
- gebunden an **Übungs-Fokusbereich** (`focus_areas.id`),
- ein **Standardprofil** ohne Fokus,
- **optional zusammengeführte** Profile bei mehreren Fokusbereichen,
- **optional Keyword-Übersteuerungen** aus Ziel/Durchführung (z.B. Rollenspiel vs. Befreiung).
**Technische Basis:** Skills mit `skills.main_category_id``skill_main_categories.slug` (`karate` | `allgemeine`) und `skills.category_id``skill_categories.slug` (`kondition`, `selbstverteidigung`, …).
**Bezüge:** `.claude/docs/working/AI_EXERCISE_IMPLEMENTATION_PLAN.md` · `backend/exercise_ai.py`
---
## 1. Datenmodell
### Tabelle `ai_skill_retrieval_profiles`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| `id` | serial | Primärschlüssel |
| `focus_area_id` | int NULL FK → `focus_areas(id)` ON DELETE SET NULL | **`NULL`** nur für Standardeintrag möglich (siehe `is_default`) |
| `is_default` | boolean | Genau **eine** Zeile mit `true` |
| `name` | varchar | Kurzer Name (Admin später) |
| `description` | text | Hinweise für Pflege |
| `active` | boolean | Nur aktive werden geladen |
| `config` | jsonb | Siehe §2 |
**Constraints / Indizes**
- Eindeutig: `(focus_area_id)` WHERE `focus_area_id IS NOT NULL`
- Eindeutig: `(is_default)` WHERE `is_default = true`
---
## 2. JSON-Konfiguration `config.version = 1`
Alle Schlüssel **optional**; fehlende Werte fallen auf **einprogrammierten Fallback** in `exercise_ai.py` zurück (entspricht bisher grob „neutral“).
### 2.1 Gewichtungen (Ranking)
| Schlüssel | Typ | Bedeutung |
|-----------|-----|------------|
| `main_slug_weights` | `object[str, float]` | Multiplikator pro Hauptkategorie-Slug (`karate`, `allgemeine`) |
| `category_slug_weights` | `object[str, float]` | Multiplikator pro `skill_categories.slug` |
Basis-Score (vereinfacht):
`(importance oder 3) × main_w × cat_w × text_overlap_bonus × importance_multiplier`
### 2.2 Kapazitätsbegrenzung (Liste)
`_MAX_SKILLS_CATALOG_LINES` (aktuell **240**) Zeilen Gesamt:
| Schlüssel | Typ | Bedeutung |
|-----------|-----|------------|
| `category_max_share` | `object[str, float]` | Max. Anteil dieser **Unterkategorie** am Endergebnis (01), z.B. `{ "kondition": 0.25 }` |
| `main_min_share` | `object[str, float]` | Mindest-Zielanteil Hauptkategorie beim **Auswahl-Greedy** (weich; Rest nach Score aufgefüllt) |
### 2.3 Text / Token-Sparen
| Schlüssel | Typ | Standard | Bedeutung |
|-----------|-----|----------|------------|
| `description_plain_max_len` | int | 160 | Gekürzte Beschreibung pro Zeile |
| `karate_relevance_max_len` | int | **0** oder 80 | **`0`** = Feld `karate_relevance`/`relevance_level` in der Promptzeile **weglassen** |
### 2.4 Keyword-Overrides (optional)
Liste `keyword_overrides`: jedes Element:
```json
{
"keywords_any": ["befreiung", "haltegriff"],
"case_insensitive": true,
"patch": {
"category_slug_weights": { "selbstverteidigung": 2.5 },
"category_max_share": { "koordination": 0.1 }
}
}
```
Textsuche in verkettetem Korpus **Titel, Ziel, Durchführung, Focus-Hint** (bereits plaintext). Reihenfolge: erst Basis-Profile zusammenmergen, dann **alle treffenden Overrides**`patch`Objekte **flach zusammenführen** (Gewichte multiplikativ übereinander, Caps den strengsten Wert nehmen aktuelle Implementierung im Code dokumentiert).
---
## 3. Mehrere Fokusbereiche auf der Übung
Request-Body: `focus_areas_context: [{ "focus_area_id": n, "is_primary": bool }, …]`
**Aktuelle Merge-Strategie (v1):** Profile laden → **gleichgewichtete Mittelwert-Bildung** der numerischen Gewichte / Caps (implementiert für `main_slug_weights`, `category_slug_weights`, `category_max_share`, `main_min_share`, `*_max_len`). Anschließend **Keyword-Overrides** anwenden.
**Primär-Fokus:** Im Frontend soll die **primäre** Zeile aus `focus_areas_multi` **zuerst** in der Liste stehen; die Merge-Strategie kann später zu „Primär dominate“ erweitert werden.
Ohne Kontext oder ohne Treffer auf aktive Profile: **nur Standardprofil** (`is_default`).
---
## 4. Seed-Daten (Migration)
- **`is_default=true`:** ausgewogene Standard-Gewichte, moderate Caps auf `kondition`/`koordination`, Karate-Relevanz gekürzt.
- **`Gewaltschutz`:** `focus_area_id` per `(SELECT id FROM focus_areas WHERE name = 'Gewaltschutz' LIMIT 1)` — höhere Gewichte für `kognition`, `psychische_faehigkeiten`, `soziale_faehigkeiten`, `selbstverteidigung`; gedrosseltes `kondition`/`koordination`; `karate_relevance_max_len`: 0; Keyword-Patches wie oben können nachgeschärft werden.
Weitere Profile (Karate-Schwerpunkt etc.) später per Admin-SQL oder UI.
---
## 5. API
`ExerciseAiSuggestBody` erweitert um **`focus_areas_context`** (Liste). Feld **`focus_area_hint`** bleibt für den **Prompt-Kontext** (bestehende Prompts).
`POST …/ai/regenerate` nutzt gespeicherte `exercise_focus_areas` zur gleichen Retrieval-Logik wie Suggest.
**Pflege der Profile:** Superadmin ohne Mandantenwahl — **`GET|POST /api/admin/ai-skill-retrieval-profiles`**, **`GET|PUT|DELETE /api/admin/ai-skill-retrieval-profiles/{id}`** (`routers/ai_skill_retrieval_admin.py`); Web-UI Superadmin unter **`/admin/ai-skill-retrieval`**.
## 6. Changelog
- **2026-05-29:** Superadmin-Pflege-Endpoints + UIRoute dokumentiert (`/admin/ai-skill-retrieval`).
- **2026-05-29:** Erstellt; gekoppelt an Migration **068** und erste `exercise_ai`-Integration.

View File

@ -0,0 +1,68 @@
# Superadmin: Übungs-Anreicherung per KI
Stand: 2026-05-23 · App 0.8.178
## Zweck
Plattform-weites Werkzeug für Superadmins, um Übungen (typisch `draft`, ohne Skills) **batchweise** per KI mit Fähigkeiten anzureichern und kontrolliert auf `in_review` zu setzen.
Verbessert indirekt die Planungs-KI (`POST /api/planning/exercise-suggest`), die gegen Skill-Profile rankt — unvollständige `exercise_skills` führen dort zu Volltext-dominiertem Ranking.
## UI
- Route: `/admin/exercise-enrichment` (nur Superadmin)
- Admin-Menü: „Übungs-Anreicherung“
## API
Prefix: `/api/admin/exercise-enrichment`
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| GET | `/candidates` | Paginierte Kandidaten (Filter: status, visibility, focus_area, without_skills, with_ai_suggested_skills, include_club, search) |
| POST | `/preview` | Dry-Run — `{ exercise_ids[], modes: { skills, summary }, merge_mode }` |
| POST | `/apply` | `{ items: [{ exercise_id, merged_skills }], merge_mode, set_status }` |
Auth: `require_auth` + `is_superadmin`**kein** `TenantContext` (EXEMPT, siehe ACCESS_LAYER_ENDPOINT_AUDIT.md).
## KI
Wiederverwendet `run_exercise_form_ai_suggestion` → Prompts `exercise_skill_suggestions` (MVP Pflicht), optional `exercise_summary`. Skill-Katalog via `build_contextual_skills_catalog_block` / `ai_skill_retrieval_profiles`.
## Merge-Modi (Skills)
- `additive` (Default): manuelle Skills bleiben; KI ergänzt neue; bestehende `ai_suggested`-Links werden aktualisiert
- `replace_ai_only`: nur `ai_suggested=true` entfernen, dann KI-Set anwenden
- `replace_all`: alle Skills ersetzen (explizit)
## Defaults
- Kandidaten: **Status** primär (Default `draft`); Sichtbarkeit Default **`private`**, wählbar bis „Alle“
- Skill-Merge Default: **`replace_all`** (alle Skills KI-neu, `ai_suggested=true` — unterscheidbar von manuell)
- Nach Apply: `set_status=in_review` (nie automatisch `approved`)
- Batch: keine Gesamtgrenze (bis 10.000 IDs); **Analyze** + explizite Nutzerbestätigung
- **Preview:** max. **3 Übungen/HTTP-Request** (parallel LLM), Frontend chunked — vermeidet Gateway-504 (~60s Fritz!Box)
- **Apply:** HTTP-Chunks à 25 (nur DB, kein LLM)
## Inhalte (modular)
| Modus | Prompt | Apply-Felder |
|-------|--------|--------------|
| Skills | `exercise_skill_suggestions` | `exercise_skills` inkl. Intensität, required/target_level, `ai_suggested` |
| Summary | `exercise_summary` | `summary`, `summary_ai_generated=true` |
| Anleitung | `exercise_instruction_rewrite` | `goal`, `execution`, `preparation`, `trainer_notes` |
## API (ergänzt)
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| GET | `/candidate-ids` | Alle IDs zum Filter (Select-all) |
| POST | `/analyze` | `{ exercise_ids[], modes }` → Kosten-Schätzung vor Start |
## Keine Migration
Bestehende Spalte `exercise_skills.ai_suggested` reicht; kein Enrichment-Log in MVP.
## Tests
`backend/tests/test_exercise_enrichment_admin.py` — 403, Merge-Logik, Status draft→in_review.

View File

@ -0,0 +1,58 @@
# Rahmenprogramm: Filter, Dauer, Fähigkeiten-Schwerpunkte (Roadmap)
**Stand:** 2026-05-20
**Status:** Phase 1 umgesetzt; Phase 3 v1.0 umgesetzt (regelbasiert); Phase 2 teilweise offen
## Phase 1 (umgesetzt)
### Listen-Anzeige Session-Dauer
- **GET `/api/training-framework-programs`:** `session_duration_min`, `session_duration_max` (aus Blueprint-`training_units.planned_duration_min`), `goal_titles_agg`, ID-Arrays für Katalog-M:N.
- **UI:** Rahmenprogramm-Liste, Trainingsplanung (Einheiten-Liste/Kalender), Import-Dialog (Programm + pro Slot).
### Import-Filter (clientseitig)
- Textsuche (Titel, Beschreibung, Ziele, Katalog-Namen)
- Fokusbereich, Trainingsart, Zielgruppe (Checkboxen, Katalog-API)
- Ziel-Session-Dauer in Minuten (±10 Min Toleranz gegen Min/Max der Slots)
**Grenze:** Entwicklungsziele sind **freie Texte** pro Rahmen (`training_framework_goals.title`), keine kontrollierte Taxonomie → Filter nur Volltext, keine homogene „Ziel-Tags“-Liste.
## Phase 2 (empfohlen, ohne KI)
| Kriterium | Datenquelle heute | Verbesserung |
|-----------|-------------------|--------------|
| Fokusbereich / Stil / Trainingsart / Zielgruppe | M:N am Rahmenkopf | bereits filterbar |
| Entwicklungsziele | Freitext-Ziele | Optional: Ziel-Vorlagen-Katalog oder Tags (Migration) |
| Session-Dauer | `planned_duration_min` pro Slot | erledigt |
| Fähigkeiten-Schwerpunkt | noch nicht | siehe Phase 3 |
**API-Erweiterung (optional):** `GET /api/training-framework-programs?focus_area_id=&training_type_id=&duration_min=` serverseitig — sinnvoll ab >50 Rahmen in der Bibliothek.
## Phase 3 — Fähigkeiten aus Übungen (umgesetzt v1.0)
**Spec:** `.claude/docs/technical/SKILL_SCORING_SPEC.md`
- Gewichtetes Profil: Rahmenprogramm (gesamt + pro Slot), Trainingsmodul, Progressionsgraph
- `GET /api/skill-discovery/suggestions?skill_ids=…` für Bibliotheks-Vorschläge
- UI: Profil-Panels in Editoren + Tab „Planungs-Vorschläge“ auf der Fähigkeiten-Seite
- **Kein** automatisches Überschreiben der Stammdaten-Fokusbereiche
### Variante B — KI-Zusammenfassung (OpenRouter, optional, offen)
1. Input: Titel Rahmen, Ziele (Text), Liste Übungstitel + Dauer + vorhandene Skill-Namen.
2. Prompt: strukturiertes JSON (`suggested_focus_areas[]`, `skill_emphasis[]`, `rationale_de`).
3. Speichern als `ai_context_summary` (Version, Modell, Timestamp) — **nur Vorschlag**, manuelle Bestätigung vor Übernahme in Stammdaten.
**Vorteil:** natürliche Schwerpunkte auch bei unvollständigen Skill-Links.
**Risiko:** Halluzination, Kosten, Datenschutz (Vereinsdaten in Prompt).
### Empfehlung
Zuerst **Variante A** für Listen/Filter und Abgleich mit manuell gesetzten Fokusbereichen; KI nur als **„Vorschlag generieren“-Button** im Rahmen-Editor, wenn Regelwerk und Katalog-Zuordnung zu dünn sind.
## Offene Produktfragen
1. Soll Filter **UND** (alle Kriterien) oder **ODER** (mindestens eines) sein? — Import aktuell **UND**.
2. Rahmen mit **unterschiedlichen** Slot-Dauern: Liste zeigt MinMax; Filter „90 Min“ trifft Range.
3. Sollen homogenisierte **Entwicklungsziel-Tags** ein eigener Katalog werden (Admin), analog `target_groups`?

View File

@ -0,0 +1,529 @@
# Planungs-KI: Übungssuche & Kontext für Neu-Anlage
**Version:** 0.2
**Datum:** 2026-05-23
**Status:** P0P2 ✅ · Phase A/B/B2 ✅ · **Phase C1C3 ✅** · **Phase E ✅** (Semantik + Pfad-QA)
**Bezüge:** `AI_PLANNING_KI_MULTISTAGE_FORECAST.md` · `AI_PROMPT_TARGET_ARCHITECTURE.md` · `SKILL_SCORING_SPEC.md` · `TRAINING_FRAMEWORK_SPEC.md` §3 (Progressionsgraph)
---
## 1. Ziel
Trainer in der **Trainingsplanung** sollen Übungen finden oder anlegen können mit natürlichen Anfragen wie:
- „Vertiefung zu Übung XY“
- „Nächste sinnvolle Übung im Progressionsgraph Z“
- „Baut auf der bisherigen Planung auf — Reaktionsschnelligkeit mit Partnern“
- **Preset:** „Schlage mir die nächste Übung vor“
**Suche** (Bibliothek) und **Neu mit KI-Assistent** (Anlage) nutzen dasselbe **`PlanningExerciseContextPack`** — unterschiedliches Ergebnis (Treffer vs. Entwurf).
---
## 2. Architektur (Mehrstufig)
| Stufe | Name | Technik | P0 |
|-------|------|---------|-----|
| **S0** | Kontext-Pack | SQL/API, deterministisch | ✅ |
| **S1a** | Intent strukturieren | LLM `planning_exercise_search_intent` (Szenario-Pipeline) | ✅ P1 |
| **S1b** | Hybrid-Retrieval | Score: Volltext + Graph + Skills + Plan + **Profil** | ✅ |
| **S1b+** | Profil-Vorselektion | `ExerciseMatchProfile` × `PlanningTargetProfile` | ✅ `profile_v1` |
| **S1c** | Rerank + Begründung | Optional LLM `planning_exercise_search_rank` | Regelbasierte `reasons[]` |
| **S2** | Neu-Anlage | Bestehende `suggestExerciseAi` + Pack als Zusatzkontext | Später |
Zwischen jeder Stufe: **nur erlaubte `exercise_id`s** (Governance / Sichtbarkeit).
---
## 3. Intent-Typen
| `intent_hint` | Bedeutung | Retrieval-Gewichtung (P0) |
|---------------|-----------|---------------------------|
| `suggest_next` | Nächste Übung (Default bei leerer/kurzer Query) | Progression + Skill-Overlap + Plan-Kontinuität |
| `progression_next` | Explizit Graph-Folge | Progression hoch |
| `deepen_exercise` | Vertiefung zu Anker-Übung | Skill-Overlap hoch, ähnlicher Fokus |
| `continue_plan_goal` | Auf bisherigen Plan aufbauen | Plan-Kontinuität, Wiederholungsstrafe |
| `free_search` | Freitext / Stichwort | Volltext hoch |
**S1a (später):** Freitext → JSON `{ intent, skill_hints[], requires_partner, level_hint, … }` validiert per Pydantic.
**P0:** `intent_hint` vom Client oder Keyword-Heuristik auf `query`.
---
## 4. PlanningExerciseContextPack (S0)
Serverseitig aus Request + DB (tokenbewusst für spätere LLM-Stufen):
| Feld | Quelle | UI-Chip |
|------|--------|---------|
| `unit_id`, Titel, `group_id`, Gruppenname | `training_units` + `training_groups` | Gruppe · Einheit |
| `section_order_index`, Abschnittstitel | `training_unit_sections` | Abschnitt |
| `planned_exercise_ids[]` | Items der Einheit (Reihenfolge) | „N Übungen im Plan“ |
| `anchor_exercise_id`, Titel | Request oder letzte Übung vor Einfügepunkt | Anker |
| `anchor_skill_ids[]` | `exercise_skills` | (intern) |
| `progression_graph_id` | Request oder **Auto-Match** vom Anker (sichtbarer Graph mit passenden Ausgangskanten) | Graph |
| `progression_graph_name`, `progression_graph_auto_resolved` | Response `context_summary` | Graph (auto) |
| `anchor_exercise_variant_id` | Request / Abschnitt-Item / DB | (intern) |
| `progression_successor_ids[]` | `exercise_progression_edges` ab Anker (variantenbewusst, Migration **034**) | (intern) |
| `progression_successor_variants` | `to_exercise_variant_id` pro Nachfolger | (intern) |
| `group_recent_exercise_ids[]` | Letzte Einheiten derselben Gruppe | Wiederholungsstrafe |
| `framework_slot_notes` | Rahmen-Slot falls `framework_slot_id` | (später) |
**Berechtigung:** `get_tenant_context` + `_assert_training_unit_permission` wie `GET /training-units/{id}`.
---
## 5. Hybrid-Retrieval (S1b, P0)
Kandidaten: sichtbare Übungen (`library_content_visibility_sql`), ohne `archived`, max. ~400 (recent).
**Score** (01, gewichtet nach Intent):
```
score = w_ft * fulltext_rank
+ w_prog * progression_hit
+ w_skill * skill_jaccard(anchor, candidate)
+ w_plan * plan_affinity
+ w_profile * profile_match(exercise, target)
+ w_repeat * (candidate in unit_plan ? -1 : 0)
+ w_group_repeat * (candidate in group_recent ? -0.5 : 0)
```
**`profile_match`** (01): siehe §12§13 — Katalog-Dimensionen + Skill-Gewichte + Skill-Gap.
**`reasons[]`** (regelbasiert, Deutsch): z. B. „Nachfolger im Progressionsgraph“, „Fähigkeiten passen zur Anker-Übung“, „Fokusbereich passend zum Planungsziel“, „Deckt Skill-Lücke im bisherigen Plan“, „Volltext-Treffer“.
---
## 6. API
### `POST /api/planning/exercise-suggest`
**Body:**
```json
{
"unit_id": 123,
"section_order_index": 0,
"phase_order_index": null,
"parallel_stream_order_index": null,
"anchor_exercise_id": 456,
"anchor_exercise_variant_id": 12,
"progression_graph_id": 7,
"query": "Schlage mir die nächste Übung vor",
"intent_hint": "suggest_next",
"limit": 20,
"exercise_kind_any": ["simple"]
}
```
**Response:**
```json
{
"context_summary": {
"unit_title": "…",
"group_name": "…",
"section_title": "Hauptteil",
"planned_count": 4,
"anchor_title": "Partner-Fangspiel"
},
"target_profile_summary": {
"sources": ["framework_catalog", "current_unit_plan", "anchor_exercise"],
"focus_areas": ["Reaktion & Abwehr"],
"top_skills": [{ "skill_id": 12, "name": "Reaktionsgeschwindigkeit", "weight": 1.0 }],
"has_skill_gap": true
},
"retrieval_phase": "profile_v1",
"intent_resolved": "suggest_next",
"hits": [
{
"id": 99,
"title": "…",
"summary": "…",
"score": 0.78,
"reasons": ["Nachfolger im Progressionsgraph", "Fokusbereich passend zum Planungsziel"],
"focus_area": "…"
}
]
}
```
**Modul:** `backend/planning_exercise_suggest.py` · `backend/planning_exercise_profiles.py` · Router `backend/routers/planning_exercise_suggest.py`
---
## 7. Frontend
| Ort | Verhalten |
|-----|-----------|
| `ExercisePickerModal` | Prop `planningContext` → Planungs-API statt reiner `listExercises`; Kontext-Chips; `reasons` unter Treffer |
| `TrainingUnitEditPage` | `planningContext` aus Einheit + Picker-Ziel (Anker = letzte Übung im Abschnitt) |
| **`ExercisesListPageRoot`** | Schalter **„Neu mit KI-Assistent“**: Planungs-KI-Suche (frei, ohne `unit_id`) + Neuanlage im Modal; **„+ Neu“** ausgeblendet |
| Rahmen / Kombi-Formular | analog, sobald `unit_id` / Slot-Blueprint bekannt |
| Übungsliste (ohne KI-Schalter) | weiter Volltext |
**Zweites Suchfeld** im Picker: Query = Volltext + ergänzender Begriff (ODER in P0 als Konkatenation an Backend).
---
## 8. Neu-Anlage (Anbindung, Phase P1)
Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“:
- `planning_context` im Request-Body → `planning_context_json` in Übungs-Prompts (Migration **085**); Pfad-Builder + Picker ✅ **0.8.208**
- Kurzbeschreibung optional leer (freier Vorschlag) oder aus Intent/Skizze
---
## 9. Phasen-Roadmap
| Phase | Inhalt | Status |
|-------|--------|--------|
| **P0** | Context-Pack, Hybrid-Score, API, Picker in Planung | ✅ |
| **P0.1** | `ExerciseMatchProfile` / `PlanningTargetProfile`, `profile_v1` | ✅ |
| **P1** | Szenario-Pipeline + LLM Query-Intent → Erwartungsprofil | ✅ |
| **P2 / B2** | LLM-Rerank bei engem Top-Feld (max. 2 Calls) | ✅ |
| **P3** | Skill-Discovery / Framework-Ziele im Pack | 🔲 |
| **A** | Voll-Library Hybrid-Ranking | ✅ **0.8.177** |
| **B** | Text-Signale guidance/Rahmen-Ziele | ✅ **0.8.181** |
| **C1** | Graph auto-match + variantenbewusste Nachfolger | ✅ **0.8.183** |
| **C2** | Varianten in Trefferliste / Picker | ✅ **0.8.184** |
| **C3** | Graph-Builder (Ziel → Pfad → speichern) | ✅ **0.8.185** |
| **E** | Semantik-Schicht + Pfad-QA (Lücken/Brücken/LLM-QS) | ✅ **0.8.186** |
| **E2** | Pfad-Neuordnung + KI-Lückenfüller | ✅ **0.8.187** |
| **D** | Neu-Anlage: `planning_context` an `suggestExerciseAi` (Migration **085**) | ✅ **0.8.208** |
---
## 10. Changelog
- **2026-05-23:** Phase C1 — Graph auto-match, variantenbewusste Nachfolger (`planning_exercise_progression.py`).
- **2026-05-23:** Phase B2 — Rerank bei engem Top-Feld; Phase B — Text-Signale; Phase A — Voll-Library (siehe §17§19).
- **2026-05-22:** Erstfassung; P0 API + Planungs-Picker.
- **2026-05-22:** P0 implementiert (`planning_exercise_suggest.py`, Router, Picker); unsaved Formular-Plan noch nicht an API (nur persistierte Einheit).
- **2026-05-22:** P0.1 — `planning_exercise_profiles.py`, Profil-Score in Hybrid-Retrieval, `retrieval_phase: profile_v1`, `target_profile_summary`.
- **2026-05-22:** P2 — LLM-Rerank optional (`include_llm_rank`); Client `planned_exercise_ids[]`; Prompt Migration 072.
---
## 11. Bekannte Lücken & Backlog
- **Ungespeicherte Plan-Änderungen:** ✅ Client übergibt `planned_exercise_ids[]` aus Formular (TrainingUnitEditPage).
- **Progressionsgraph-ID:** ✅ Auto-Match vom Anker (**C1**); manuelle Auswahl in UI noch offen.
- **Anker-Variante:** ✅ Client + DB (**C1**); Picker wählt Variante bei Treffer (**C2** — Dropdown + Graph-Vorschlag).
- **Graph-Builder (C3):** Ziel → Pfad vorschlagen → in Graph speichern — ✅ **0.8.185**
- **Varianten-Suche:** Library-Picker nutzt `include_variants`; Planungs-KI rankt primär **Übungsebene** — Varianten-Expansion nur gezielt (**C2**).
- **Enrichment:** Superadmin-Tool für Skills; Datenqualität der Bibliothek entscheidend für Profil-Score.
- **LLM-Intent:** ✅ P1 Szenario-Pipeline + `planning_exercise_search_intent` (Migration 073).
- **Preset + LLM:** ✅ Erwartungs-LLM (074) bei Planungsbezug; Preset ohne Plan = kein Erwartungs-LLM.
---
## 16. Szenario-Pipeline & Query-Erwartungsprofil (P1)
Komplexe Planungsanfragen brauchen **Schritte vor** dem Profil-Match — nicht jede Query ist gleich.
### 16.1 Szenario-Klassen
| `scenario_kind` | Typische Anfrage | LLM Intent? |
|-----------------|------------------|-------------|
| `preset_next` | „Nächste Übung vorschlagen“ (Preset) | Erwartungs-LLM (074) wenn Planungsbezug |
| `progression` | Progressionsgraph / Pfad | Ja (wenn Freitext) |
| `deepen` | Vertiefung Anker | Ja |
| `continue_plan` | Auf bisherigen Plan aufbauen | Ja |
| `additive_constraint` | Plan **+** Zusatz (z. B. Schnellkraft) | Ja |
| `free_search` | Offene Stichwortsuche | Ja |
**Routing:** `planning_exercise_target_pipeline.classify_planning_scenario()``should_run_llm_intent_pipeline()`.
### 16.2 Pipeline (Reihenfolge)
```
S0 Kontext-Pack
→ Heuristik-Intent + Szenario
→ [optional] LLM planning_exercise_search_intent
→ Basis PlanningTargetProfile (Rahmen, Plan, Anker, Gap)
→ Merge Query-Overlay (Katalog-IDs aus Hints)
→ Hybrid-Retrieval + Profil-Score
→ [optional] LLM-Rerank
```
Module: `planning_exercise_target_pipeline.py` · `planning_exercise_intent.py`
### 16.3 API (Erweiterung)
| Request | Default | Bedeutung |
|---------|---------|-----------|
| `include_llm_intent` | `true` | LLM nur wenn Szenario ≠ preset_next und Query nicht leer |
| Response | Bedeutung |
|----------|-----------|
| `scenario_kind` | Szenario-Klasse |
| `query_intent_summary` | intent, llm_applied, rationale, skill_hints_resolved |
| `intent_heuristic` | Heuristik vor LLM |
| `retrieval_phase` | z. B. `profile_v1+query_intent+llm_rank` |
**Prompt 073:** `planning_exercise_search_intent` — Ausgabe JSON mit `skill_hints`, `focus_hints`, `emphasis` (`additive`|`replace`).
---
## 15. LLM-Rerank (P2)
**Request:**
| Feld | Typ | Default | Bedeutung |
|------|-----|---------|-----------|
| `planned_exercise_ids` | `int[]` | — | Optional: Reihenfolge aus Formular (überschreibt DB-Plan) |
| `include_llm_rank` | `bool` | `true` (Client) | Backend gated (B2): Rerank nur bei engem Top-Feld, max. 2 LLM-Calls |
**Response:**
| Feld | Wert |
|------|------|
| `retrieval_phase` | `profile_v1` oder `profile_v1+llm_rank` |
| `llm_rank_applied` | `true` wenn LLM erfolgreich sortiert hat |
| `hits[].llm_rank` | optional: Position nach LLM (1…n) |
**Fallback:** Kein API-Key, inaktiver Prompt oder Parse-Fehler → Hybrid-Reihenfolge unverändert, `llm_rank_applied: false`.
**Prompt:** Migration **072**, Slug `planning_exercise_search_rank` — Kandidaten als JSON mit Titel, summary, goal (Plaintext), skills; Ausgabe `{ ranked_ids, reasons }`.
---
## 12. ExerciseMatchProfile & PlanningTargetProfile (Phase 1)
Ziel: deterministische Vorselektion über **Profil-Dimensionen** statt nur Titel/Jaccard.
### 12.1 ExerciseMatchProfile (pro Übung)
| Feld | Quelle |
|------|--------|
| `focus_area_ids` | `exercise_focus_areas` (Primary = 1.0, sonst 0.85) |
| `style_direction_ids` | `exercise_style_directions` |
| `training_type_ids` | `exercise_training_types` |
| `target_group_ids` | `exercise_target_groups` |
| `skill_weights` | `exercise_skills` × Intensitäts-Multiplikator (`skill_scoring._skill_link_multiplier`) |
Bulk-Lader: `load_exercise_match_profiles_bulk(cur, exercise_ids)`.
### 12.2 PlanningTargetProfile (Planungsziel)
Zusammensetzung aus mehreren Quellen (`sources[]`):
| Quelle | Inhalt |
|--------|--------|
| `framework_catalog` | Fokus/Stil/Trainingsstil/Zielgruppe aus `training_framework_program_*` |
| `framework_slot_skill_profile` | Skill-Profil des Slot-Blueprints (`profile_for_occurrences`) |
| `framework_overall_skill_profile` | Fallback: alle Blueprint-Einheiten des Rahmens |
| `current_unit_plan` | Skill-Profil der bereits eingeplanten Übungen dieser Einheit |
| `anchor_exercise` | Katalog + Skills der Anker-Übung (Intent-abhängig) |
| `skill_gap_vs_plan` | `target_skills plan_skills` (normalisiert, Schwelle > 0.08) |
Builder: `build_planning_target_profile(cur, unit=…, planned_exercise_ids=…, anchor_exercise_id=…, intent=…)`.
Rahmen-Anbindung über `unit.framework_slot_id` oder `origin_framework_slot_id`.
---
## 13. Profil-Score (Formeln)
**Gewichtete Überlappung** (Katalog + Skills):
```
overlap(a, b) = Σ min(a[k], b[k]) / Σ max(a[k], b[k])
```
**Skill-Gap-Abdeckung:**
```
gap_coverage(gap, candidate) = Σ min(gap[k], candidate[k]) / Σ gap[k]
```
**Profil-Score** (intent-gewichtet, Summe Dimensionen = 1.0):
```
profile_score = w_focus * overlap(focus)
+ w_style * overlap(style)
+ w_tt * overlap(training_type)
+ w_tg * overlap(target_group)
+ w_skill * overlap(skill_weights)
+ w_gap * gap_coverage(skill_gap)
```
Intent-Gewichte (Auszug): `deepen_exercise` → Skill hoch; `continue_plan_goal` → Gap hoch; `free_search` → Gap + Skill moderat.
Scorer: `score_exercise_against_target(exercise_profile, target_profile, intent=…) → (score, reasons[])`.
---
## 14. Hybrid + Profil (P0.1)
Im Hybrid-Score kommt **`w_profile * profile_score`** hinzu (Intent-abhängig ~0.150.35). Jaccard auf Anker-Skills bleibt parallel (schneller Anker-Fokus).
**Response-Felder:**
| Feld | Bedeutung |
|------|-----------|
| `retrieval_phase` | `"profile_v1"` — Phase-1 aktiv, kein LLM-Rerank |
| `target_profile_summary` | Lesbare Kurzinfo für UI-Chips (Fokus, Top-Skills, Quellen) |
**Phase 2 (P2 / B2):** siehe §15 und §18 — `include_llm_rank: true` vom Client, Backend entscheidet.
---
## 17. Phase A — Voll-Library-Ranking (0.8.177)
- Kein OR-Profil-Pool (~500 Übungen) mehr.
- Alle sichtbaren Übungen (bis 8000) werden hybrid gescored (`fetch_all_visible_exercise_rows` + `rank_visible_library_hits`).
- API: `full_library_ranked: true`, `retrieval_phase` enthält `+full_library+`.
---
## 18. Phase B / B2 — Text-Signale & Rerank-Gates (0.8.1810.8.182)
**B — Text-Signale (`planning_exercise_text_signals.py`):**
- `section_guidance_notes`, Rahmen-Ziele/Notizen → Skill-/Katalog-Gewichte ohne LLM.
- `requires_partner` aus Intent filtert Kandidaten.
- `retrieval_phase +text_signals`.
**B2 — Rerank bei unklarem Ranking:**
- `hybrid_ranking_ambiguous(hits)` (Top-4-/Top-10-Gap).
- Rerank auch nach Erwartungs-/Intent-LLM, wenn Scores eng beieinander.
- Budget: max. **2** LLM-Calls (Profil + optional Rerank).
---
## 19. Phase C1 — Progressionsgraph im Planungskontext (0.8.183)
**Modul:** `planning_exercise_progression.py`
### Auto-Match Graph
Wenn `progression_graph_id` fehlt und Anker-Übung gesetzt: sichtbarer Graph mit passender `next_exercise`-Kante vom Anker (variantenbewusst). Bevorzugung: variantenspezifische Kanten > Anzahl Kanten.
### Variantenbewusste Nachfolger (Migration 034)
Generische Kante (`from_exercise_variant_id IS NULL`) gilt für jeden Anker; variantenspezifische Kante nur bei passender Anker-Variante.
Treffer: optional `hits[].suggested_variant_id`.
### Request / Response
| Feld | Bedeutung |
|------|-----------|
| `anchor_exercise_variant_id` | Request — Variante der Anker-Übung |
| `progression_graph_name` | Response — Name des (auto-)Graphs |
| `progression_graph_auto_resolved` | Response — Auto-Match aktiv |
---
## 20. Phase C2 — Varianten in Treffern (0.8.184) ✅
- API: `variants[]`, `suggested_variant_name` pro Treffer (Batch aus `exercise_variants`).
- **`ExercisePickerModal`:** Dropdown pro Treffer; Graph-`suggested_variant_id` vorausgewählt; Übernahme setzt `exercise_variant_id`.
- **`hydrateExercisePlanningRow`:** übernimmt `exercise_variant_id` / `suggested_variant_id` in die Planungszeile.
---
## 21. Phase C3 — Graph-Builder (0.8.185) ✅
**API:** `POST /api/planning/progression-path-suggest`
| Feld | Bedeutung |
|------|-----------|
| `query` | Ziel / Entwicklungsrichtung (Freitext, min. 3 Zeichen) |
| `max_steps` | 210, Default 5 |
| `progression_graph_id` | optional — Graph-Kontext für Nachfolger ab Schritt 2 |
| `include_llm_intent` | LLM nur Schritt 1 (Budget) |
**Response:** `steps[]` mit `exercise_id`, `variant_id`, `title`, `reasons`, `variants`; `retrieval_phase: …+path_builder`.
**Algorithmus:** Iterativ Hybrid-Ranking — Schritt 1 aus Zielprofil, Folgeschritte mit Anker = letzte Übung, ohne Duplikate.
**UI:** `ExerciseProgressionPathBuilder` im Progressionsgraph-Panel — Review, Varianten, `POST …/edges/sequence`.
---
## 22. Phase E — Semantik-Schicht + Pfad-QA (0.8.186) ✅
### Semantic Brief (`planning_exercise_semantics.py`)
Parallel zum Katalog-Overlay — **nicht ersetzend**:
| Feld | Bedeutung |
|------|-----------|
| `primary_topic` | z. B. `mae geri` |
| `must_phrases` / `exclude_phrases` | Phrasen-Match in Titel/Ziel/Varianten |
| `development_arc` | einstieg → … → perfektion |
| `semantic_strength` | 01 — steuert dynamisches Blend im Hybrid-Score |
| `retrieval_query` | fokussierte Volltext-Query (nicht ganzer Satz) |
Optional LLM: Prompt `planning_exercise_query_semantics` (Migration **075**).
**Hybrid-Score:** neuer Term `w_semantic * semantic_score` — Profil/Volltext werden bei hoher `semantic_strength` relativ abgeschwächt.
### Pfad-QA (`planning_exercise_path_qa.py`)
Nach Pfad-Bildung:
1. **Lücken-Messung** zwischen benachbarten Schritten (Skill-Jaccard + Semantik zum erwarteten Phasen-Segment)
2. **Brücken-Übungen** bei großen Lücken (zusätzliche Schritte, markiert `is_bridge`)
3. **LLM-QS** (Prompt `planning_exercise_path_qa`): Reihenfolge, Themen-Abdeckung, Empfehlungen
**API-Erweiterung** `progression-path-suggest`: `include_path_qa`, `include_llm_path_qa` · Response: `semantic_brief_summary`, `path_qa`.
**Pfad-Schritte:** Semantic Brief + Entwicklungsphase in **allen** Schritten (nicht nur Schritt 1).
### Phase E2 (0.8.187)
- **LLM-QS → Neuordnung:** `ordered_step_indices` im Prompt `planning_exercise_path_qa` (Migration **076**)
- **KI-Lückenfüller:** `planning_exercise_path_ai_fill.py``is_ai_proposal` wenn Bibliothek keine Brücke liefert
- Request: `include_path_reorder`, `include_ai_gap_fill`
---
## 23. Phase E3 (0.8.203) ✅
- Off-Topic aus Pfad entfernen; `gap_fill_offers` mit `goal_for_ai`; voller KI-Call im UI (kein Pre-Vorschlag)
- Migration **077** `suggested_new_exercises` im Pfad-QS-Prompt
---
## 24. Phase F — Roadmap-first Progressionsgraph (0.8.204217) ✅
**Entscheidung:** Progressionsgraph plant **vom Ziel rückwärts** (Roadmap → Stufenspezifikation → Bibliothek/KI). **Keine Gruppenanalyse** — die gehört zur Trainingsplanung.
**Ist-Stand (vollständig):** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`
**Spec:** `working/PLANNING_PROGRESSION_ROADMAP_SPEC.md` · **Roadmap:** `docs/architecture/PLANNING_KI_ROADMAP.md`
| Teil | Modul / API |
|------|-------------|
| Pipeline | `planning_progression_roadmap.py` (Workflow-lite) |
| Match | `planning_exercise_path_builder.py``roadmap_first`, `roadmap_override` |
| Skills | `planning_skill_expectations.py` — pro Stufe + Pfad |
| Gap-KI | `planning_exercise_form_context.py`, `planning_exercise_path_ai_fill.py` |
| Persistenz | `planning_roadmap` JSONB (Migration **088**) |
| API | `progression-path-suggest`, `PUT` Graph, `POST …/edges/sequence` |
| Prompts | **078/079/087** — Slugs nur in `ai_prompts` |
| UI | `ExerciseProgressionPathBuilder`, `ExerciseGapFillPrepModal` |
**Graph-Bias:** `progression_graph_id` bevorzugt **bestehende Nachfolger** ab Schritt 2 (Gewicht ~410 %), baut aber **keinen** Pfad aus vorhandenen Knoten — siehe Ist-Doku §5.
**Mitai Workflow-Engine:** bewusst **nicht** jetzt — Pipeline workflow-ready für spätere Anbindung.
---
## 25. Backlog (offen)
Siehe priorisierte Liste in **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`** §10:
1. UI-Wizard (Progressionsgraph) — separater Chat
2. Graph-Erweiterungsmodus (Start ab Knoten)
3. Trainingsplanung Phase G (Gruppenkontext, `planning_skill_expectations`)
4. Kontext auf allen Pfad-Schritten in der UI
5. Enrichment / Prompt-Feintuning
6. Mitai Workflow-Engine (langfristig)

View File

@ -0,0 +1,209 @@
# Planungs-KI — Progressions-Roadmap (Phase F)
**Version:** 0.1
**Datum:** 2026-06-07
**Status:** VERBINDLICHE ZIELARCHITEKTUR — **F0F9 umgesetzt** (0.8.217)
**Geltungsbereich:** **Progressionsgraph** (`exercise_progression_graphs`) — **ohne** Gruppenanalyse
**Ist-Stand (Module, API, Graph-Verhalten, Persistenz):** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`
**Bezüge:**
`working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md` · `working/AI_PLANNING_KI_MULTISTAGE_FORECAST.md` · `technical/AI_TRAINING_PLANNING_CONCEPT.md` · `technical/AI_PROMPT_TARGET_ARCHITECTURE.md` · `docs/architecture/PLANNING_KI_ROADMAP.md` · `docs/HANDOVER.md`
---
## 1. Entscheidung (2026-06-07)
### 1.1 Problem
Der Pfad-Builder (Phase C3/E) ist **retrieval-first**: Zieltext → N Übungen aus der Bibliothek → QS nachbessern. Das entspricht nicht der menschlichen Planung (Ziel → Roadmap → Stufenspezifikation → Übung).
### 1.2 Festlegung
| Thema | Entscheidung |
|--------|----------------|
| **Progressionsgraph** | **Roadmap-first** — Phasen A→B→C, dann Bibliothek (D), dann Feinausplanung (E) |
| **Gruppenanalyse** | **Nicht** in der Graphen-Pipeline — erst bei **Trainingsplanung** (Einheit/Rahmen) |
| **Mitai Workflow-Engine** | **Nicht** jetzt portieren — **Workflow-lite** (`PlanningProgressionPipeline`), später workflow-ready |
| **Ein Mega-Prompt** | **Verboten** — validierte Artefakte pro Phase |
### 1.3 Abgrenzung Trainingsplanung
```
Progressionsgraph-Pipeline Trainingsplanungs-Pipeline (später)
───────────────────────── ───────────────────────────────────
Ziel + N Major Steps Gruppe + Historie + Termin + Rahmen
Kein Gruppenkontext Kontext-Pack S0 (AI_PLANNING_KI_MULTISTAGE_FORECAST)
Curriculum / Technikpfad Session-Füllung / Reihenfolge / Zeiten
```
---
## 2. Menschliches Vorbild → Phasen
| Mensch | Phase | Output-Artefakt | LLM |
|--------|-------|-----------------|-----|
| Startpunkt + Zielzustand | **A** Zielanalyse | `goal_analysis` | Optional (klein) |
| Zwischenziele, gewichten, auf N reduzieren | **B** Roadmap | `roadmap` (`micro_objectives[]`, `major_steps[N]`) | Ja |
| Belastung, Übungstyp, Lernziel je Stufe | **C** Stufenspezifikation | `stage_specs[]` | Teilweise |
| Bibliothek / Brücke | **D** Match | `step_matches[]` oder `gaps[]` | Nein (Retrieval) |
| Skizze + Feinplan | **E** Übungsentwurf | bestehend `suggestExerciseAi` | On-demand |
**Phase B** = Kern: 812 `micro_objectives` → Konsolidierung → exakt `max_steps` `major_steps`.
---
## 3. Pipeline-Orchestrator (Workflow-lite)
Modul: **`backend/planning_progression_roadmap.py`**
```python
ctx = ProgressionRoadmapContext(goal_query=..., max_steps=N, semantic_brief=...)
ctx = phase_a_goal_analysis(ctx) # deterministisch + optional LLM
ctx = phase_b_roadmap(ctx) # micro → major
ctx = phase_c_stage_specs(ctx) # je major_step
# Phase D/E: bestehende path_builder / retrieval / ai_fill — speisen von ctx.major_steps
```
Jede Phase: `(ctx) → ctx`, Zwischenergebnisse in API-Response für **Human-in-the-loop** (Roadmap-Review vor Übungs-Match).
**Später:** jede Phase = Workflow-Knoten (Mitai-kompatibel), keine API-Änderung an Artefakten.
---
## 4. JSON-Artefakte (Pydantic)
### 4.1 `goal_analysis` (Phase A)
```json
{
"primary_topic": "Mae Geri",
"start_assumption": "Grundkenntnisse der Standführung, keine Perfektion",
"target_state": "Sicherer, präziser Mae Geri unter Belastung und in Anwendung",
"success_criteria": ["saubere Kammerhaltung", "Hüftführung", "Kime am Zielpunkt"],
"constraints": { "partner_required": false, "equipment": [] }
}
```
### 4.2 `roadmap` (Phase B)
```json
{
"micro_objectives": [
{ "id": "m1", "phase": "grundlage", "title": "Stellung und Kammerhaltung", "weight": 0.9, "depends_on": [] },
{ "id": "m2", "phase": "vertiefung", "title": "Hüft- und Kniekoordination", "weight": 0.85, "depends_on": ["m1"] }
],
"major_steps": [
{
"index": 0,
"phase": "grundlage",
"learning_goal": "Stabile Mae-Geri-Grundstellung",
"consolidates": ["m1"],
"rationale": "Einstieg ohne Perfektionsdruck"
}
],
"consolidation_notes": ["Perfektion mit Anwendung zusammengeführt"]
}
```
### 4.3 `stage_spec` (Phase C, je Major Step)
```json
{
"major_step_index": 2,
"learning_goal": "…",
"load_profile": ["präzision", "koordination"],
"exercise_type": "kihon_einzel",
"success_criteria": ["…"],
"anti_patterns": ["reine Kraftübung ohne Technikbezug"]
}
```
---
## 5. API (schrittweise)
### 5.1 Erweiterung `POST /api/planning/progression-path-suggest`
| Feld (neu) | Default | Bedeutung |
|------------|---------|-----------|
| `roadmap_first` | `false` → später `true` | Roadmap-Pipeline vor Retrieval |
| `include_roadmap_preview` | `true` wenn `roadmap_first` | Artefakte A/B/C in Response |
**Response (neu):**
```json
{
"progression_roadmap": {
"goal_analysis": { },
"roadmap": { },
"stage_specs": [ ],
"pipeline_phase": "roadmap_v1"
},
"steps": [ ]
}
```
**Übergangsphase (0.8.204):** `include_roadmap_preview=true` liefert Roadmap **parallel** zum bestehenden retrieval-first Pfad — UI kann Roadmap reviewen, Schritte bleiben vorerst retrieval-basiert.
**Zielphase (F2):** `roadmap_first=true` — Retrieval pro Major Step aus `stage_specs`, nicht mehr iterativ „beste nächste Übung“.
### 5.2 Prompt-Slugs — nur in `ai_prompts`, nie im Code
**Regel:** Prompt-**Texte** leben ausschließlich in der Tabelle `ai_prompts` (Superadmin bearbeitbar, Vorschau, `openrouter_model` pro Zeile). Python referenziert nur **Slugs** (`PROMPT_SLUG_*` in `planning_progression_roadmap.py`). Kein verstecktes Hardcoding von Templates.
| Slug | Phase | Migration |
|------|-------|-----------|
| `planning_progression_start_target` | Start/Ziel | **087** |
| `planning_progression_goal_analysis` | A | **078** |
| `planning_progression_roadmap` | B | **078** |
| `planning_progression_stage_spec` | C | **079** |
**API:** `include_llm_roadmap` (Default `true`) — lädt Prompts via `load_and_render_ai_prompt`. Bei Fehler/kein OpenRouter: **deterministischer Fallback** (kein stilles Versagen).
**Response:** `prompt_slugs` (genutzte Slugs), `prompt_slug_catalog` (Referenz), `llm_*_applied` Flags.
**Admin:** Templates unter Kategorie `training` pflegen — siehe `AI_PROMPT_SYSTEM_SPEC.md`.
---
## 6. UI-Roadmap
1. **F1:** Roadmap-Box unter Ziel-Eingabe (Major Steps als Karten, editierbar) — vor Übungsliste
2. **F2:** Match-Ergebnis pro Major Step (Bibliothek / Lücke / KI anlegen)
3. **F3:** `roadmap_first` als Default im Graph-Builder
---
## 7. Was bewusst nicht in Phase F
- Gruppen-Historie, Belastungssteuerung der Gruppe
- Mitai `workflow_engine` Port
- Vollautomatisches Speichern ohne Trainer-Review
---
## 8. Implementierungsstände
| ID | Inhalt | Status |
|----|--------|--------|
| **F0** | Spec + Doku + `planning_progression_roadmap.py` Scaffold | ✅ 0.8.204 |
| **F1** | `include_roadmap_preview` in API + deterministische A/B | ✅ 0.8.204 |
| **F2** | LLM Phase A/B/C über `ai_prompts` (078/079), `include_llm_roadmap` | ✅ 0.8.205 |
| **F3** | Retrieval aus `stage_specs` (roadmap_first) | ✅ 0.8.206209 |
| **F4** | UI Roadmap-Review + `roadmap_override` | ✅ 0.8.207 |
| **F5** | Start/Ziel strukturiert + Prompt **087** + Zwei-Schritt-UI | ✅ 0.8.210214 |
| **F6** | Gap-Prep + `planning_context` an Übungs-KI | ✅ 0.8.212214 |
| **F7** | `planning_skill_expectations` | ✅ 0.8.215216 |
| **F8** | Editierbare `stage_specs` in UI | ✅ 0.8.216 |
| **F9** | `planning_roadmap` JSONB (Migration **088**) | ✅ 0.8.217 |
| **G** | Trainingsplanung: eigene Pipeline + Workflow-Engine | 🔲 |
Details: `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`
---
## 9. Changelog
- **2026-05-22:** Ist-Stand F5F9 dokumentiert; Verweis auf `PLANNING_PROGRESSION_GRAPH_KI.md`.
- **2026-06-07:** Erstfassung — Roadmap-first Entscheidung, Abgrenzung Graphen vs. Planung, Workflow-lite.

View File

@ -0,0 +1,101 @@
# Progressionsgraph — Slot-Editor (Phase B + F15)
**Stand:** 2026-05-22 · **Status:** Umgesetzt (F14 + F15 lokal nach 0.8.233)
## Ziel
Ein Progressionsgraph = **ein linearer Hauptpfad** (Roadmap = strukturgebend). Jeder **Major Step** ist ein **Slot** mit:
- **primary** — Hauptübung des Slots (Pfadknoten)
- **siblings** — 0..n Schwestern (gleiche Stufe, `edge_type: sibling`)
KI-Entwürfe und Bibliotheksübungen leben **im selben Slot-Modell**, ohne sofortige Übungsanlage.
## Slot-Zustände (`kind`)
| kind | Bedeutung |
|------|-----------|
| `empty` | Noch keine Übung |
| `library` | `exercise_id` (+ optional `variant_id`) |
| `proposal` | KI-Entwurf (`ai_suggestion`, kein `exercise_id`) |
## Kanten
- `primary(n) → primary(n+1)``next_exercise` (nur befüllte Primärkette, lückenlos verbunden)
- `primary ↔ sibling``sibling` (pro Slot)
Leere Slots in der Roadmap sind erlaubt; Kanten nur zwischen aufeinanderfolgenden befüllten Primär-Slots.
## Editor-Zustand (`ProgressionGraphDraft`)
```ts
{
goalQuery, startSituation, targetState, roadmapNotes, maxSteps,
majorSteps: MajorStep[],
slots: Slot[], // index = major_step_index
pathSkillExpectations?,
lastFindings?, // path_qa-Snapshot
findingsStale?: boolean, // Bewertung veraltet (↔ Artefakt findings_stale)
dirty: boolean,
}
```
**Hydration:** `planning_roadmap` + Kanten → Slots; `slot_contents[]` für Entwürfe; Primärkette aus `next_exercise`.
**Speichern:** Batch-Delete bestehender Pfad-/Schwester-Kanten → `edges/sequence` (Primärkette) → einzelne `sibling`-Kanten → `PUT`/`sequence` mit Artefakt inkl. `slot_contents`, `last_findings`, **`findings_stale`**.
## Findings-Panel
Nutzt `path_qa`:
| Feld | Bedeutung |
|------|-----------|
| `quality_score` | Gesamt = **min(`roadmap_qa`, `assignment_qa`)** |
| `roadmap_qa` | Stufen/Roadmap (LLM `topic_coverage`, …) |
| `assignment_qa` | Slot-Befüllung (`empty_slot_count`, …) |
| `overall_ok`, `issues`, `recommendations`, `gap_fill_offers`, … | wie bisher |
**API:** `POST /api/planning/progression-path-suggest` mit `evaluate_only: true` und `evaluate_steps[]` — QA ohne Re-Match.
**Bewertung veraltet:** Jede Graph-Änderung setzt `findingsStale: true` → Banner im Panel. Nach „Graph bewerten“ → `false`. Persistenz: `planning_roadmap.findings_stale`.
## Match-Flow („Übungen matchen“)
1. **Schritt 1:** `evaluate_only` + volle Pfad-QS (wie „Graph bewerten“)
2. **Schritt 2:** `unified_slot_review: true`**`ProgressionOptimizeCompareModal`**
3. Pro Slot: aktuell vs. beste Bibliothek vs. optional KI-Vorschlag
4. **Vorauswahl:** Bibliothek nur wenn Stufen-Fit ≥ 50 % und besser als Baseline; sonst KI (bei leerem/schwachem Slot)
5. **Übernahme:** nur gewählte Slots speichern — **keine** automatische Nach-Bewertung
## Artefakt-Erweiterung (`GraphPlanningRoadmapArtifact`)
Optional:
- `slot_contents[]``{ major_step_index, primary, siblings[] }`
- `last_findings` — letzter `path_qa`-Snapshot
- **`findings_stale`** — bool, Bewertung bezieht sich nicht mehr auf aktuellen Graph-Stand
## UI (konsolidiert)
- **Eine Oberfläche:** `ExerciseProgressionGraphPanel` embeddet `ProgressionGraphEditor` (Slots + Findings)
- Kein separater Slot-Editor, kein 4-Schritt-KI-Wizard, kein `ProgressionChainEditor` im Panel
- Route `/progression-graphs/:id` → Redirect nach `/exercises` (Deep-Link wählt Graph)
- **Slot-Keys:** stabil `slot-{index}` (nicht Lernziel-Text) — sonst Fokusverlust beim Tippen
## Ersetzt (Legacy, nicht mehr im Panel)
- `ExerciseProgressionPathBuilder` · `ProgressionChainEditor` — Code bleibt vorerst, nicht eingebunden
## Implementierungsreihenfolge
| ID | Inhalt | Status |
|----|--------|--------|
| B.0 | Draft + Laden/Speichern Slots ↔ Kanten | ✅ |
| B.1 | Slot-Karten, Bibliothek + Entwurf | ✅ |
| B.2 | Findings-Panel + `evaluate_only` | ✅ |
| B.3 | Entwürfe im Artefakt + „Übung anlegen“ | ✅ |
| B.4 | Route + Panel vereinfachen | ✅ |
| B.5 | `last_findings` + Phase-C-Vorbereitung | ✅ |
| F15 | Unified Slot-Review, getrennte QS, `findings_stale` | ✅ |
**Ist-Doku:** `docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md` §8.1 · `docs/HANDOVER.md` §2.8 F15

View File

@ -2,7 +2,7 @@
**Bezug:** `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (Kopf „V3“, inkl. **§10.2.1**, **§10.4 Coaching-Stufen**, **Anhang A** Implementierungsabgleich — Drift-Schutz) **Bezug:** `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (Kopf „V3“, inkl. **§10.2.1**, **§10.4 Coaching-Stufen**, **Anhang A** Implementierungsabgleich — Drift-Schutz)
**Technische Entwurfsspezifikation:** `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` **Technische Entwurfsspezifikation:** `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md`
**Stand dieses Dokuments:** 2026-05-12 (Abgleich mit Code App **0.8.110**, siehe `backend/version.py`) **Stand dieses Dokuments:** 2026-05-20 (Abgleich mit Code, siehe `backend/version.py`)
## Ziele ## Ziele
@ -12,7 +12,7 @@ Umsetzung der MVP-Punkte aus der Fachspezifikation ohne die bestehende Planung z
| Phase | Inhalt | Status | | Phase | Inhalt | Status |
|-------|--------|--------| |-------|--------|--------|
| **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“ | **umgesetzt (MVP Schritt 1)** | | **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“; **Ergänzung 2026-05-20:** Fähigkeiten-Profil + Listen-Filter (Peer-Vergleich nur unter Modulen) — `technical/SKILL_SCORING_SPEC.md` | **umgesetzt (MVP Schritt 1)** |
| **2** | Kombinationsübungen: `exercise_kind`/`combination_*`, Slots, Pools, `method_archetype`, `method_profile` (JSON) | **teilweise** — wie links; zusätzlich **057** `planning_method_profile`; Planungs-Merge Client (`effectiveComboMethodProfile`); Archetypen weiterhin **nur Code-Konstanten** (kein Admin) | **Offen:** Archetyp-Admin-UI; Profil↔Archetyp-Validierung Backend; „alle Slots vorbelegen“ / Presets (siehe Fachspez **§10.6**); Haupt-/Nebenmethoden an Kombi wo Spec es verlangt | | **2** | Kombinationsübungen: `exercise_kind`/`combination_*`, Slots, Pools, `method_archetype`, `method_profile` (JSON) | **teilweise** — wie links; zusätzlich **057** `planning_method_profile`; Planungs-Merge Client (`effectiveComboMethodProfile`); Archetypen weiterhin **nur Code-Konstanten** (kein Admin) | **Offen:** Archetyp-Admin-UI; Profil↔Archetyp-Validierung Backend; „alle Slots vorbelegen“ / Presets (siehe Fachspez **§10.6**); Haupt-/Nebenmethoden an Kombi wo Spec es verlangt |
| **3** | Planungsblöcke: Gruppierung, Auflösen, „als Modul speichern“, erweiterter Übernahmemodus (Zwischenposition) | geplant | | **3** | Planungsblöcke: Gruppierung, Auflösen, „als Modul speichern“, erweiterter Übernahmemodus (Zwischenposition) | geplant |
| **4** | Coaching: Archetyp-Support | **teilweise:** **Stufe A** — Merge Katalog+Planung; `CombinationPlanBracket` in Peek/Run; globale Profilzahlen mit Labels (`describeGlobalComboProfile`); Stations-/Timing-Zusammenfassung inkl. Wdh.-Hinweise. **Stufe B/C****offen**10.6, Anhang A) | | **4** | Coaching: Archetyp-Support | **teilweise:** **Stufe A** — Merge Katalog+Planung; `CombinationPlanBracket` in Peek/Run; globale Profilzahlen mit Labels (`describeGlobalComboProfile`); Stations-/Timing-Zusammenfassung inkl. Wdh.-Hinweise. **Stufe B/C****offen**10.6, Anhang A) |

View File

@ -35,6 +35,16 @@ DB_PASSWORD=CHANGE_ME_SECURE_PASSWORD
OPENROUTER_API_KEY=your_api_key_here OPENROUTER_API_KEY=your_api_key_here
OPENROUTER_MODEL=anthropic/claude-sonnet-4 OPENROUTER_MODEL=anthropic/claude-sonnet-4
# Vereins-Kontingente hart blockieren (KI-Kosten!). Nur 1, true oder yes aktivieren.
# Nach Änderung: docker compose -f docker-compose.dev-env.yml up -d backend
CLUB_FEATURE_ENFORCE=1
# Standard-OpenRouter-Modell (alle Aufrufe). Optional pro Prompt in ai_prompts.openrouter_model
# ueberschreibbar (Migration 070, Superadmin unter „KI Prompts“).
# Übungs-KI (Docker): ohne Eintrag im compose „environment:“ landet keine .env-Zeile im Container.
# Hier ist SHINKAN_AI_DEBUG in docker-compose*.yml angebunden — 1 = ausführliche WARN-Logs (exercise_ai, openrouter).
# SHINKAN_AI_DEBUG=1
SMTP_HOST=smtp.example.com SMTP_HOST=smtp.example.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_USER=noreply@jinkendo.de SMTP_USER=noreply@jinkendo.de

View File

@ -18,6 +18,11 @@ jobs:
docker compose -f docker-compose.dev-env.yml build --no-cache docker compose -f docker-compose.dev-env.yml build --no-cache
docker compose -f docker-compose.dev-env.yml up -d docker compose -f docker-compose.dev-env.yml up -d
sleep 5 sleep 5
curl -sf http://localhost:8098/api/version && echo "✓ DEV API healthy" if ! curl -sf http://localhost:8098/api/version; then
echo "✗ DEV API nicht erreichbar — Backend-Logs (Migration/Startup):"
docker compose -f docker-compose.dev-env.yml logs backend --tail 120 || true
exit 1
fi
echo "✓ DEV API healthy"
curl -sf http://localhost:3098/api/version && echo "✓ DEV über Frontend-Nginx (wie Browser) healthy" curl -sf http://localhost:3098/api/version && echo "✓ DEV über Frontend-Nginx (wie Browser) healthy"
echo "=== Shinkan DEV Deploy complete ===" echo "=== Shinkan DEV Deploy complete ==="

View File

@ -1,10 +1,13 @@
name: Test Suite name: Test Suite
# develop: push/PR → Tests gegen Dev (parallel oder vor Deploy Development).
# main: kein push/PR-Trigger — vermeidet doppelten Dev-Lauf beim Merge develop→main;
# Prod-Tests nur via workflow_run nach erfolgreichem Deploy Production.
on: on:
push: push:
branches: [main, develop] branches: [develop]
pull_request: pull_request:
branches: [main, develop] branches: [develop]
workflow_run: workflow_run:
workflows: ["Deploy Development", "Deploy Production"] workflows: ["Deploy Development", "Deploy Production"]
types: [completed] types: [completed]
@ -17,8 +20,10 @@ jobs:
steps: steps:
- name: Backend pytest im deployten Container - name: Backend pytest im deployten Container
run: | run: |
set -e
EVENT_NAME="${{ github.event_name }}" EVENT_NAME="${{ github.event_name }}"
REF_NAME="${{ github.ref_name }}" REF_NAME="${{ github.ref_name }}"
BASE_REF="${{ github.base_ref }}"
RUN_WORKFLOW="${{ github.event.workflow_run.name }}" RUN_WORKFLOW="${{ github.event.workflow_run.name }}"
APP_DIR="/home/lars/docker/shinkan" APP_DIR="/home/lars/docker/shinkan"
COMPOSE_FILE="docker-compose.yml" COMPOSE_FILE="docker-compose.yml"
@ -28,12 +33,27 @@ jobs:
APP_DIR="/home/lars/docker/shinkan-dev" APP_DIR="/home/lars/docker/shinkan-dev"
COMPOSE_FILE="docker-compose.dev-env.yml" COMPOSE_FILE="docker-compose.dev-env.yml"
fi fi
elif [ "$REF_NAME" = "develop" ]; then elif [ "$REF_NAME" = "develop" ] || [ "$BASE_REF" = "develop" ]; then
APP_DIR="/home/lars/docker/shinkan-dev" APP_DIR="/home/lars/docker/shinkan-dev"
COMPOSE_FILE="docker-compose.dev-env.yml" COMPOSE_FILE="docker-compose.dev-env.yml"
fi fi
cd "$APP_DIR" cd "$APP_DIR"
echo "Warte auf stabilen backend-Container …"
for i in $(seq 1 60); do
if docker compose -f "$COMPOSE_FILE" exec -T backend true 2>/dev/null; then
echo "Backend bereit (Versuch $i)"
break
fi
if [ "$i" -eq 60 ]; then
echo "Timeout: backend-Container nicht bereit"
docker compose -f "$COMPOSE_FILE" ps || true
docker compose -f "$COMPOSE_FILE" logs backend --tail 80 || true
exit 1
fi
sleep 5
done
docker compose -f "$COMPOSE_FILE" exec -T backend sh -lc " docker compose -f "$COMPOSE_FILE" exec -T backend sh -lc "
pip install -r /app/requirements-dev.txt && pip install -r /app/requirements-dev.txt &&
cd /app && cd /app &&

View File

@ -12,10 +12,13 @@
> | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` | > | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` |
> | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` | > | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` |
> | Medien-Archiv, Lifecycle, Inline (Plan §11) | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | > | Medien-Archiv, Lifecycle, Inline (Plan §11) | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
> | Fähigkeiten-Scoring (Planungs-Bausteine) | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
> | Handover / nächste Session | **`docs/HANDOVER.md`** | > | Handover / nächste Session | **`docs/HANDOVER.md`** |
> | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** | > | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
> | Architektur-Zielbild, Refaktor-Roadmap, verbindliche Shinkan-Regeln | **`docs/architecture/README.md`** | > | Architektur-Zielbild, Refaktor-Roadmap, verbindliche Shinkan-Regeln | **`docs/architecture/README.md`** |
> | Performance-Baseline (Phase 0) | **`docs/architecture/BASELINE_SNAPSHOT.md`** | > | Performance-Baseline (Phase 0) | **`docs/architecture/BASELINE_SNAPSHOT.md`** |
> | KI-Prompt-System — Zielarchitektur | `.claude/docs/technical/AI_PROMPT_TARGET_ARCHITECTURE.md` |
> | Planungs-KI Progressionsgraph (Ist-Stand) | **`docs/architecture/PLANNING_PROGRESSION_GRAPH_KI.md`** · Spec **`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md`** · Roadmap **`docs/architecture/PLANNING_KI_ROADMAP.md`** |
## Projekt-Übersicht ## Projekt-Übersicht
@ -86,10 +89,11 @@ frontend/src/
**Siehe:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, `MODULE_VERSIONS`) und `.claude/docs/PROJECT_STATUS.md`. **Siehe:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, `MODULE_VERSIONS`) und `.claude/docs/PROJECT_STATUS.md`.
Kurz (Stand 2026-05-12): App **0.8.96**, DBSchemaVersion siehe **`backend/version.py`**; Kern: Übungen, Varianten, **Medien-Archiv & Bibliothek (`/media`)**, **Inline-Medien im Rich-Text**, **Inhaltsmeldungen (P-13)** im Posteingang, Mandanten-Sync aktiver Verein, Planung mit Sektionen, **Trainingsrahmen Bibliothek + SlotBlueprint** (036037), Progressionsgraph, Reifegrad/MatrixStack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, Nutzerüberblick **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (Abschnitt 11 umgesetzt). Kurz (Stand 2026-05-14): App- und DB-Version siehe **`backend/version.py`**; Kern: Übungen, Varianten, **Medien-Archiv & Bibliothek (`/media`)**, **Inline-Medien im Rich-Text**, **Inhaltsmeldungen (P-13)** im Posteingang, Mandanten-Sync aktiver Verein, Planung mit **Phasen & parallelen Streams (Breakout, 063)**, **Trainingsrahmen Bibliothek + SlotBlueprint** (036037), Progressionsgraph, Reifegrad/MatrixStack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, Nutzerüberblick **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**, `PARALLEL_TRAINING_STREAMS_SPEC.md`, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (Abschnitt 11 umgesetzt).
### Log (Auszug) ### Log (Auszug)
- 2026-05-20: **Fähigkeiten-Scoring Phase 3** — gewichtete Profile für Module/Rahmen/Pfade; Peer-Vergleich getrennt nach Artefakttyp; Listen-Filter + Discovery — siehe `SKILL_SCORING_SPEC.md`, `docs/HANDOVER.md` §2.6, `FEATURES_DELIVERED_2026-Q2.md` §15.
- 2026-05-07: **Medien** — zentrales Archiv (`media_assets`), Bibliothek-UI, Lifecycle/Papierkorb, `from-asset`, Speicherpfade `library/…`, Governance `official`/Copyright; **0.8.59** aktiver Verein UI/API-Sync — siehe `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12, `docs/HANDOVER.md`. - 2026-05-07: **Medien** — zentrales Archiv (`media_assets`), Bibliothek-UI, Lifecycle/Papierkorb, `from-asset`, Speicherpfade `library/…`, Governance `official`/Copyright; **0.8.59** aktiver Verein UI/API-Sync — siehe `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12, `docs/HANDOVER.md`.
- 2026-05-05: Rahmen nur Bibliothek (**036**), SlotAblauf = `training_units` + Sektionen (**037**), `POST /api/training-units/from-framework-slot`, keine `training_framework_slot_exercises` mehr — siehe `DATABASE_SCHEMA.md` / `FEATURES_DELIVERED_2026-Q2.md`. - 2026-05-05: Rahmen nur Bibliothek (**036**), SlotAblauf = `training_units` + Sektionen (**037**), `POST /api/training-units/from-framework-slot`, keine `training_framework_slot_exercises` mehr — siehe `DATABASE_SCHEMA.md` / `FEATURES_DELIVERED_2026-Q2.md`.
- 2026-04-27: Übungsvarianten API/UI, Migration 030, Listen-UX-Suche, Admin-Upload-Limits — siehe `PROJECT_STATUS.md` und `docs/library/FEATURES_DELIVERED_2026-Q2.md`. - 2026-04-27: Übungsvarianten API/UI, Migration 030, Listen-UX-Suche, Admin-Upload-Limits — siehe `PROJECT_STATUS.md` und `docs/library/FEATURES_DELIVERED_2026-Q2.md`.

View File

@ -2,14 +2,16 @@ FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# Install system dependencies # Install system dependencies (tzdata für zoneinfo/ZoneInfo unter Linux)
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
postgresql-client \ postgresql-client \
tzdata \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy requirements and install dependencies # Copy requirements and install dependencies
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt ENV PIP_DEFAULT_TIMEOUT=120
RUN pip install --no-cache-dir --retries 5 -r requirements.txt
# Copy application code # Copy application code
COPY . . COPY . .

View File

@ -0,0 +1,77 @@
"""
Account-Lifecycle (CAPABILITY_CATALOG.v1.md §3, M3 C0).
Zustände: unverified verified_pending_club active_member; platform_admin separat.
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Optional
from fastapi import HTTPException
from club_tenancy import is_platform_admin
if TYPE_CHECKING:
from tenant_context import TenantContext
_ACCOUNT_STATE_RANK = {
"unverified": 1,
"verified_pending_club": 2,
"active_member": 3,
"platform_admin": 4,
}
def resolve_account_state(
*,
email_verified: bool,
global_role: str,
has_active_membership: bool,
) -> str:
"""Ermittelt account_state für ein Profil."""
if is_platform_admin(global_role):
return "platform_admin"
if not email_verified:
return "unverified"
if not has_active_membership:
return "verified_pending_club"
return "active_member"
def account_state_satisfies(current: str, required: str) -> bool:
"""True wenn current mindestens required ist."""
cur = _ACCOUNT_STATE_RANK.get(current, 0)
req = _ACCOUNT_STATE_RANK.get(required, 99)
if current == "platform_admin":
return True
return cur >= req
def account_gate_enforcement_enabled() -> bool:
"""Account-Gates aktiv (Default an — nur wenige Endpoints in M3)."""
return os.getenv("ACCOUNT_GATE_ENFORCE", "1").strip() == "1"
def assert_min_account_state(
tenant: "TenantContext",
min_state: str,
*,
endpoint: Optional[str] = None,
) -> None:
"""
Prüft Mindest-Account-Status. Wirft 403 wenn ACCOUNT_GATE_ENFORCE=1 (Default).
"""
current = getattr(tenant, "account_state", "active_member")
ok = account_state_satisfies(current, min_state)
if ok:
return
if not account_gate_enforcement_enabled():
return
detail = (
f"Account-Status „{current}“ reicht nicht für diese Aktion "
f"(erforderlich: {min_state})."
)
if endpoint:
detail = f"{detail} ({endpoint})"
raise HTTPException(status_code=403, detail=detail)

View File

@ -0,0 +1,178 @@
"""
API-Gates für Onboarding (Phase A MEMBERSHIP_RBAC_DECISIONS_2026-06.md §1.1).
Blockiert Domänen-APIs für unverified / verified_pending_club vor dem Router.
"""
from __future__ import annotations
import os
import re
from typing import Optional, Tuple
from account_lifecycle import resolve_account_state
from club_tenancy import memberships_with_roles
# Öffentlich ohne Session
PUBLIC_API_PREFIXES = (
"/api/auth/login",
"/api/auth/register",
"/api/auth/forgot-password",
"/api/auth/reset-password",
"/api/auth/verify/",
"/api/legal-documents/",
"/api/clubs/public-directory",
"/api/version",
"/api/health/",
"/health",
)
# Mit Session, unabhängig vom account_state (Logout, Profil lesen, …)
AUTH_INFRA_PREFIXES = (
"/api/auth/logout",
"/api/auth/me",
"/api/auth/status",
"/api/auth/pin",
"/api/auth/resend-verification",
"/api/profiles/me",
"/api/me/entitlements",
)
# Zusätzlich für verified_pending_club (Verein bewerben)
PENDING_CLUB_PREFIXES = (
"/api/me/club-join-requests",
"/api/me/club-creation-requests",
)
_PROFILE_MUTATION_RE = re.compile(r"^/api/profiles/(\d+)$")
def api_onboarding_gate_enabled() -> bool:
"""Produktions-Gate aktiv (ACCOUNT_GATE_API_ENFORCE=0 zum Abschalten)."""
return os.getenv("ACCOUNT_GATE_API_ENFORCE", "1").strip() == "1"
def _middleware_db_lookup_enabled() -> bool:
"""
Middleware-Session-Lookup nur mit echter DB (nicht in pytest TestClient ohne Postgres).
"""
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() in ("1", "true", "yes"):
return False
if os.getenv("PYTEST_CURRENT_TEST"):
return False
return True
def normalize_api_path(path: str) -> str:
p = (path or "").split("?", 1)[0].strip()
if not p.startswith("/"):
p = "/" + p
if len(p) > 1 and p.endswith("/"):
p = p[:-1]
return p
def is_public_api_path(path: str) -> bool:
p = normalize_api_path(path)
return any(p == pref or p.startswith(pref) for pref in PUBLIC_API_PREFIXES)
def _path_allowed_for_state(path: str, method: str, account_state: str, profile_id: int) -> bool:
p = normalize_api_path(path)
m = (method or "GET").upper()
for pref in AUTH_INFRA_PREFIXES:
if p == pref or p.startswith(pref + "/"):
return True
match = _PROFILE_MUTATION_RE.match(p)
if match and m in ("PUT", "PATCH") and int(match.group(1)) == int(profile_id):
return True
if account_state == "unverified":
return False
if account_state == "verified_pending_club":
for pref in PENDING_CLUB_PREFIXES:
if p == pref or p.startswith(pref + "/"):
return True
return False
return True
def resolve_account_state_for_token(cur, session_row: dict) -> str:
profile_id = int(session_row["profile_id"])
role = (session_row.get("role") or "").lower()
cur.execute(
"SELECT COALESCE(email_verified, false) AS email_verified FROM profiles WHERE id = %s",
(profile_id,),
)
prof = cur.fetchone()
email_verified = bool(prof.get("email_verified")) if prof else False
memberships = memberships_with_roles(cur, profile_id, active_only=True)
has_active = len(memberships) > 0
return resolve_account_state(
email_verified=email_verified,
global_role=role,
has_active_membership=has_active,
)
def check_api_onboarding_gate(
*,
path: str,
method: str,
profile_id: int,
account_state: str,
) -> Tuple[bool, Optional[str]]:
"""
Returns (allowed, reason).
active_member / platform_admin immer erlaubt (Domain).
"""
if not api_onboarding_gate_enabled():
return True, None
if account_state in ("active_member", "platform_admin"):
return True, None
if _path_allowed_for_state(path, method, account_state, profile_id):
return True, None
return False, f"account_state_{account_state}"
def evaluate_request_gate(token: Optional[str], path: str, method: str) -> Tuple[bool, Optional[str], Optional[str]]:
"""
Vollständige Prüfung inkl. Session-Lookup.
Returns: allowed, reason, account_state (für Logging)
"""
if not api_onboarding_gate_enabled() or not _middleware_db_lookup_enabled():
return True, None, None
p = normalize_api_path(path)
if not p.startswith("/api/"):
return True, None, None
if is_public_api_path(p):
return True, None, None
if not token:
return True, None, None
from auth import get_session
from db import get_db, get_cursor
session = get_session(token)
if not session:
return True, None, None
profile_id = int(session["profile_id"])
with get_db() as conn:
cur = get_cursor(conn)
account_state = resolve_account_state_for_token(cur, session)
allowed, reason = check_api_onboarding_gate(
path=p,
method=method,
profile_id=profile_id,
account_state=account_state,
)
return allowed, reason, account_state

View File

@ -0,0 +1,108 @@
"""
Gemeinsame Pydantic-Modelle fuer Uebungs-KI-Kontext (Formularfelder Prompt-Platzhalter).
Keine Imports aus exercise_ai vermeidet Zirkelimporte mit ai_prompt_job / exercise_ai.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Sequence, Tuple
from pydantic import BaseModel, Field
class ExerciseFormAiFocusRow(BaseModel):
"""Fokusbereich fuer Skill-Retrieval (ai_skill_retrieval_profiles)."""
focus_area_id: int = Field(..., ge=1)
is_primary: Optional[bool] = False
class ExerciseFormAiPromptContext(BaseModel):
"""
Inhaltliche Eingabe fuer Uebungs-Prompts (Kurzfassung / Skills / Anleitung).
Wird genutzt von Admin-Prompt-Vorschau und POST /exercises/ai/suggest (via Mapping).
"""
title: Optional[str] = ""
goal: Optional[str] = None
execution: Optional[str] = None
preparation: Optional[str] = None
trainer_notes: Optional[str] = None
focus_hint: Optional[str] = None
focus_areas_context: Optional[List[ExerciseFormAiFocusRow]] = None
planning_context: Optional[Dict[str, Any]] = None
def focus_area_tuples(self) -> Optional[List[Tuple[int, bool]]]:
if not self.focus_areas_context:
return None
return [(int(x.focus_area_id), bool(x.is_primary)) for x in self.focus_areas_context]
def has_instruction_source_text(self) -> bool:
"""Mindestens ein Anleitungsfeld oder Titel fuer instruction_rewrite."""
if (self.title or "").strip():
return True
for val in (self.goal, self.execution, self.preparation, self.trainer_notes):
if val and str(val).strip():
return True
return False
@classmethod
def from_api_suggest(
cls,
*,
title: Optional[str] = None,
goal: Optional[str] = None,
execution: Optional[str] = None,
preparation: Optional[str] = None,
trainer_notes: Optional[str] = None,
focus_area_hint: Optional[str] = None,
focus_areas_context: Optional[Sequence[ExerciseFormAiFocusRow]] = None,
planning_context: Optional[Dict[str, Any]] = None,
) -> ExerciseFormAiPromptContext:
"""Mappt Felder aus POST /exercises/ai/suggest (focus_area_hint → focus_hint)."""
hint = (focus_area_hint or "").strip() or None
return cls(
title=(title or "").strip(),
goal=goal,
execution=execution,
preparation=preparation,
trainer_notes=trainer_notes,
focus_hint=hint,
focus_areas_context=list(focus_areas_context) if focus_areas_context else None,
planning_context=dict(planning_context) if planning_context else None,
)
@classmethod
def from_focus_tuples(
cls,
*,
title: str = "",
goal: Optional[str] = None,
execution: Optional[str] = None,
preparation: Optional[str] = None,
trainer_notes: Optional[str] = None,
focus_hint: Optional[str] = None,
focus_tuples: Optional[Sequence[Tuple[int, bool]]] = None,
) -> ExerciseFormAiPromptContext:
rows = None
if focus_tuples:
rows = [
ExerciseFormAiFocusRow(focus_area_id=int(fid), is_primary=bool(prim))
for fid, prim in focus_tuples
]
return cls(
title=(title or "").strip(),
goal=goal,
execution=execution,
preparation=preparation,
trainer_notes=trainer_notes,
focus_hint=(focus_hint or "").strip() or None,
focus_areas_context=rows,
)
__all__ = [
"ExerciseFormAiFocusRow",
"ExerciseFormAiPromptContext",
]

59
backend/ai_prompt_job.py Normal file
View File

@ -0,0 +1,59 @@
"""
KI-Prompt Jobs: Resolver + oeffentliche Fassade fuer Uebungs-KI-Aufrufe.
Importiert exercise_ai fuer Platzhalter-Builder und OpenRouter-Orchestrierung.
"""
from __future__ import annotations
from typing import Any, Dict
from ai_prompt_context import ExerciseFormAiFocusRow, ExerciseFormAiPromptContext
from exercise_ai import build_exercise_placeholder_variables
def resolve_exercise_form_variables(cur, slug: str, ctx: ExerciseFormAiPromptContext) -> Dict[str, str]:
"""Baut die Mustache-Map fuer exercise_summary / exercise_skill_suggestions."""
return build_exercise_placeholder_variables(
cur,
slug=slug,
title=(ctx.title or "").strip(),
goal=ctx.goal,
execution=ctx.execution,
focus_area_hint=ctx.focus_hint,
focus_areas_context=ctx.focus_area_tuples(),
preparation=ctx.preparation,
trainer_notes=ctx.trainer_notes,
planning_context=ctx.planning_context,
)
def run_exercise_form_ai_suggestion(
cur,
ctx: ExerciseFormAiPromptContext,
*,
want_summary: bool,
want_skills: bool,
want_instructions: bool = False,
) -> Dict[str, Any]:
"""
Fuehrt Uebungs-KI aus (OpenRouter) ein Einstieg fuer Router und kuenftige Jobs.
``ctx`` = Formularinhalt; ``want_*`` = welche Prompt-Slugs angefragt werden.
"""
from exercise_ai import run_exercise_ai_suggestion
return run_exercise_ai_suggestion(
cur,
form_ctx=ctx,
want_summary=want_summary,
want_skills=want_skills,
want_instructions=want_instructions,
)
__all__ = [
"ExerciseFormAiFocusRow",
"ExerciseFormAiPromptContext",
"resolve_exercise_form_variables",
"run_exercise_form_ai_suggestion",
]

View File

@ -0,0 +1,317 @@
"""
Admin-Vorschau: Platzhalter für Planungs-Prompts (Progressionsgraph, Pfad-QS, Suggest).
Nutzt repräsentative Beispieldaten + echte Katalog-Auszüge aus der DB.
"""
from __future__ import annotations
import json
from typing import Any, Dict, List, Mapping, Optional
from pydantic import BaseModel, Field
from planning_exercise_semantics import brief_to_summary_dict, build_semantic_brief
from planning_intent_context import build_planning_intent_context
from planning_prompt_variables import merge_planning_prompt_variables
PLANNING_PROMPT_SLUGS = frozenset(
{
"planning_progression_start_target",
"planning_progression_goal_analysis",
"planning_progression_roadmap",
"planning_progression_stage_spec",
"planning_exercise_query_semantics",
"planning_exercise_path_qa",
"planning_exercise_search_intent",
"planning_exercise_search_rank",
"planning_exercise_expectation_profile",
}
)
class PlanningPromptPreviewInput(BaseModel):
goal_query: str = Field(
default="Mae Geri vom Grundschritt bis zur kontrollierten Kumite-Nähe",
max_length=2000,
)
user_notes: str = Field(default="Fokus Breitensport, ohne Wettkampfdruck.", max_length=2000)
max_steps: int = Field(default=5, ge=2, le=10)
search_query: Optional[str] = Field(default=None, max_length=2000)
planning_catalog_context: Optional[Dict[str, Any]] = Field(default=None)
def is_planning_prompt_slug(slug: str) -> bool:
return (slug or "").strip().lower() in PLANNING_PROMPT_SLUGS
def _compact_json(obj: Any) -> str:
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def _sample_goal_analysis() -> Dict[str, Any]:
return {
"primary_topic": "Mae Geri",
"start_assumption": "Grundstellung und einfache Frontkick-Bewegung bekannt",
"target_state": "Kontrollierter Mae Geri in Kumite-Nähe mit Hüftöffnung",
"success_criteria": [
"Hüfte öffnet vor dem Kick",
"Ballen trifft Zielzone",
"Rückzug ohne Balanceverlust",
],
"constraints": {
"partner_required": False,
"excluded_themes": ["reine Kraft ohne Technikbezug"],
"trainer_notes": "Breitensport, kein Wettkampf",
},
}
def _sample_major_steps(max_steps: int) -> List[Dict[str, Any]]:
phases = ["einstieg", "grundlage", "vertiefung", "anwendung", "perfektion"]
titles = [
"Grundstellung und Mae Geri Einstieg",
"Hüftöffnung und Ballen-Fokus",
"Koordination und Rückzug",
"Anwendung in Partnerübung",
"Qualität unter leichtem Druck",
]
out: List[Dict[str, Any]] = []
for i in range(max_steps):
out.append(
{
"index": i,
"phase": phases[min(i, len(phases) - 1)],
"title": titles[min(i, len(titles) - 1)],
"learning_goal": titles[min(i, len(titles) - 1)],
}
)
return out
def _sample_path_steps() -> List[Dict[str, Any]]:
return [
{
"index": 1,
"exercise_id": 101,
"title": "Mae Geri — Stand und Hüftöffnung",
"goal": "Frontkick mit geöffneter Hüfte aus Grundstellung",
"is_bridge": False,
"is_ai_proposal": False,
"reasons": ["Stufen-Gate: Grundlagen"],
},
{
"index": 2,
"exercise_id": 102,
"title": "Mae Geri — Ballen und Rückzug",
"goal": "Präziser Ballentreffer mit kontrolliertem Rückzug",
"is_bridge": False,
"is_ai_proposal": False,
"reasons": ["Nachfolger im Graph"],
},
]
def _sample_planning_context() -> Dict[str, Any]:
return {
"scope": "progression_path",
"goal_query": "Mae Geri vom Grundschritt bis zur Kumite-Nähe",
"stage_index": 1,
"learning_goal": "Hüftöffnung und Ballen-Fokus",
}
def _sample_target_profile() -> Dict[str, Any]:
return {
"primary_focus": "Kihon",
"training_type": "Breitensport",
"skill_expectations": ["Geri Waza", "Koordination"],
}
def _sample_candidates() -> List[Dict[str, Any]]:
return [
{
"exercise_id": 101,
"title": "Mae Geri — Stand und Hüftöffnung",
"summary": "Frontkick mit Hüftöffnung",
"skill_names": ["Geri Waza"],
"score_hint": 0.82,
},
{
"exercise_id": 102,
"title": "Mae Geri — Ballen und Rückzug",
"summary": "Ballentreffer mit Rückzug",
"skill_names": ["Geri Waza", "Koordination"],
"score_hint": 0.76,
},
]
def _load_catalog_variables(cur) -> Dict[str, str]:
from planning_exercise_intent import (
_load_compact_catalog,
_load_skills_catalog_compact,
)
return {
"skills_catalog_json": _compact_json(_load_skills_catalog_compact(cur)),
"focus_areas_catalog_json": _compact_json(_load_compact_catalog(cur, "focus_areas", "id")),
"training_types_catalog_json": _compact_json(_load_compact_catalog(cur, "training_types", "id")),
"style_directions_catalog_json": _compact_json(_load_compact_catalog(cur, "style_directions", "id")),
"target_groups_catalog_json": _compact_json(_load_compact_catalog(cur, "target_groups", "id")),
}
def _preview_catalog_context(body: PlanningPromptPreviewInput):
from planning_catalog_context import catalog_context_from_mapping
raw = body.planning_catalog_context
if raw:
return catalog_context_from_mapping(raw)
return None
def _merge_catalog_preview(cur, slug: str, base: Dict[str, str], body: PlanningPromptPreviewInput) -> Dict[str, str]:
return merge_planning_prompt_variables(
cur,
base,
catalog=_preview_catalog_context(body),
slug=slug,
)
def resolve_planning_prompt_preview_variables(
cur,
slug: str,
body: PlanningPromptPreviewInput,
) -> Dict[str, str]:
"""Mustache-Variablen für Planungs-Prompt-Vorschau im Admin."""
s = (slug or "").strip().lower()
if s not in PLANNING_PROMPT_SLUGS:
raise ValueError(f"Kein Planungs-Prompt-Slug: {slug!r}")
goal_query = (body.goal_query or "").strip() or "Mae Geri Progression"
search_query = (body.search_query or "").strip() or goal_query
max_steps = int(body.max_steps)
brief = build_semantic_brief(goal_query)
brief_json = _compact_json(brief_to_summary_dict(brief))
goal_analysis = _sample_goal_analysis()
major_steps = _sample_major_steps(max_steps)
intent_ctx = build_planning_intent_context(
goal_query=goal_query,
goal_analysis=goal_analysis,
semantic_brief=brief,
extra_context=(body.user_notes or "").strip() or None,
)
intent_ctx_json = _compact_json(intent_ctx.to_api_dict())
ctx = _sample_planning_context()
target = _sample_target_profile()
catalogs = _load_catalog_variables(cur)
if s == "planning_progression_start_target":
return _merge_catalog_preview(
cur,
s,
{
"goal_query": goal_query,
"semantic_brief_json": brief_json,
"user_notes": (body.user_notes or "").strip(),
},
body,
)
if s == "planning_progression_goal_analysis":
return _merge_catalog_preview(
cur,
s,
{
"goal_query": goal_query,
"semantic_brief_json": brief_json,
},
body,
)
if s == "planning_progression_roadmap":
return _merge_catalog_preview(
cur,
s,
{
"goal_query": goal_query,
"goal_analysis_json": _compact_json(goal_analysis),
"semantic_brief_json": brief_json,
"max_steps": str(max_steps),
},
body,
)
if s == "planning_progression_stage_spec":
return _merge_catalog_preview(
cur,
s,
{
"goal_query": goal_query,
"goal_analysis_json": _compact_json(goal_analysis),
"major_steps_json": _compact_json(major_steps),
"intent_context_json": intent_ctx_json,
"semantic_brief_json": brief_json,
},
body,
)
if s == "planning_exercise_query_semantics":
return {
"search_query": search_query,
"semantic_brief_json": brief_json,
}
if s == "planning_exercise_path_qa":
return _merge_catalog_preview(
cur,
s,
{
"goal_query": goal_query,
"semantic_brief_json": brief_json,
"steps_json": _compact_json(_sample_path_steps()),
"gaps_json": _compact_json([]),
"bridge_inserts_json": _compact_json([]),
},
body,
)
if s == "planning_exercise_search_intent":
return {
"search_query": search_query,
"heuristic_intent": "progression_next",
"scenario_hint": "preset_next",
"planning_context_json": _compact_json(ctx),
"target_profile_json": _compact_json(target),
**catalogs,
}
if s == "planning_exercise_search_rank":
return {
"search_query": search_query,
"intent": "progression_next",
"planning_context_json": _compact_json(ctx),
"target_profile_json": _compact_json(target),
"candidates_json": _compact_json(_sample_candidates()),
"result_limit": "5",
}
if s == "planning_exercise_expectation_profile":
return {
"heuristic_intent": "suggest_next",
"planning_context_json": _compact_json(ctx),
"target_profile_json": _compact_json(target),
**{k: v for k, v in catalogs.items() if k != "style_directions_catalog_json"},
}
raise ValueError(f"Planungs-Prompt-Slug nicht implementiert: {slug!r}")
__all__ = [
"PLANNING_PROMPT_SLUGS",
"PlanningPromptPreviewInput",
"is_planning_prompt_slug",
"resolve_planning_prompt_preview_variables",
]

View File

@ -0,0 +1,125 @@
"""
Gemeinsame KI-Prompt-Laufzeit (Shinkan): DB-Lesezugriff ai_prompts + Kontext-Arten.
Bleibt ohne Import von exercise_ai (kein Zirkel). Domänen wie exercise_ai nutzen
load_ai_prompt_row und die Enum; Platzhalter bauen sie selbst oder über geteilte Builder.
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, Mapping, Optional, Tuple
from prompt_resolver import MustacheRenderResult, render_mustache_template
_PLANNING_AI_SLUGS = frozenset(
{
"planning_exercise_search_rank",
"planning_exercise_search_intent",
"planning_exercise_expectation_profile",
}
)
_EXERCISE_AI_SLUGS = frozenset(
{
"exercise_summary",
"exercise_skill_suggestions",
"exercise_instruction_rewrite",
}
)
class AiPromptContextKind(str, Enum):
"""
Logischer Kontext fuer Platzhalter/Builder erweiterbar fuer Planung/Rahmen
ohne bestehende Slugs zu invalidieren.
"""
PLANNING_EXERCISE_SEARCH = "planning_exercise_search"
EXERCISE_FORM_AI = "exercise_form_ai"
def context_kind_for_slug(slug: str) -> Optional[AiPromptContextKind]:
"""Ordnet einen DB-Slug einer Kontext-Art zu, sofern registriert."""
s = (slug or "").strip().lower()
if s in _PLANNING_AI_SLUGS:
return AiPromptContextKind.PLANNING_EXERCISE_SEARCH
if s in _EXERCISE_AI_SLUGS:
return AiPromptContextKind.EXERCISE_FORM_AI
return None
def load_ai_prompt_row(cur, slug: str, *, active_only: bool = True) -> Optional[Dict[str, Any]]:
"""
Laedt eine Zeile ai_prompts fuer Laufzeit-Orchestrierung.
active_only=True: inaktive Prompts werden wie fehlend behandelt (503 im Aufrufer).
"""
if active_only:
cur.execute(
"""
SELECT slug, display_name, template, output_format, active, openrouter_model
FROM ai_prompts
WHERE slug = %s AND active = true
""",
(slug,),
)
else:
cur.execute(
"""
SELECT slug, display_name, template, output_format, active, openrouter_model
FROM ai_prompts
WHERE slug = %s
""",
(slug,),
)
row = cur.fetchone()
if not row:
return None
d = dict(row)
if active_only and not d.get("active", True):
return None
return d
class AiPromptUnavailableError(LookupError):
"""Kein aktiver Prompt fuer slug (oder Zeile fehlt)."""
def __init__(self, slug: str) -> None:
self.slug = (slug or "").strip()
super().__init__(self.slug)
def render_ai_prompt_template_for_row(
row: Mapping[str, Any],
variables: Mapping[str, str],
) -> MustacheRenderResult:
"""Ersetzt Platzhalter anhand einer bereits geladenen ai_prompts-Zeile (z. B. Admin-Vorschauch, inkl. inaktiv)."""
return render_mustache_template(str(row.get("template") or ""), variables)
def load_and_render_ai_prompt(
cur,
slug: str,
variables: Mapping[str, str],
*,
active_only: bool = True,
) -> Tuple[Dict[str, Any], MustacheRenderResult]:
"""
Laedt einen aktiven Prompt und wendet Mustache-Variablen an.
Wirft AiPromptUnavailableError, wenn die Zeile fehlt oder (bei active_only) inaktiv ist.
"""
row = load_ai_prompt_row(cur, slug, active_only=active_only)
if not row:
raise AiPromptUnavailableError(slug)
rr = render_ai_prompt_template_for_row(row, variables)
return dict(row), rr
__all__ = [
"AiPromptContextKind",
"AiPromptUnavailableError",
"context_kind_for_slug",
"load_ai_prompt_row",
"load_and_render_ai_prompt",
"render_ai_prompt_template_for_row",
]

View File

@ -170,6 +170,10 @@ def get_effective_tier(profile_id: str, conn=None) -> str:
def check_feature_access(profile_id: str, feature_id: str, conn=None) -> dict: def check_feature_access(profile_id: str, feature_id: str, conn=None) -> dict:
""" """
DEPRECATED für Shinkan: Mitai-v9c-Profil-Limits Schema 001 ist archiviert (Migration 078).
Für Vereins-Kontingente: club_features.check_club_feature_access(club_id, feature_id).
Check if a profile has access to a feature. Check if a profile has access to a feature.
Access hierarchy: Access hierarchy:
@ -315,6 +319,8 @@ def _check_impl(profile_id: str, feature_id: str, conn) -> dict:
def increment_feature_usage(profile_id: str, feature_id: str) -> None: def increment_feature_usage(profile_id: str, feature_id: str) -> None:
""" """
DEPRECATED für Shinkan siehe club_features.increment_club_feature_usage.
Increment usage counter for a feature. Increment usage counter for a feature.
Creates usage record if it doesn't exist, with reset_at based on Creates usage record if it doesn't exist, with reset_at based on

285
backend/capabilities.py Normal file
View File

@ -0,0 +1,285 @@
"""
Capability-Auflösung (CAPABILITY_CATALOG.v1.md, M3 C1).
Phase 2: probe_capability JSON-Log, kein Block (CAPABILITY_ENFORCE=0).
Phase 3+: CAPABILITY_ENFORCE=1 HTTP 403 bei fehlender Capability.
"""
from __future__ import annotations
import os
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from fastapi import HTTPException
from account_lifecycle import account_state_satisfies
from club_tenancy import is_platform_admin
from db import get_db, get_cursor
if TYPE_CHECKING:
from tenant_context import TenantContext
def capability_enforcement_enabled() -> bool:
v = os.getenv("CAPABILITY_ENFORCE", "0").strip().lower()
return v in ("1", "true", "yes")
def club_roles_in_club(tenant: "TenantContext", club_id: Optional[int]) -> List[str]:
if club_id is None:
return []
for m in tenant.memberships or []:
if int(m.get("id") or 0) == int(club_id):
roles = m.get("roles") or []
if hasattr(roles, "tolist"):
roles = roles.tolist()
return list(roles)
return []
def check_capability(
cur,
tenant: "TenantContext",
capability_id: str,
*,
club_id: Optional[int] = None,
) -> Dict[str, Any]:
"""
Prüft eine Capability für Tenant + optionalen Vereinskontext.
Returns: allowed, reason, account_state, club_roles, linked_feature_id
"""
account_state = getattr(tenant, "account_state", "active_member")
eff_club = club_id if club_id is not None else tenant.effective_club_id
club_roles = club_roles_in_club(tenant, eff_club) if eff_club is not None else []
cur.execute(
"""
SELECT id, min_account_state, linked_feature_id, active, domain
FROM capabilities
WHERE id = %s
""",
(capability_id,),
)
cap = cur.fetchone()
if not cap or not cap.get("active"):
return {
"allowed": False,
"reason": "capability_not_found",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": None,
}
min_state = cap.get("min_account_state") or "active_member"
if not account_state_satisfies(account_state, min_state):
return {
"allowed": False,
"reason": "account_state_insufficient",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
domain = (cap.get("domain") or "").strip().lower()
# Kontingent-Bypass (konfigurierbar per portal_role / profile grants, ohne Plattform-Admin-Pflicht)
if domain == "quota_bypass":
role_lc = (tenant.global_role or "").lower()
cur.execute(
"""
SELECT 1 FROM portal_role_capability_grants
WHERE portal_role = %s AND capability_id = %s
LIMIT 1
""",
(role_lc, capability_id),
)
if cur.fetchone():
return {
"allowed": True,
"reason": "quota_bypass_portal_grant",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
cur.execute(
"""
SELECT 1 FROM profile_capability_grants
WHERE profile_id = %s AND capability_id = %s
LIMIT 1
""",
(tenant.profile_id, capability_id),
)
if cur.fetchone():
return {
"allowed": True,
"reason": "quota_bypass_profile_grant",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
return {
"allowed": False,
"reason": "quota_bypass_denied",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
# Plattform-Capabilities
if domain == "platform" or capability_id.startswith("platform."):
role_lc = (tenant.global_role or "").lower()
if not is_platform_admin(role_lc):
return {
"allowed": False,
"reason": "portal_role_required",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
cur.execute(
"""
SELECT 1 FROM portal_role_capability_grants
WHERE portal_role = %s AND capability_id = %s
LIMIT 1
""",
(role_lc, capability_id),
)
if not cur.fetchone():
cur.execute(
"""
SELECT 1 FROM profile_capability_grants
WHERE profile_id = %s AND capability_id = %s
LIMIT 1
""",
(tenant.profile_id, capability_id),
)
if not cur.fetchone():
return {
"allowed": False,
"reason": "portal_capability_denied",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
return {
"allowed": True,
"reason": "portal_granted",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
# Plattform-Admin-Bypass für Mandanten-Funktionen (Audit-Pflicht, s. Katalog §9)
if is_platform_admin(tenant.global_role):
return {
"allowed": True,
"reason": "platform_admin_bypass",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
# Vereins-Capabilities: aktive Mitgliedschaft im Zielverein
if min_state == "active_member":
if eff_club is None:
return {
"allowed": False,
"reason": "no_club_context",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
if eff_club not in tenant.club_ids:
return {
"allowed": False,
"reason": "not_club_member",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
cur.execute(
"""
SELECT role_code FROM club_role_capability_grants
WHERE capability_id = %s
""",
(capability_id,),
)
required_roles = [r["role_code"] for r in cur.fetchall()]
if required_roles:
if not any(r in required_roles for r in club_roles):
return {
"allowed": False,
"reason": "club_role_denied",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
elif min_state == "active_member" and eff_club is not None:
# Offene Capability für alle aktiven Mitglieder — Mitgliedschaft reicht
pass
return {
"allowed": True,
"reason": "granted",
"account_state": account_state,
"club_roles": club_roles,
"linked_feature_id": cap.get("linked_feature_id"),
}
def resolve_capabilities_map(
cur,
tenant: "TenantContext",
*,
club_id: Optional[int] = None,
) -> Dict[str, bool]:
"""Alle aktiven Capabilities → bool (für späteres /me/entitlements)."""
cur.execute("SELECT id FROM capabilities WHERE active = true ORDER BY id")
ids = [r["id"] for r in cur.fetchall()]
out: Dict[str, bool] = {}
for cid in ids:
res = check_capability(cur, tenant, cid, club_id=club_id)
out[cid] = bool(res.get("allowed"))
return out
def probe_capability(
tenant: "TenantContext",
capability_id: str,
*,
action: str,
club_id: Optional[int] = None,
endpoint: Optional[str] = None,
conn=None,
) -> Dict[str, Any]:
"""Phase 2: Capability prüfen + JSON-Log; blockiert nur bei CAPABILITY_ENFORCE=1."""
from capability_logger import log_capability_check
def _run(c):
cur = get_cursor(c)
result = check_capability(cur, tenant, capability_id, club_id=club_id)
log_capability_check(
club_id=club_id if club_id is not None else tenant.effective_club_id,
profile_id=tenant.profile_id,
capability_id=capability_id,
action=action,
result=result,
endpoint=endpoint,
phase="enforce" if capability_enforcement_enabled() else "probe",
)
if capability_enforcement_enabled() and not result.get("allowed"):
raise HTTPException(
status_code=403,
detail=(
f"Keine Berechtigung für {capability_id} "
f"({result.get('reason', 'denied')})."
),
)
return result
if conn is not None:
return _run(conn)
with get_db() as c:
return _run(c)

View File

@ -0,0 +1,94 @@
"""
Audit: Welche Capabilities sind an Endpoints angebunden?
Für Admin-Matrix (Rollen & Rechte) und Roadmap bei neuem probe_capability hier eintragen.
"""
from __future__ import annotations
from typing import Any, Dict
# Endpoints rufen probe_capability auf (Log; Block nur bei CAPABILITY_ENFORCE=1)
WIRED_PROBE = frozenset(
{
"exercises.ai.suggest",
"exercises.ai.regenerate",
"exercises.create",
"exercises.media.upload",
"planning.ai.suggest",
"planning.ai.progression_path",
"club.creation_request.read_own",
"club.creation_request.create",
"club.creation_request.withdraw",
"platform.club_creation.approve",
}
)
# Kontingent-Verbrauch nach Erfolg (consume_club_feature_with_usage)
FEATURE_CONSUME_WIRED = frozenset(
{
"ai_calls",
}
)
def enforcement_status_for_capability(capability_id: str) -> Dict[str, Any]:
"""
Anzeige-Status für Superadmin-Matrix.
level: probe | legacy | platform | open | none
"""
cid = (capability_id or "").strip()
if cid in WIRED_PROBE:
return {
"level": "probe",
"label": "API vorbereitet (Log)",
"detail": "probe_capability am Endpoint; Hard-Block erst mit CAPABILITY_ENFORCE=1",
"implemented": True,
}
if cid.startswith("platform."):
if cid == "platform.admin.access":
return {
"level": "platform",
"label": "Plattform (Router-Guard)",
"detail": "RequireAdmin / Superadmin-Checks",
"implemented": True,
}
if cid in WIRED_PROBE:
pass
return {
"level": "platform",
"label": "Plattform (teilweise)",
"detail": "Meist Router-Guard; Capability-Probe nur wo eingetragen",
"implemented": cid in WIRED_PROBE,
}
if cid.startswith("club."):
return {
"level": "open",
"label": "Onboarding",
"detail": "Account-State / eigene Flows",
"implemented": cid in WIRED_PROBE,
}
# Vereins-Capabilities ohne Probe: Legacy club_tenancy (can_plan_in_club, has_club_role, …)
return {
"level": "legacy",
"label": "Nur Legacy-Rollen",
"detail": "Noch kein probe_capability — prüft can_plan_in_club / club_admin im Code",
"implemented": False,
}
def feature_consume_status(feature_id: str) -> Dict[str, Any]:
fid = (feature_id or "").strip()
if fid in FEATURE_CONSUME_WIRED:
return {
"level": "consume",
"label": "Verbrauch aktiv",
"detail": "consume_club_feature_with_usage + feature_usage in Response",
"implemented": True,
}
return {
"level": "inventory",
"label": "Bestand / Probe",
"detail": "Probe oder Live-Zählung; kein Consume nach Aktion",
"implemented": False,
}

View File

@ -0,0 +1,64 @@
"""
JSON-Log für Capability-Checks (M3 Phase 2 analog club_feature_logger).
"""
from __future__ import annotations
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional
def _log_dir() -> Path:
custom = (os.getenv("CAPABILITY_LOG_DIR") or "").strip()
if custom:
return Path(custom)
return Path("/app/logs")
capability_logger = logging.getLogger("shinkan.capability_usage")
capability_logger.setLevel(logging.INFO)
capability_logger.propagate = False
if not capability_logger.handlers:
log_dir = _log_dir()
try:
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "capability-usage.log"
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter("%(message)s"))
capability_logger.addHandler(file_handler)
except OSError:
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter("[capability-usage] %(message)s"))
capability_logger.addHandler(stream_handler)
def log_capability_check(
*,
club_id: Optional[int],
profile_id: Optional[int],
capability_id: str,
action: str,
result: Dict[str, Any],
endpoint: Optional[str] = None,
phase: str = "probe",
) -> None:
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"club_id": club_id,
"profile_id": profile_id,
"capability": capability_id,
"action": action,
"endpoint": endpoint,
"phase": phase,
"allowed": result.get("allowed", True),
"reason": result.get("reason", "unknown"),
"account_state": result.get("account_state"),
"club_roles": result.get("club_roles"),
"enforcement": os.getenv("CAPABILITY_ENFORCE", "0") == "1",
}
capability_logger.info(json.dumps(entry, ensure_ascii=False))

View File

@ -0,0 +1,432 @@
"""
Katalog-Prompt-Slots Slot-Typ-Vokabular + Werte pro Stammdaten-Zeile (H2).
Prompts in ai_prompts referenzieren Platzhalter wie {{focus_area_hints_on_progression}}.
Inhalte liegen in catalog_prompt_slots (Admin-editierbar), nicht im Code pro Eintrag.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
from planning_catalog_context import (
ProgressionPlanningCatalogContext,
PlanningCatalogContextItem,
catalog_context_has_items,
)
from catalog_slot_fallbacks import merge_stored_slots_with_fallbacks
# ---------------------------------------------------------------------------
# Dimensionen (Prioritätsreihenfolge)
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class CatalogKindConfig:
kind: str
table: str
context_attr: str
label_de: str
CATALOG_KINDS: Tuple[CatalogKindConfig, ...] = (
CatalogKindConfig("focus_area", "focus_areas", "focus_areas", "Primärfokus"),
CatalogKindConfig("training_type", "training_types", "training_types", "Trainingsstil"),
CatalogKindConfig("target_group", "target_groups", "target_groups", "Zielgruppe"),
CatalogKindConfig("style_direction", "style_directions", "style_directions", "Stilrichtung"),
)
_KIND_BY_NAME = {c.kind: c for c in CATALOG_KINDS}
# ---------------------------------------------------------------------------
# Slot-Typen (Vokabular — erweiterbar via catalog_prompt_slot_types)
# ---------------------------------------------------------------------------
SLOT_KEYS: Tuple[str, ...] = (
"description",
"hints_on_progression",
"hints_on_exercise",
"hints_on_path_qa",
"anti_patterns",
"rematch_guard",
)
LLM_SLOT_KEYS: Tuple[str, ...] = tuple(k for k in SLOT_KEYS if k != "rematch_guard")
GUIDANCE_BLOCK_SLOTS: Tuple[str, ...] = (
"description",
"hints_on_progression",
"hints_on_path_qa",
"anti_patterns",
)
GUIDANCE_PROFILE_BY_SLUG: Dict[str, Tuple[str, ...]] = {
"planning_exercise_path_qa": ("description", "hints_on_path_qa", "anti_patterns"),
"planning_progression_roadmap": ("description", "hints_on_progression", "anti_patterns"),
"planning_progression_stage_spec": ("hints_on_progression", "anti_patterns", "description"),
"planning_progression_goal_analysis": ("description", "hints_on_progression"),
"planning_progression_start_target": ("description",),
}
def placeholder_key(catalog_kind: str, slot_key: str) -> str:
return f"{catalog_kind}_{slot_key}"
def all_placeholder_keys() -> List[str]:
keys: List[str] = []
for cfg in CATALOG_KINDS:
for slot in SLOT_KEYS:
keys.append(placeholder_key(cfg.kind, slot))
keys.extend(["catalog_guidance_block", "catalog_context_json", "has_catalog_guidance"])
return keys
def empty_catalog_variables() -> Dict[str, str]:
out = {k: "" for k in all_placeholder_keys()}
return out
# ---------------------------------------------------------------------------
# Katalog-Kontext → aktiver Eintrag
# ---------------------------------------------------------------------------
def pick_active_catalog_item(
items: Sequence[PlanningCatalogContextItem],
) -> Optional[PlanningCatalogContextItem]:
if not items:
return None
primaries = [i for i in items if i.is_primary]
if primaries:
return primaries[0]
if len(items) == 1:
return items[0]
return max(items, key=lambda i: (float(i.weight), -int(i.id)))
def _load_catalog_row(cur, table: str, item_id: int) -> Optional[Dict[str, Any]]:
cur.execute(
f"""
SELECT id, name, description
FROM {table}
WHERE id = %s
""",
(int(item_id),),
)
row = cur.fetchone()
if not row:
return None
return {
"id": int(row["id"]),
"name": str(row.get("name") or "").strip(),
"description": str(row.get("description") or "").strip(),
}
def _load_slots_for_entry(cur, catalog_kind: str, catalog_id: int) -> Dict[str, str]:
cur.execute(
"""
SELECT slot_key, content
FROM catalog_prompt_slots
WHERE catalog_kind = %s AND catalog_id = %s
""",
(catalog_kind, int(catalog_id)),
)
out: Dict[str, str] = {}
for row in cur.fetchall():
key = str(row.get("slot_key") or "").strip()
if key:
out[key] = str(row.get("content") or "").strip()
return out
def _slot_types_table_ready(cur) -> bool:
cur.execute("SELECT to_regclass(%s)::text AS t", ("public.catalog_prompt_slot_types",))
row = cur.fetchone()
if not row:
return False
val = row.get("t") if isinstance(row, dict) else row[0]
return val is not None and str(val).strip() != ""
def list_slot_type_definitions(cur) -> List[Dict[str, Any]]:
if not _slot_types_table_ready(cur):
return _fallback_slot_type_rows()
cur.execute(
"""
SELECT slot_key, display_name, description, applicable_kinds, sort_order, for_llm, for_code
FROM catalog_prompt_slot_types
ORDER BY sort_order ASC NULLS LAST, slot_key ASC
"""
)
rows = []
for row in cur.fetchall():
d = dict(row)
kinds = d.get("applicable_kinds")
if isinstance(kinds, str):
kinds = [k.strip() for k in kinds.strip("{}").split(",") if k.strip()]
d["applicable_kinds"] = list(kinds or [])
rows.append(d)
return rows
def _fallback_slot_type_rows() -> List[Dict[str, Any]]:
labels = {
"description": "Allgemeine Beschreibung",
"hints_on_progression": "Hinweise Progressionsgraph",
"hints_on_exercise": "Hinweise Übungsanlage",
"hints_on_path_qa": "Hinweise Pfad-QS",
"anti_patterns": "Anti-Patterns (Fehlbewertung vermeiden)",
"rematch_guard": "Rematch-Guard (Code)",
}
kinds = [c.kind for c in CATALOG_KINDS]
rows = []
for i, key in enumerate(SLOT_KEYS):
rows.append(
{
"slot_key": key,
"display_name": labels.get(key, key),
"description": "",
"applicable_kinds": kinds,
"sort_order": (i + 1) * 10,
"for_llm": key != "rematch_guard",
"for_code": key == "rematch_guard",
}
)
return rows
def _resolve_entry_slot_values(
stored: Mapping[str, str],
row: Mapping[str, Any],
catalog_kind: str,
) -> Dict[str, str]:
"""DB → Namens-Fallback → Stammdaten-Beschreibung (nur description)."""
return merge_stored_slots_with_fallbacks(
stored,
catalog_kind=catalog_kind,
name=str(row.get("name") or ""),
stammdaten_description=str(row.get("description") or ""),
)
def get_catalog_entry_slots(cur, catalog_kind: str, catalog_id: int) -> Dict[str, Any]:
cfg = _KIND_BY_NAME.get((catalog_kind or "").strip())
if not cfg:
raise ValueError(f"Unbekannter catalog_kind: {catalog_kind!r}")
row = _load_catalog_row(cur, cfg.table, catalog_id)
if not row:
raise LookupError("Katalog-Eintrag nicht gefunden")
stored = _load_slots_for_entry(cur, cfg.kind, catalog_id)
merged = _resolve_entry_slot_values(stored, row, cfg.kind)
return {
"catalog_kind": cfg.kind,
"catalog_id": int(catalog_id),
"name": row["name"],
"slots": merged,
"stored_slots": {k: stored.get(k, "") for k in SLOT_KEYS},
}
def upsert_catalog_entry_slots(
cur,
catalog_kind: str,
catalog_id: int,
slots: Mapping[str, Any],
) -> Dict[str, Any]:
cfg = _KIND_BY_NAME.get((catalog_kind or "").strip())
if not cfg:
raise ValueError(f"Unbekannter catalog_kind: {catalog_kind!r}")
row = _load_catalog_row(cur, cfg.table, catalog_id)
if not row:
raise LookupError("Katalog-Eintrag nicht gefunden")
for slot_key, raw in (slots or {}).items():
sk = str(slot_key or "").strip()
if sk not in SLOT_KEYS:
continue
content = str(raw or "").strip()
if not content:
cur.execute(
"""
DELETE FROM catalog_prompt_slots
WHERE catalog_kind = %s AND catalog_id = %s AND slot_key = %s
""",
(cfg.kind, int(catalog_id), sk),
)
continue
cur.execute(
"""
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content, updated_at)
VALUES (%s, %s, %s, %s, NOW())
ON CONFLICT (catalog_kind, catalog_id, slot_key)
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW()
""",
(cfg.kind, int(catalog_id), sk, content),
)
return get_catalog_entry_slots(cur, cfg.kind, catalog_id)
def _render_dimension_section(
label_de: str,
name: str,
slot_values: Mapping[str, str],
*,
slot_keys: Sequence[str],
) -> Optional[str]:
parts: List[str] = [f"### {label_de}{name}"]
labels = {
"description": "Beschreibung",
"hints_on_progression": "Progressions-Hinweise",
"hints_on_path_qa": "QS-Hinweise",
"hints_on_exercise": "Übungsanlage",
"anti_patterns": "Vermeiden",
}
added = False
for sk in slot_keys:
text = str(slot_values.get(sk) or "").strip()
if not text:
continue
added = True
if sk == "description":
parts.append(text)
else:
parts.append(f"{labels.get(sk, sk)}: {text}")
if not added:
return None
return "\n".join(parts)
def _compose_guidance_block(
sections: List[str],
) -> str:
if not sections:
return ""
return "## Katalog-Kontext (Didaktik & Bewertung)\n\n" + "\n\n".join(sections)
def resolve_catalog_prompt_variables(
cur,
catalog: Optional[ProgressionPlanningCatalogContext],
*,
slug: Optional[str] = None,
) -> Dict[str, Any]:
"""
Liefert Mustache-Strings + Metadaten.
Returns dict mit allen {{kind_slot}} Keys, catalog_guidance_block, catalog_context_json,
has_catalog_guidance (bool), active_slots (list).
"""
variables = empty_catalog_variables()
meta: Dict[str, Any] = {
"active_slots": [],
"audit": {},
}
if cur is None or not catalog_context_has_items(catalog):
variables["catalog_context_json"] = ""
return {**variables, **meta}
profile = GUIDANCE_PROFILE_BY_SLUG.get((slug or "").strip().lower(), GUIDANCE_BLOCK_SLOTS)
sections: List[str] = []
audit: Dict[str, Any] = {}
has_any = False
active_slots: List[str] = []
for cfg in CATALOG_KINDS:
items = getattr(catalog, cfg.context_attr, None) or []
active = pick_active_catalog_item(items)
if not active:
continue
row = _load_catalog_row(cur, cfg.table, active.id)
if not row:
continue
stored = _load_slots_for_entry(cur, cfg.kind, row["id"]) if _slot_types_table_ready(cur) else {}
slot_values = _resolve_entry_slot_values(stored, row, cfg.kind)
for sk in SLOT_KEYS:
pk = placeholder_key(cfg.kind, sk)
text = slot_values.get(sk, "")
variables[pk] = text
if text.strip() and sk in LLM_SLOT_KEYS:
has_any = True
active_slots.append(pk)
audit[cfg.context_attr] = {
"catalog_kind": cfg.kind,
"id": row["id"],
"name": row["name"],
"is_primary": bool(active.is_primary),
"weight": float(active.weight),
"filled_slots": [k for k in LLM_SLOT_KEYS if slot_values.get(k, "").strip()],
"stored_slots": [k for k in SLOT_KEYS if (stored.get(k) or "").strip()],
}
section = _render_dimension_section(cfg.label_de, row["name"], slot_values, slot_keys=profile)
if section:
sections.append(section)
variables["catalog_guidance_block"] = _compose_guidance_block(sections)
ctx_json = json.dumps(audit, ensure_ascii=False, separators=(",", ":"))
variables["catalog_context_json"] = f"Katalog-Audit: {ctx_json}" if audit else ""
variables["has_catalog_guidance"] = "true" if has_any else ""
return {
**variables,
"active_slots": active_slots,
"audit": audit,
}
def get_rematch_guard_for_catalog(
cur,
catalog: Optional[ProgressionPlanningCatalogContext],
) -> Optional[str]:
"""Erste passende rematch_guard entlang der Dimensions-Priorität."""
if cur is None or not catalog_context_has_items(catalog):
return None
for cfg in CATALOG_KINDS:
items = getattr(catalog, cfg.context_attr, None) or []
active = pick_active_catalog_item(items)
if not active:
continue
stored = _load_slots_for_entry(cur, cfg.kind, active.id)
row = _load_catalog_row(cur, cfg.table, active.id)
if not row:
continue
slot_values = _resolve_entry_slot_values(stored, row, cfg.kind)
guard = (slot_values.get("rematch_guard") or "").strip()
if guard:
return guard
return None
# Abwärtskompatibilität H1-API
def build_catalog_guidance_for_prompt(
cur,
catalog: Optional[ProgressionPlanningCatalogContext],
*,
slug: Optional[str] = None,
) -> Dict[str, Any]:
resolved = resolve_catalog_prompt_variables(cur, catalog, slug=slug)
return {
"catalog_guidance_block": resolved.get("catalog_guidance_block", ""),
"catalog_context_json": resolved.get("catalog_context_json", ""),
"has_catalog_guidance": resolved.get("has_catalog_guidance") == "true",
"snippet_keys": list(resolved.get("active_slots") or []),
"variables": {k: str(resolved.get(k) or "") for k in all_placeholder_keys()},
}
__all__ = [
"CATALOG_KINDS",
"GUIDANCE_PROFILE_BY_SLUG",
"SLOT_KEYS",
"build_catalog_guidance_for_prompt",
"empty_catalog_variables",
"get_catalog_entry_slots",
"get_rematch_guard_for_catalog",
"list_slot_type_definitions",
"pick_active_catalog_item",
"placeholder_key",
"all_placeholder_keys",
"resolve_catalog_prompt_variables",
"upsert_catalog_entry_slots",
]

View File

@ -0,0 +1,284 @@
"""
Namensbasierte Fallback-Slots bis Admin/DB befüllt sind (H1-Registry-Inhalt).
DB-Werte in catalog_prompt_slots haben immer Vorrang. Fallbacks füllen nur leere Slot-Keys.
"""
from __future__ import annotations
import re
import unicodedata
from typing import Dict, Mapping, Optional, Sequence, Tuple
_UMLAUT_MAP = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss", "Ä": "ae", "Ö": "oe", "Ü": "ue"})
SlotPack = Dict[str, str]
# (catalog_kind, name_pattern_lower) — erste passende Regel gewinnt; * = Default pro Kind
_FALLBACK_RULES: Tuple[Tuple[str, str, SlotPack], ...] = (
# --- focus_area ---
(
"focus_area",
"gewaltschutz",
{
"description": (
"Planung zielt auf Prävention, Deeskalation, Grenzen und sichere Übungsformen — "
"nicht auf Wettkampf-Perfektion oder Technik-Show."
),
"hints_on_progression": (
"Phasen: Wahrnehmung → Grenzen → Deeskalation → sichere Übungsformen; "
"keine Kumite-Perfektionsstufen erzwingen."
),
"hints_on_exercise": (
"Übungen mit Rollen, Kommunikation, Ausweichen; keine rein technischen Kick-Fokus-Inseln ohne Bezug."
),
"hints_on_path_qa": (
"Gute Pfade bauen Sicherheit, Kommunikation und Alternativen auf; "
"„Lücken“ sind fehlende Deeskalations- oder Rollenspiel-Stufen, nicht fehlende Kick-Varianten."
),
"anti_patterns": "Nicht nach Kumite-Tiefe, Explosivität oder Wettkampf-Belastung bewerten.",
},
),
(
"focus_area",
"selbstverteidigung",
{
"description": (
"Praktische Selbstverteidigung: realistische Szenarien, Sicherheit und "
"anwendungsnahe Progression — nicht Show-Technik oder Wettkampf-Kata."
),
"hints_on_progression": (
"Von Wahrnehmung und Distanz zu einfachen Abwehrmustern und kontrollierter Anwendung."
),
"hints_on_exercise": "Partnerübungen mit klaren Sicherheitsregeln; Szenario-Bezug wichtiger als Stil-Show.",
"hints_on_path_qa": (
"Lücken bei Szenario- oder Sicherheitsstufen sind relevant; "
"fehlende Kick-Varianten oder Wettkampftiefe sind kein Mangel."
),
"anti_patterns": "Keine Wettkampf- oder Kata-Perfektion als QS-Maßstab.",
},
),
(
"focus_area",
"fitness",
{
"description": (
"Fitness- und Konditionsorientierung mit sicherer Belastungssteuerung; "
"Technikbezug nur wo fachlich sinnvoll."
),
"hints_on_progression": "Progression von niedriger zu moderater Belastung; klare Pausen und Technikhygiene.",
"hints_on_path_qa": (
"Keine Wettkampf-Spezialisierung als Pflicht-Kriterium; "
"Belastungssteigerung ohne Technikbezug abwerten."
),
"anti_patterns": "Keine Kumite-Perfektion oder Wettkampf-Kombinationen als QS-Maßstab verlangen.",
},
),
(
"focus_area",
"karate",
{
"description": (
"Technik-Curriculum im Karate-Kontext: aufeinander aufbauende Kihon-Progression "
"mit klaren Qualitätsankern (Stand, Hüfte, Kime)."
),
"hints_on_progression": (
"Typische Phasen: Einstieg → Grundlagen → Koordination/Kraft → Anwendung → optional Vertiefung; "
"Grundlagen vor Perfektion."
),
"hints_on_exercise": (
"Kihon und Partnerübungen mit Technikbezug; reine Kraft-/Ausdauer-Inseln nur mit klarer Begründung."
),
"hints_on_path_qa": (
"Kohärente Progression Grundlagen → Anwendung → Vertiefung; "
"Übergänge ohne Sprünge; themenfremde Kraft-/Ausdauer-Inseln abwerten."
),
"anti_patterns": "Keine pauschale Perfektions-Stufe verlangen, wenn Kontext Breitensport ist.",
},
),
(
"focus_area",
"*",
{
"description": "Technik- oder Themen-Curriculum mit didaktisch aufeinander aufbauenden Stufen.",
"hints_on_progression": "Grundlagen vor Anwendung; moderate Sprünge zwischen Stufen vermeiden.",
"hints_on_path_qa": (
"Kohärente Progression zum Anfrage-Thema; "
"Lücken sind fehlende Zwischenstufen im Lernpfad, nicht fehlende Nebenthemen."
),
"hints_on_exercise": "Übungen mit klarem Bezug zum Pfad-Thema und zur Stufe.",
},
),
# --- training_type ---
(
"training_type",
"breitensport",
{
"description": (
"Partizipation, Verständlichkeit, Freude am Bewegen; weniger maximale Spezialisierung."
),
"hints_on_progression": "Moderater Schwierigkeitsanstieg; Perfektionsphasen optional.",
"hints_on_path_qa": (
"Hohe OK-Rate bei moderatem Schwierigkeitsanstieg; "
"„Perfektion“-Stufen nur optional, nicht als Pflicht-Lücke."
),
"rematch_guard": "Keine leeren Slots erzwingen, nur um eine Leistungs-Perfektionsstufe zu füllen.",
},
),
(
"training_type",
"leistungssport",
{
"description": "Leistungsorientiertes Training mit höherer Anspruchskurve und Spezialisierung.",
"hints_on_progression": "Belastungs- und Kombinationsprogressionen sind erwünscht.",
"hints_on_path_qa": (
"Höhere Anspruchskurven sind passend; Lücken in Spezialisierung können echte Hinweise sein."
),
},
),
(
"training_type",
"wettkampf",
{
"description": (
"Wettkampforientiertes Training mit höherer Anspruchskurve und belastungsnahen Phasen."
),
"hints_on_progression": "Anwendungs- und Druckphasen zeitig einplanen.",
"hints_on_path_qa": (
"Spezialisierung, Kombination und Belastung unter Druck sind relevant; "
"Lücken in Anwendungs- oder Perfektionsphasen können echte Hinweise sein."
),
},
),
(
"training_type",
"*",
{
"hints_on_path_qa": "Didaktische Kohärenz wichtiger als maximale Spezialisierung — Kontext beachten.",
},
),
# --- target_group ---
(
"target_group",
"kinder",
{
"description": (
"Kinder: kurze Einheiten, spielerische Einstiege, Sicherheit und altersgerechte Komplexität."
),
"hints_on_progression": "Spielerische Einstiege; kurze Abschnitte; Sicherheit vor Perfektion.",
"hints_on_path_qa": (
"Didaktik ohne Überforderung; klare Regeln und Sicherheit vor Perfektion; "
"Lücken bei Spiel-/Rollenelementen wichtiger als Wettkampftiefe."
),
"anti_patterns": "Keine Erwachsenen-Wettkampf-Perfektion als QS-Maßstab.",
},
),
(
"target_group",
"leistungssportler",
{
"description": "Leistungsgruppe: höhere Anspruchskurven und Spezialisierung sind fachlich passend.",
"hints_on_progression": "Anspruchskurve und Spezialisierung dürfen steiler sein.",
"hints_on_path_qa": (
"Höhere Anspruchskurven, Belastungs- und Kombinationsprogressionen sind relevant; "
"Lücken in Spezialisierung können echte Hinweise sein."
),
},
),
(
"target_group",
"breitensportler",
{
"description": "Breitensport: Partizipation und Verständlichkeit vor maximaler Spezialisierung.",
"hints_on_path_qa": (
"Moderate Progression; Perfektions-Lücken sind selten echte Mängel."
),
"anti_patterns": "Keine Leistungssport-Perfektion als Pflicht-Kriterium.",
},
),
(
"target_group",
"*",
{
"hints_on_path_qa": "Zielgruppe im Tempo und in der Komplexität berücksichtigen.",
},
),
# --- style_direction ---
(
"style_direction",
"shotokan",
{
"description": (
"Shotokan-Linie: klare Kihon-Struktur, Hüft- und Standarbeit als wiederkehrende Qualitätsanker."
),
"hints_on_progression": "Nuancen in Stellung und Hüfttechnik; kein neuer Planungstyp.",
"hints_on_path_qa": "Konsistenz von Stand, Hüfte und Kime entlang des Pfads bewerten.",
},
),
(
"style_direction",
"*",
{
"hints_on_progression": (
"Stil-spezifische Nuancen (Stand, Hüfte, Rhythmus) einbeziehen — ohne Stilwechsel zu erzwingen."
),
},
),
)
def normalize_catalog_name_key(name: str) -> str:
s = unicodedata.normalize("NFKD", (name or "").translate(_UMLAUT_MAP))
s = s.encode("ascii", "ignore").decode("ascii").lower()
s = re.sub(r"[^a-z0-9]+", "_", s).strip("_")
return s or "unknown"
def get_fallback_slots_for_entry(catalog_kind: str, name: str) -> SlotPack:
kind = (catalog_kind or "").strip().lower()
norm = normalize_catalog_name_key(name)
default: SlotPack = {}
for rule_kind, pattern, pack in _FALLBACK_RULES:
if rule_kind != kind:
continue
if pattern == "*":
default = dict(pack)
continue
if pattern in norm or norm.startswith(pattern) or pattern in (name or "").lower():
return dict(pack)
return default
def merge_stored_slots_with_fallbacks(
stored: Mapping[str, str],
*,
catalog_kind: str,
name: str,
stammdaten_description: str = "",
) -> Dict[str, str]:
"""DB + Stammdaten-Beschreibung + Namens-Fallback."""
fallbacks = get_fallback_slots_for_entry(catalog_kind, name)
out: Dict[str, str] = {}
for key in (
"description",
"hints_on_progression",
"hints_on_exercise",
"hints_on_path_qa",
"anti_patterns",
"rematch_guard",
):
if key == "description":
out[key] = (
(stored.get(key) or "").strip()
or (fallbacks.get(key) or "").strip()
or (stammdaten_description or "").strip()
)
else:
out[key] = (stored.get(key) or "").strip() or (fallbacks.get(key) or "").strip()
return out
__all__ = [
"get_fallback_slots_for_entry",
"merge_stored_slots_with_fallbacks",
"normalize_catalog_name_key",
]

View File

@ -0,0 +1,74 @@
"""
JSON-Log für Vereins-Feature-Zugriffe (Phase 2: nur Monitoring, kein Block).
Spez: CLUB_MEMBERSHIP_AND_FEATURES.v1.md §9 Phase 2 analog Mitai feature_logger.py.
"""
from __future__ import annotations
import json
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional
def _log_dir() -> Path:
custom = (os.getenv("CLUB_FEATURE_LOG_DIR") or "").strip()
if custom:
return Path(custom)
return Path("/app/logs")
feature_usage_logger = logging.getLogger("shinkan.club_feature_usage")
feature_usage_logger.setLevel(logging.INFO)
feature_usage_logger.propagate = False
if not feature_usage_logger.handlers:
log_dir = _log_dir()
try:
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "club-feature-usage.log"
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter("%(message)s"))
feature_usage_logger.addHandler(file_handler)
except OSError:
# Dev ohne /app/logs: Fallback stderr
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter("[club-feature-usage] %(message)s"))
feature_usage_logger.addHandler(stream_handler)
def log_club_feature_usage(
*,
club_id: Optional[int],
profile_id: Optional[int],
feature_id: str,
action: str,
access: Dict[str, Any],
endpoint: Optional[str] = None,
phase: str = "probe",
) -> None:
"""
Strukturiertes JSON-Log eines Feature-Checks.
phase: probe (Phase 2, non-blocking) | enforce (Phase 4, nach Block-Entscheid)
"""
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"club_id": club_id,
"profile_id": profile_id,
"feature": feature_id,
"action": action,
"endpoint": endpoint,
"phase": phase,
"plan_id": access.get("plan_id"),
"used": access.get("used", 0),
"limit": access.get("limit"),
"remaining": access.get("remaining"),
"allowed": access.get("allowed", True),
"reason": access.get("reason", "unknown"),
"enforcement": os.getenv("CLUB_FEATURE_ENFORCE", "0") == "1",
}
feature_usage_logger.info(json.dumps(entry, ensure_ascii=False))

713
backend/club_features.py Normal file
View File

@ -0,0 +1,713 @@
"""
Vereinsbezogene Feature-Limits (Mitai-v9c-Pattern, Subjekt club_id).
Spez: .claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md
Phase 2 (M2): probe_club_feature_access JSON-Log, kein HTTP-Block.
Phase 4 (M5+): CLUB_FEATURE_ENFORCE=1 HTTP 403 + increment.
Verbrauch-Standard für Router:
probe_club_feature_access Business-Logik consume_club_feature_with_usage merge_feature_usage_into_response
Legacy profil-zentriert: auth.check_feature_access (001 / Mitai-Überbleibsel) nicht für Shinkan-Limits nutzen.
"""
from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional, TYPE_CHECKING
from fastapi import HTTPException
from db import get_db, get_cursor
if TYPE_CHECKING:
from tenant_context import TenantContext
# Bestands-Features: Verbrauch = Live-Zählung in DB (nicht club_feature_usage)
_INVENTORY_FEATURES = frozenset(
{"exercises", "training_groups", "active_members", "training_programs"}
)
def _calculate_next_reset(reset_period: str, *, now: Optional[datetime] = None) -> Optional[datetime]:
"""Nächster Reset-Zeitpunkt; None bei 'never'."""
ref = now or datetime.now(timezone.utc)
if reset_period == "never":
return None
if reset_period == "daily":
tomorrow = ref.date() + timedelta(days=1)
return datetime.combine(tomorrow, datetime.min.time(), tzinfo=timezone.utc)
if reset_period == "monthly":
if ref.month == 12:
return datetime(ref.year + 1, 1, 1, tzinfo=timezone.utc)
return datetime(ref.year, ref.month + 1, 1, tzinfo=timezone.utc)
return None
def _normalize_limit(raw: Any) -> Optional[int]:
"""NULL = unbegrenzt; -1 (Legacy 001) wird als unbegrenzt behandelt."""
if raw is None:
return None
try:
v = int(raw)
except (TypeError, ValueError):
return None
if v < 0:
return None
return v
def get_effective_club_plan(cur, club_id: int) -> str:
"""
Effektiver Plan für einen Verein.
1. Aktiver club_access_grants mit plan_id (Zeitfenster, neueste ends_at)
2. club_subscriptions.status = 'active' plan_id
3. Fallback 'free'
"""
cur.execute(
"""
SELECT plan_id
FROM club_access_grants
WHERE club_id = %s
AND plan_id IS NOT NULL
AND starts_at <= NOW()
AND ends_at > NOW()
ORDER BY ends_at DESC
LIMIT 1
""",
(club_id,),
)
grant = cur.fetchone()
if grant and grant.get("plan_id"):
return str(grant["plan_id"])
cur.execute(
"""
SELECT plan_id
FROM club_subscriptions
WHERE club_id = %s AND status = 'active'
LIMIT 1
""",
(club_id,),
)
sub = cur.fetchone()
if sub and sub.get("plan_id"):
return str(sub["plan_id"])
return "free"
def _resolve_club_limit(cur, club_id: int, feature_id: str, feature_row: dict) -> Optional[int]:
"""Limit-Wert: Override > Plan > Feature-Default."""
cur.execute(
"""
SELECT limit_value
FROM club_feature_overrides
WHERE club_id = %s AND feature_id = %s
""",
(club_id, feature_id),
)
override = cur.fetchone()
if override is not None:
return _normalize_limit(override.get("limit_value"))
plan_id = get_effective_club_plan(cur, club_id)
cur.execute(
"""
SELECT limit_value
FROM club_plan_limits
WHERE plan_id = %s AND feature_id = %s
""",
(plan_id, feature_id),
)
plan_lim = cur.fetchone()
if plan_lim is not None:
return _normalize_limit(plan_lim.get("limit_value"))
return _normalize_limit(feature_row.get("default_limit"))
def _live_inventory_count(cur, club_id: int, feature_id: str) -> Optional[int]:
"""Aktueller Bestand für reset_period=never Features."""
if feature_id == "exercises":
cur.execute(
"""
SELECT COUNT(*)::int AS c
FROM exercises
WHERE club_id = %s AND status != 'archived'
""",
(club_id,),
)
elif feature_id == "training_groups":
cur.execute(
"SELECT COUNT(*)::int AS c FROM training_groups WHERE club_id = %s",
(club_id,),
)
elif feature_id == "active_members":
cur.execute(
"""
SELECT COUNT(*)::int AS c
FROM club_members
WHERE club_id = %s AND status = 'active'
""",
(club_id,),
)
elif feature_id == "training_programs":
cur.execute(
"""
SELECT COUNT(*)::int AS c FROM (
SELECT id FROM training_framework_programs WHERE club_id = %s
UNION ALL
SELECT id FROM training_modules WHERE club_id = %s
) t
""",
(club_id, club_id),
)
else:
return None
row = cur.fetchone()
return int(row["c"] or 0) if row else 0
def resolve_club_id_for_probe(
tenant: "TenantContext",
*,
object_club_id: Optional[int] = None,
) -> Optional[int]:
"""Verein für Feature-Probe: explizites Objekt > effective_club_id."""
if object_club_id is not None:
return int(object_club_id)
eff = getattr(tenant, "effective_club_id", None)
return int(eff) if eff is not None else None
def _maybe_reset_usage(cur, conn, club_id: int, feature_id: str, feature_row: dict, usage_row: Optional[dict]) -> int:
"""Setzt Zähler zurück wenn reset_at überschritten; gibt aktuellen used zurück."""
used = int(usage_row.get("usage_count") or 0) if usage_row else 0
reset_at = usage_row.get("reset_at") if usage_row else None
period = (feature_row.get("reset_period") or "never").strip().lower()
if not usage_row or not reset_at or period == "never":
return used
now = datetime.now(timezone.utc)
ra = reset_at
if hasattr(ra, "tzinfo") and ra.tzinfo is None:
ra = ra.replace(tzinfo=timezone.utc)
if ra and now > ra:
next_reset = _calculate_next_reset(period, now=now)
cur.execute(
"""
UPDATE club_feature_usage
SET usage_count = 0, reset_at = %s, updated_at = NOW()
WHERE club_id = %s AND feature_id = %s
""",
(next_reset, club_id, feature_id),
)
conn.commit()
return 0
return used
def check_club_feature_access(
club_id: int,
feature_id: str,
*,
conn=None,
) -> Dict[str, Any]:
"""
Prüft Vereins-Kontingent für ein Feature.
Returns:
allowed, limit, used, remaining, reason, plan_id, reset_at (optional)
"""
if conn is not None:
return _check_club_impl(club_id, feature_id, conn)
with get_db() as c:
return _check_club_impl(club_id, feature_id, c)
def _check_club_impl(club_id: int, feature_id: str, conn) -> Dict[str, Any]:
cur = get_cursor(conn)
cur.execute(
"""
SELECT id, limit_type, reset_period, default_limit, active, enforcement_subject
FROM features
WHERE id = %s AND app = 'shinkan'
""",
(feature_id,),
)
feature = cur.fetchone()
if not feature or not feature.get("active"):
return {
"allowed": False,
"limit": None,
"used": 0,
"remaining": None,
"reason": "feature_not_found",
"plan_id": get_effective_club_plan(cur, club_id),
}
plan_id = get_effective_club_plan(cur, club_id)
limit = _resolve_club_limit(cur, club_id, feature_id, feature)
limit_type = (feature.get("limit_type") or "count").strip().lower()
if limit_type == "boolean":
allowed = limit == 1
return {
"allowed": allowed,
"limit": limit,
"used": 0,
"remaining": None,
"reason": "enabled" if allowed else "feature_disabled",
"plan_id": plan_id,
}
cur.execute(
"""
SELECT usage_count, reset_at
FROM club_feature_usage
WHERE club_id = %s AND feature_id = %s
""",
(club_id, feature_id),
)
usage = cur.fetchone()
used = _maybe_reset_usage(cur, conn, club_id, feature_id, feature, usage)
period = (feature.get("reset_period") or "never").strip().lower()
if period == "never" and feature_id in _INVENTORY_FEATURES:
inv = _live_inventory_count(cur, club_id, feature_id)
if inv is not None:
used = inv
if limit is None:
return {
"allowed": True,
"limit": None,
"used": used,
"remaining": None,
"reason": "unlimited",
"plan_id": plan_id,
"reset_at": usage.get("reset_at") if usage else None,
}
if limit == 0:
return {
"allowed": False,
"limit": 0,
"used": used,
"remaining": 0,
"reason": "feature_disabled",
"plan_id": plan_id,
"reset_at": usage.get("reset_at") if usage else None,
}
allowed = used < limit
return {
"allowed": allowed,
"limit": limit,
"used": used,
"remaining": max(0, limit - used),
"reason": "within_limit" if allowed else "limit_exceeded",
"plan_id": plan_id,
"reset_at": usage.get("reset_at") if usage else None,
}
def club_feature_enforcement_enabled() -> bool:
"""Phase 4: Hard-Block aktiv (Env CLUB_FEATURE_ENFORCE=1|true|yes)."""
v = os.getenv("CLUB_FEATURE_ENFORCE", "0").strip().lower()
return v in ("1", "true", "yes")
def probe_club_feature_access(
*,
feature_id: str,
action: str,
club_id: Optional[int] = None,
profile_id: Optional[int] = None,
portal_role: Optional[str] = None,
endpoint: Optional[str] = None,
tenant: Optional["TenantContext"] = None,
conn=None,
) -> Dict[str, Any]:
"""
Phase 2: Prüft Vereins-Kontingent, schreibt JSON-Log, blockiert standardmäßig nicht.
Bei CLUB_FEATURE_ENFORCE=1: HTTP 403 wenn nicht allowed.
"""
from club_feature_logger import log_club_feature_usage
if club_id is None:
access = {
"allowed": not club_feature_enforcement_enabled(),
"limit": None,
"used": 0,
"remaining": None,
"reason": "no_club_context",
"plan_id": None,
}
log_club_feature_usage(
club_id=None,
profile_id=profile_id,
feature_id=feature_id,
action=action,
access=access,
endpoint=endpoint,
phase="enforce" if club_feature_enforcement_enabled() else "probe",
)
if club_feature_enforcement_enabled() and not access.get("allowed"):
raise HTTPException(
status_code=403,
detail=(
f"Kein Vereinskontext für {feature_id}"
"aktiven Verein wählen (X-Active-Club-Id)."
),
)
return access
def _resolve_access(connection):
from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access
cur = get_cursor(connection)
if is_club_feature_quota_bypassed(
cur,
profile_id=profile_id,
portal_role=portal_role,
feature_id=feature_id,
tenant=tenant,
):
plan_id = get_effective_club_plan(cur, int(club_id))
return quota_bypass_access(
feature_id=feature_id,
club_id=int(club_id),
plan_id=plan_id,
)
return check_club_feature_access(club_id, feature_id, conn=connection)
if conn is not None:
access = _resolve_access(conn)
else:
with get_db() as c:
access = _resolve_access(c)
log_club_feature_usage(
club_id=club_id,
profile_id=profile_id,
feature_id=feature_id,
action=action,
access=access,
endpoint=endpoint,
phase="enforce" if club_feature_enforcement_enabled() else "probe",
)
if club_feature_enforcement_enabled() and not access.get("allowed"):
limit = access.get("limit")
used = access.get("used", 0)
detail = (
f"Kontingent überschritten für {feature_id} "
f"({used}/{limit if limit is not None else ''}). "
f"Grund: {access.get('reason', 'limit_exceeded')}."
)
raise HTTPException(status_code=403, detail=detail)
return access
def consume_club_feature(
*,
feature_id: str,
club_id: Optional[int],
profile_id: Optional[int] = None,
portal_role: Optional[str] = None,
action: Optional[str] = None,
amount: int = 1,
conn=None,
) -> None:
"""
Phase 4 (M5): Zähler nach erfolgreichem Verbrauch erhöhen.
Nur wenn club_id gesetzt (Vereins-Kontingent); amount = Anzahl LLM/API-Verbrauchseinheiten.
Plattform-Ausnahmen (superadmin, konfigurierte Rollen/Profile) werden nicht gezählt.
"""
if club_id is None:
return
def _is_exempt(connection) -> bool:
from club_quota_bypass import is_club_feature_quota_bypassed
cur = get_cursor(connection)
return is_club_feature_quota_bypassed(
cur,
profile_id=profile_id,
portal_role=portal_role,
feature_id=feature_id,
)
if conn is not None:
if _is_exempt(conn):
return
else:
with get_db() as c:
if _is_exempt(c):
return
try:
n = int(amount)
except (TypeError, ValueError):
n = 1
if n < 1:
return
for _ in range(n):
increment_club_feature_usage(
int(club_id),
feature_id,
profile_id=profile_id,
action=action,
conn=conn,
)
def _log_consume(connection) -> None:
from club_feature_logger import log_club_feature_usage
access = check_club_feature_access(int(club_id), feature_id, conn=connection)
log_club_feature_usage(
club_id=int(club_id),
profile_id=profile_id,
feature_id=feature_id,
action=action or "consume",
access=access,
phase="consume",
)
if conn is not None:
_log_consume(conn)
else:
with get_db() as c:
_log_consume(c)
def consume_club_feature_with_usage(
*,
feature_id: str,
club_id: Optional[int],
profile_id: Optional[int] = None,
portal_role: Optional[str] = None,
action: Optional[str] = None,
amount: int = 1,
cur,
tenant: Optional["TenantContext"] = None,
conn=None,
) -> Optional[Dict[str, Dict[str, Any]]]:
"""
Standard nach erfolgreichem Verbrauch: zählen, protokollieren, Snapshot für Response.
Alle Endpoints mit Vereins-Kontingent-Verbrauch nutzen diese Funktion und
``merge_feature_usage_into_response`` kein duplizierter Einzelcode pro Route.
"""
consume_club_feature(
feature_id=feature_id,
club_id=club_id,
profile_id=profile_id,
portal_role=portal_role,
action=action,
amount=amount,
conn=conn,
)
if club_id is None:
return None
return {
feature_id: club_feature_usage_for_api(
cur,
club_id=int(club_id),
feature_id=feature_id,
profile_id=profile_id,
portal_role=portal_role,
tenant=tenant,
conn=conn,
),
}
def merge_feature_usage_into_response(
payload: Any,
feature_usage: Optional[Dict[str, Dict[str, Any]]],
) -> Any:
"""Standard-Einbettung ``feature_usage`` in JSON-Responses."""
if not feature_usage or not isinstance(payload, dict):
return payload
return {**payload, "feature_usage": feature_usage}
def club_feature_usage_for_api(
cur,
*,
club_id: int,
feature_id: str,
profile_id: Optional[int] = None,
portal_role: Optional[str] = None,
tenant: Optional["TenantContext"] = None,
conn=None,
) -> Dict[str, Any]:
"""Feature-Zustand wie GET /me/entitlements → features[feature_id] (nach Verbrauch)."""
from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access
db_conn = conn if conn is not None else cur.connection
access = check_club_feature_access(int(club_id), feature_id, conn=db_conn)
plan_id = access.get("plan_id") or get_effective_club_plan(cur, int(club_id))
if is_club_feature_quota_bypassed(
cur,
profile_id=profile_id,
portal_role=portal_role,
feature_id=feature_id,
tenant=tenant,
):
ex = quota_bypass_access(
feature_id=feature_id,
club_id=int(club_id),
plan_id=plan_id,
)
reset_at = access.get("reset_at")
return {
"allowed": True,
"used": access.get("used"),
"limit": None,
"remaining": None,
"reason": ex.get("reason"),
"platform_exempt": True,
"reset_at": reset_at.isoformat() if hasattr(reset_at, "isoformat") else reset_at,
}
return {
"allowed": access.get("allowed"),
"used": access.get("used"),
"limit": access.get("limit"),
"remaining": access.get("remaining"),
"reason": access.get("reason"),
"platform_exempt": False,
"reset_at": access.get("reset_at").isoformat()
if access.get("reset_at") is not None and hasattr(access.get("reset_at"), "isoformat")
else access.get("reset_at"),
}
def increment_club_feature_usage(
club_id: int,
feature_id: str,
*,
profile_id: Optional[int] = None,
action: Optional[str] = None,
conn=None,
) -> None:
"""Erhöht Vereins-Zähler (nur bei neuem Verbrauch / INSERT-Pfad aufrufen)."""
def _run(c):
cur = get_cursor(c)
cur.execute(
"""
SELECT reset_period, limit_type
FROM features
WHERE id = %s AND app = 'shinkan' AND active = true
""",
(feature_id,),
)
feature = cur.fetchone()
if not feature:
return
if (feature.get("limit_type") or "count").strip().lower() == "boolean":
return
period = (feature.get("reset_period") or "never").strip().lower()
next_reset = _calculate_next_reset(period)
cur.execute(
"""
INSERT INTO club_feature_usage (club_id, feature_id, usage_count, reset_at, last_used_at)
VALUES (%s, %s, 1, %s, NOW())
ON CONFLICT (club_id, feature_id)
DO UPDATE SET
usage_count = club_feature_usage.usage_count + 1,
last_used_at = NOW(),
updated_at = NOW()
""",
(club_id, feature_id, next_reset),
)
if profile_id is not None or action:
cur.execute(
"""
INSERT INTO club_feature_usage_events (club_id, feature_id, profile_id, action)
VALUES (%s, %s, %s, %s)
""",
(club_id, feature_id, profile_id, action or feature_id),
)
if conn is not None:
_run(conn)
else:
with get_db() as c:
_run(c)
def list_club_entitlements(cur, club_id: int, *, conn=None) -> Dict[str, Any]:
"""Alle aktiven Shinkan-Features mit effektivem Limit und Verbrauch (Liste, intern)."""
db_conn = conn if conn is not None else cur.connection
plan_id = get_effective_club_plan(cur, club_id)
cur.execute(
"""
SELECT id, name, category, limit_type, reset_period
FROM features
WHERE app = 'shinkan' AND active = true
ORDER BY category, id
"""
)
rows = cur.fetchall()
features_out = []
for row in rows:
fid = row["id"]
access = _check_club_impl(club_id, fid, db_conn)
features_out.append(
{
"id": fid,
"name": row.get("name"),
"category": row.get("category"),
"limit_type": row.get("limit_type"),
"reset_period": row.get("reset_period"),
"allowed": access.get("allowed"),
"limit": access.get("limit"),
"used": access.get("used"),
"remaining": access.get("remaining"),
"reason": access.get("reason"),
"reset_at": access.get("reset_at"),
}
)
return {"club_id": club_id, "plan_id": plan_id, "features": features_out}
def club_features_map(cur, club_id: int, *, conn=None) -> Dict[str, Any]:
"""Feature-Kontingente als Dict feature_id → Zustand (für /me/entitlements)."""
raw = list_club_entitlements(cur, club_id, conn=conn)
features_dict: Dict[str, Any] = {}
for row in raw.get("features") or []:
fid = row["id"]
features_dict[fid] = {
"name": row.get("name"),
"category": row.get("category"),
"limit_type": row.get("limit_type"),
"reset_period": row.get("reset_period"),
"allowed": row.get("allowed"),
"limit": row.get("limit"),
"used": row.get("used"),
"remaining": row.get("remaining"),
"reason": row.get("reason"),
"reset_at": row.get("reset_at"),
}
return {
"club_id": raw.get("club_id"),
"plan_id": raw.get("plan_id"),
"features": features_dict,
}

View File

@ -0,0 +1,180 @@
"""
Vereins-Kontingent-Bypass über das Capability-System (kein Parallel-Rechtemodell).
Capabilities:
- platform.club_quota.bypass alle Vereins-Features (Portal-Admin, Grant via portal_role)
- platform.club_quota.bypass.{feature_id} ein Feature (domain quota_bypass, auch für Nicht-Admins per Grant)
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from tenant_context import TenantContext
QUOTA_BYPASS_ALL = "platform.club_quota.bypass"
QUOTA_BYPASS_FEATURE_PREFIX = "platform.club_quota.bypass."
def quota_bypass_capability_id_for_feature(feature_id: str) -> str:
return f"{QUOTA_BYPASS_FEATURE_PREFIX}{feature_id}"
def ensure_quota_bypass_capability(cur, feature_id: str) -> str:
"""Legt feature-spezifische Bypass-Capability an falls nötig."""
cap_id = quota_bypass_capability_id_for_feature(feature_id)
cur.execute(
"""
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
VALUES (%s, %s, 'quota_bypass', 'active_member', %s)
ON CONFLICT (id) DO NOTHING
""",
(cap_id, f"Vereins-Kontingent umgehen: {feature_id}", feature_id),
)
return cap_id
def _bypass_capability_ids(cur, feature_id: str) -> List[str]:
ids: List[str] = [QUOTA_BYPASS_ALL, quota_bypass_capability_id_for_feature(feature_id)]
cur.execute(
"""
SELECT id FROM capabilities
WHERE active = true
AND domain = 'quota_bypass'
AND linked_feature_id = %s
AND id <> %s
""",
(feature_id, quota_bypass_capability_id_for_feature(feature_id)),
)
for row in cur.fetchall():
cid = row.get("id")
if cid and cid not in ids:
ids.append(str(cid))
return ids
def _portal_role_has_grant(cur, portal_role: str, capability_id: str) -> bool:
role = (portal_role or "").strip().lower()
if not role:
return False
cur.execute(
"""
SELECT 1 FROM portal_role_capability_grants
WHERE portal_role = %s AND capability_id = %s
LIMIT 1
""",
(role, capability_id),
)
return cur.fetchone() is not None
def _profile_has_grant(cur, profile_id: int, capability_id: str) -> bool:
cur.execute(
"""
SELECT 1 FROM profile_capability_grants
WHERE profile_id = %s AND capability_id = %s
LIMIT 1
""",
(int(profile_id), capability_id),
)
return cur.fetchone() is not None
def is_club_feature_quota_bypassed(
cur,
*,
profile_id: Optional[int],
portal_role: Optional[str],
feature_id: str,
tenant: Optional["TenantContext"] = None,
) -> bool:
"""
True wenn ein konfigurierter Capability-Grant das Vereins-Kontingent für feature_id umgeht.
"""
if tenant is not None:
from capabilities import check_capability
for cap_id in _bypass_capability_ids(cur, feature_id):
if check_capability(cur, tenant, cap_id).get("allowed"):
return True
return False
for cap_id in _bypass_capability_ids(cur, feature_id):
if _portal_role_has_grant(cur, portal_role or "", cap_id):
return True
if profile_id is not None and _profile_has_grant(cur, int(profile_id), cap_id):
return True
return False
def quota_bypass_access(
*,
feature_id: str,
club_id: Optional[int] = None,
plan_id: Optional[str] = None,
capability_id: Optional[str] = None,
) -> Dict[str, Any]:
return {
"allowed": True,
"limit": None,
"used": 0,
"remaining": None,
"reason": "capability_quota_bypass",
"platform_exempt": True,
"quota_bypass_capability": capability_id,
"plan_id": plan_id,
"club_id": club_id,
"feature_id": feature_id,
}
def list_quota_bypass_grants(cur) -> Dict[str, Any]:
"""Admin: alle Grants zu Kontingent-Bypass-Capabilities."""
cur.execute(
"""
SELECT g.portal_role, g.capability_id, c.name AS capability_name,
c.linked_feature_id, c.domain
FROM portal_role_capability_grants g
INNER JOIN capabilities c ON c.id = g.capability_id
WHERE g.capability_id = %s
OR g.capability_id LIKE %s
OR c.domain = 'quota_bypass'
ORDER BY g.portal_role, g.capability_id
""",
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
)
portal_grants = [dict(r) for r in cur.fetchall()]
cur.execute(
"""
SELECT g.profile_id, p.email, p.name AS profile_name,
g.capability_id, c.name AS capability_name, c.linked_feature_id,
g.reason, g.granted_by_profile_id, g.created_at
FROM profile_capability_grants g
INNER JOIN profiles p ON p.id = g.profile_id
INNER JOIN capabilities c ON c.id = g.capability_id
WHERE g.capability_id = %s
OR g.capability_id LIKE %s
OR c.domain = 'quota_bypass'
ORDER BY g.profile_id, g.capability_id
""",
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
)
profile_grants = [dict(r) for r in cur.fetchall()]
cur.execute(
"""
SELECT id, name, domain, linked_feature_id
FROM capabilities
WHERE id = %s OR id LIKE %s OR domain = 'quota_bypass'
ORDER BY id
""",
(QUOTA_BYPASS_ALL, f"{QUOTA_BYPASS_FEATURE_PREFIX}%"),
)
capabilities = [dict(r) for r in cur.fetchall()]
return {
"capabilities": capabilities,
"portal_role_grants": portal_grants,
"profile_grants": profile_grants,
}

View File

@ -3,7 +3,7 @@ Vereins-Mandanten: Mitgliedschaften, aktiver Vereinskontext, einfache Berechtigu
Siehe .claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md Siehe .claude/docs/technical/MULTI_TENANCY_RBAC_ARCHITECTURE.md
""" """
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Mapping, Optional, Set, Union
from fastapi import HTTPException from fastapi import HTTPException
@ -155,6 +155,165 @@ def club_ids_for_profile_with_roles(cur, profile_id: int, *role_codes: str) -> S
_GOVERNANCE_VISIBILITY = frozenset({"private", "club", "official"}) _GOVERNANCE_VISIBILITY = frozenset({"private", "club", "official"})
def _library_governance_triplet(
row: Mapping[str, Any],
) -> tuple[str, Optional[int], Optional[int]]:
"""visibility, club_id, created_by als normalisierte Werte für Bibliotheks-/Planungsartefakte."""
vis = str(row.get("visibility") or "private").strip().lower()
if vis not in _GOVERNANCE_VISIBILITY:
vis = "private"
cid_raw = row.get("club_id")
try:
ex_cid = int(cid_raw) if cid_raw is not None else None
except (TypeError, ValueError):
ex_cid = None
cr_raw = row.get("created_by")
try:
creator = int(cr_raw) if cr_raw is not None else None
except (TypeError, ValueError):
creator = None
return vis, ex_cid, creator
def assert_library_content_editable(
cur,
profile_id: int,
role: Optional[str],
row: Union[Dict[str, Any], Mapping[str, Any]],
) -> None:
"""Inhalt bearbeiten: wie Übungen — Ersteller, Plattform-Admin oder Planungsberechtigter im Verein."""
pid = int(profile_id)
ex_vis, ex_cid, creator = _library_governance_triplet(row)
if creator is not None and creator == pid:
return
if is_platform_admin(role):
return
if ex_vis == "club" and ex_cid is not None and can_plan_in_club(cur, pid, ex_cid, role):
return
raise HTTPException(status_code=403, detail="Keine Berechtigung zum Bearbeiten dieses Inhalts")
def assert_library_content_deletable(
cur,
profile_id: int,
role: Optional[str],
row: Union[Dict[str, Any], Mapping[str, Any]],
) -> None:
"""Löschen: wie Übungen — privat Eigentümer/Vereins-Admin-Kontext, Verein nur Vereinsadmin, offiziell nur Plattform."""
pid = int(profile_id)
if is_platform_admin(role):
return
vis, cid, creator = _library_governance_triplet(row)
try:
creator_int = int(creator) if creator is not None else None
except (TypeError, ValueError):
creator_int = None
if vis == "official":
raise HTTPException(
status_code=403,
detail="Offizielle Inhalte dürfen nur von Plattform-Admins gelöscht werden.",
)
if vis == "club":
try:
ex_club = int(cid) if cid is not None else None
except (TypeError, ValueError):
ex_club = None
if ex_club is None:
raise HTTPException(status_code=400, detail="Vereinsinhalt ohne gültige Vereinszuordnung")
if not has_club_role(cur, pid, ex_club, "club_admin"):
raise HTTPException(
status_code=403,
detail="Nur Vereins-Admins dürfen Vereins-Inhalte löschen.",
)
return
if creator_int is not None and creator_int == pid:
return
if creator_int is not None and club_admin_shares_club_with_creator(cur, pid, creator_int):
return
raise HTTPException(status_code=403, detail="Keine Berechtigung zum Löschen dieses Inhalts")
def assert_library_content_governance_transition(
cur,
profile_id: int,
role: Optional[str],
prev_row: Union[Dict[str, Any], Mapping[str, Any]],
next_visibility: str,
next_club_id: Optional[int],
) -> None:
"""
Zusätzliche Regeln beim Ändern von visibility/club_id (Zielzustand vor assert_valid_governance_visibility prüfen).
- Abwahl official: nur Plattform-Admin.
- private club: nur Ersteller (oder Plattform-Admin).
- club private: Ersteller, Vereinsadmin im bisherigen Verein oder Plattform-Admin.
- club club mit Wechsel club_id: Vereinsadmin im alten oder neuen Verein oder Plattform-Admin.
"""
nv = str(next_visibility or "").strip().lower()
if nv not in _GOVERNANCE_VISIBILITY:
raise HTTPException(status_code=400, detail="Ungültige visibility")
old_vis, old_cid, creator = _library_governance_triplet(prev_row)
new_cid: Optional[int]
try:
new_cid = int(next_club_id) if next_club_id is not None else None
except (TypeError, ValueError):
new_cid = None
pid = int(profile_id)
try:
creator_int = int(creator) if creator is not None else None
except (TypeError, ValueError):
creator_int = None
if old_vis == nv and (nv != "club" or old_cid == new_cid):
return
if old_vis == "official" and nv != "official":
if not is_platform_admin(role):
raise HTTPException(
status_code=403,
detail="Nur Plattform-Admins dürfen offizielle Inhalte auf Verein oder privat setzen.",
)
if nv == "official":
return
if old_vis == "private" and nv == "club":
if creator_int is not None and creator_int != pid and not is_platform_admin(role):
raise HTTPException(
status_code=403,
detail="Nur der Ersteller darf private Inhalte für den Verein freigeben.",
)
return
if old_vis == "club" and nv == "private":
if is_platform_admin(role):
return
if creator_int is not None and creator_int == pid:
return
if old_cid is not None and has_club_role(cur, pid, old_cid, "club_admin"):
return
raise HTTPException(
status_code=403,
detail="Nur Ersteller, Vereins-Admins oder Plattform-Admins dürfen Vereins-Inhalte auf privat setzen.",
)
if old_vis == "club" and nv == "club" and old_cid != new_cid:
if is_platform_admin(role):
return
ok_old = old_cid is not None and has_club_role(cur, pid, old_cid, "club_admin")
ok_new = new_cid is not None and has_club_role(cur, pid, new_cid, "club_admin")
if ok_old or ok_new:
return
raise HTTPException(
status_code=403,
detail="Nur Vereins-Admins oder Plattform-Admins dürfen die Vereinszuordnung ändern.",
)
def assert_valid_governance_visibility( def assert_valid_governance_visibility(
cur, cur,
profile_id: int, profile_id: int,

View File

@ -180,12 +180,17 @@ def init_db():
cur.execute("SELECT COUNT(*) as count FROM ai_prompts WHERE slug='pipeline'") cur.execute("SELECT COUNT(*) as count FROM ai_prompts WHERE slug='pipeline'")
if cur.fetchone()['count'] == 0: if cur.fetchone()['count'] == 0:
cur.execute(""" cur.execute("""
INSERT INTO ai_prompts (slug, name, description, template, active, sort_order) INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, active, sort_order
)
VALUES ( VALUES (
'pipeline', 'pipeline',
'Mehrstufige Gesamtanalyse', 'Mehrstufige Gesamtanalyse',
'Master-Schalter für die gesamte Pipeline. Deaktiviere diese Analyse, um die Pipeline komplett zu verstecken.', 'Master-Schalter fuer die gesamte Pipeline. Deaktiviere diese Zeile um die Pipeline zu verstecken.',
'PIPELINE_MASTER', 'PIPELINE_MASTER',
'admin',
'text',
true, true,
-10 -10
) )

113
backend/entitlements.py Normal file
View File

@ -0,0 +1,113 @@
"""
Zusammenstellung effektiver Rechte für GET /api/me/entitlements (M4).
Spez: CAPABILITY_CATALOG.v1.md §7.1, CLUB_MEMBERSHIP_AND_FEATURES.v1.md §8.1
"""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, Optional, TYPE_CHECKING
from fastapi import HTTPException
from capabilities import club_roles_in_club, resolve_capabilities_map
from club_quota_bypass import is_club_feature_quota_bypassed, quota_bypass_access
from club_features import club_features_map
from club_tenancy import is_platform_admin
from tenant_context import _club_exists
if TYPE_CHECKING:
from tenant_context import TenantContext
def _serialize_reset_at(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, datetime):
if value.tzinfo is None:
return value.replace(tzinfo=None).isoformat() + "Z"
return value.isoformat()
return str(value)
def _resolve_target_club_id(
cur,
tenant: "TenantContext",
club_id: Optional[int],
) -> Optional[int]:
"""Effektiver Verein für Entitlements (Query > Tenant)."""
target = int(club_id) if club_id is not None else tenant.effective_club_id
if target is None:
return None
if is_platform_admin(tenant.global_role):
if not _club_exists(cur, target):
raise HTTPException(status_code=400, detail="Verein nicht gefunden")
return target
if target not in tenant.club_ids:
raise HTTPException(status_code=403, detail="Keine Mitgliedschaft in diesem Verein")
return target
def build_me_entitlements(
cur,
tenant: "TenantContext",
*,
club_id: Optional[int] = None,
) -> Dict[str, Any]:
"""
Kombiniert Account-Status, Capabilities und Feature-Kontingente.
"""
target_club = _resolve_target_club_id(cur, tenant, club_id)
club_roles = club_roles_in_club(tenant, target_club) if target_club is not None else []
capabilities = resolve_capabilities_map(cur, tenant, club_id=target_club)
features: Dict[str, Any] = {}
plan_id = None
if target_club is not None:
raw = club_features_map(cur, target_club)
plan_id = raw.get("plan_id")
for fid, row in (raw.get("features") or {}).items():
if is_club_feature_quota_bypassed(
cur,
profile_id=tenant.profile_id,
portal_role=tenant.global_role,
feature_id=fid,
tenant=tenant,
):
ex = quota_bypass_access(
feature_id=fid,
club_id=target_club,
plan_id=plan_id,
)
features[fid] = {
"allowed": True,
"used": row.get("used"),
"limit": None,
"remaining": None,
"reset_at": _serialize_reset_at(row.get("reset_at")),
"reason": ex.get("reason"),
"platform_exempt": True,
}
else:
features[fid] = {
"allowed": row.get("allowed"),
"used": row.get("used"),
"limit": row.get("limit"),
"remaining": row.get("remaining"),
"reset_at": _serialize_reset_at(row.get("reset_at")),
"reason": row.get("reason"),
"platform_exempt": False,
}
return {
"account_state": tenant.account_state,
"portal_role": tenant.global_role,
"club_id": target_club,
"plan_id": plan_id,
"club_roles": club_roles,
"capabilities": capabilities,
"features": features,
}

1122
backend/exercise_ai.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,536 @@
"""
Superadmin-Werkzeug: Übungs-Anreicherung per KI (Skills + optional Metadaten).
Wiederverwendet run_exercise_form_ai_suggestion / exercise_ai keine neue OpenRouter-Pipeline.
"""
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional
from ai_prompt_context import ExerciseFormAiPromptContext
from ai_prompt_job import run_exercise_form_ai_suggestion
from exercise_ai import strip_html_to_plain
from exercise_rich_text import normalize_inline_exercise_media_markup
from routers.exercises import (
enrich_exercise_detail,
normalize_exercise_skill_intensity,
normalize_exercise_skill_level,
)
SkillMergeMode = Literal["additive", "replace_ai_only", "replace_all"]
SKILL_MERGE_MODES = frozenset({"additive", "replace_ai_only", "replace_all"})
DEFAULT_SET_STATUS = "in_review"
# Max. IDs pro Apply-HTTP-Anfrage (kein LLM).
MAX_BATCH_EXERCISES = 50
# Preview: pro Request nur wenige Übungen — sonst Gateway-504 (Fritz!Box o.ä. ~60s).
MAX_PREVIEW_BATCH_EXERCISES = 3
_INSTRUCTION_FIELDS = ("goal", "execution", "preparation", "trainer_notes")
_SKILL_COMPARE_KEYS = ("intensity", "required_level", "target_level", "is_primary")
def _focus_areas_ai_ctx_from_detail(exercise: Dict[str, Any]) -> list[tuple[int, bool]]:
rows: list[tuple[int, bool]] = []
for row in exercise.get("focus_areas") or []:
if not isinstance(row, dict):
continue
try:
fid = int(row.get("focus_area_id"))
except (TypeError, ValueError):
continue
if fid < 1:
continue
rows.append((fid, bool(row.get("is_primary"))))
rows.sort(key=lambda x: (not x[1], x[0]))
return rows
def _focus_area_hint_from_detail(exercise: Dict[str, Any]) -> str:
parts: List[str] = []
for row in exercise.get("focus_areas") or []:
if isinstance(row, dict):
nm = (row.get("name") or "").strip()
if nm:
parts.append(nm)
txt = ", ".join(parts).strip()
if len(txt) > 900:
return txt[:899] + ""
return txt
def build_form_context_from_exercise(exercise: Dict[str, Any]) -> ExerciseFormAiPromptContext:
focus = _focus_area_hint_from_detail(exercise)
fctx = _focus_areas_ai_ctx_from_detail(exercise)
return ExerciseFormAiPromptContext.from_focus_tuples(
title=str(exercise.get("title") or "").strip(),
goal=exercise.get("goal"),
execution=exercise.get("execution"),
preparation=exercise.get("preparation"),
trainer_notes=exercise.get("trainer_notes"),
focus_hint=focus or None,
focus_tuples=fctx or None,
)
def validate_exercise_for_enrichment(
exercise: Dict[str, Any],
*,
want_skills: bool = False,
want_summary: bool = False,
want_instructions: bool = False,
) -> Optional[str]:
title = str(exercise.get("title") or "").strip()
if not title:
return "Titel fehlt"
ctx = build_form_context_from_exercise(exercise)
g_plain = strip_html_to_plain(exercise.get("goal"))
e_plain = strip_html_to_plain(exercise.get("execution"))
if want_skills or want_summary:
if not (g_plain.strip() or e_plain.strip()):
return "Mindestens Ziel oder Durchführung muss Inhalt liefern (für Skills/Kurzfassung)"
if want_instructions and not ctx.has_instruction_source_text():
return "Für Anleitungs-Überarbeitung fehlt Ausgangstext (Titel oder Anleitungsfeld)"
if not (want_skills or want_summary or want_instructions):
return "Kein Anreicherungsmodus aktiv"
return None
def _normalize_skill_row(raw: Dict[str, Any], *, ai_suggested: bool) -> Dict[str, Any]:
return {
"skill_id": int(raw["skill_id"]),
"skill_name": (raw.get("skill_name") or "").strip() or f"Skill #{raw['skill_id']}",
"skill_category": raw.get("skill_category"),
"is_primary": bool(raw.get("is_primary")),
"intensity": normalize_exercise_skill_intensity(raw.get("intensity")),
"required_level": normalize_exercise_skill_level(raw.get("required_level")),
"target_level": normalize_exercise_skill_level(raw.get("target_level")),
"ai_suggested": ai_suggested,
}
def _skill_meta_differs(a: Dict[str, Any], b: Dict[str, Any]) -> bool:
for k in _SKILL_COMPARE_KEYS:
av = a.get(k)
bv = b.get(k)
if k in ("required_level", "target_level"):
av = normalize_exercise_skill_level(av)
bv = normalize_exercise_skill_level(bv)
elif k == "intensity":
av = normalize_exercise_skill_intensity(av)
bv = normalize_exercise_skill_intensity(bv)
elif k == "is_primary":
av = bool(av)
bv = bool(bv)
if av != bv:
return True
return False
def merge_skills(
existing: List[Dict[str, Any]],
suggested: List[Dict[str, Any]],
mode: SkillMergeMode,
) -> List[Dict[str, Any]]:
"""Merge-Modi: additive | replace_ai_only | replace_all (alle KI-Skills mit ai_suggested=true)."""
existing_norm = [_normalize_skill_row(s, ai_suggested=bool(s.get("ai_suggested"))) for s in existing]
suggested_norm = [_normalize_skill_row(s, ai_suggested=True) for s in suggested]
suggested_by_id = {int(s["skill_id"]): s for s in suggested_norm}
if mode == "replace_all":
return list(suggested_norm)
if mode == "replace_ai_only":
manual = [s for s in existing_norm if not s.get("ai_suggested")]
manual_ids = {int(s["skill_id"]) for s in manual}
result = list(manual)
for s in suggested_norm:
sid = int(s["skill_id"])
if sid in manual_ids:
continue
result.append(s)
return result
# additive
result: List[Dict[str, Any]] = []
seen: set[int] = set()
for s in existing_norm:
sid = int(s["skill_id"])
seen.add(sid)
if sid in suggested_by_id and s.get("ai_suggested"):
merged = {**s, **suggested_by_id[sid], "ai_suggested": True}
result.append(merged)
else:
result.append(dict(s))
for s in suggested_norm:
sid = int(s["skill_id"])
if sid not in seen:
result.append(s)
seen.add(sid)
return result
def compute_skill_diff(
before: List[Dict[str, Any]],
after: List[Dict[str, Any]],
) -> Dict[str, Any]:
before_ids = {int(s["skill_id"]): s for s in before}
after_ids = {int(s["skill_id"]): s for s in after}
added = [after_ids[i] for i in sorted(after_ids) if i not in before_ids]
removed = [before_ids[i] for i in sorted(before_ids) if i not in after_ids]
changed: List[Dict[str, Any]] = []
for sid in before_ids:
if sid in after_ids and _skill_meta_differs(before_ids[sid], after_ids[sid]):
changed.append(
{
"skill_id": sid,
"skill_name": after_ids[sid].get("skill_name") or before_ids[sid].get("skill_name"),
"before": before_ids[sid],
"after": after_ids[sid],
}
)
kept = [
before_ids[i]
for i in sorted(before_ids)
if i in after_ids and i not in {c["skill_id"] for c in changed}
]
return {"added": added, "removed": removed, "changed": changed, "kept": kept}
def _skills_from_ai_payload(payload: Dict[str, Any]) -> List[Dict[str, Any]]:
rows = payload.get("skills")
if not isinstance(rows, list):
return []
return [_normalize_skill_row(r, ai_suggested=True) for r in rows if isinstance(r, dict) and r.get("skill_id")]
def _summary_from_ai_payload(payload: Dict[str, Any]) -> Optional[str]:
block = payload.get("summary")
if isinstance(block, dict):
text = (block.get("text") or "").strip()
return text or None
if isinstance(block, str) and block.strip():
return block.strip()
return None
def _instructions_from_ai_payload(payload: Dict[str, Any]) -> Dict[str, str]:
block = payload.get("instructions")
if not isinstance(block, dict):
return {}
fields = block.get("fields")
if not isinstance(fields, dict):
return {}
out: Dict[str, str] = {}
for key in _INSTRUCTION_FIELDS:
val = fields.get(key)
if val is not None and str(val).strip():
out[key] = str(val).strip()
return out
def _instruction_snapshot(exercise: Dict[str, Any]) -> Dict[str, str]:
out: Dict[str, str] = {}
for key in _INSTRUCTION_FIELDS:
raw = exercise.get(key)
plain = strip_html_to_plain(raw, max_len=400) if raw else ""
if plain.strip():
out[key] = plain.strip()
return out
def compute_instruction_diff(
before: Dict[str, str],
after: Dict[str, str],
) -> Dict[str, Any]:
changed: List[Dict[str, Any]] = []
added: List[str] = []
for key in _INSTRUCTION_FIELDS:
b = (before.get(key) or "").strip()
a = (after.get(key) or "").strip()
if not a:
continue
if not b:
added.append(key)
elif b != strip_html_to_plain(a, max_len=400).strip() and b != a:
changed.append({"field": key, "before_plain": b, "after_html": a})
return {"changed_fields": changed, "added_fields": added}
def preview_exercise_enrichment(
cur,
exercise_id: int,
*,
want_skills: bool = True,
want_summary: bool = False,
want_instructions: bool = False,
merge_mode: SkillMergeMode = "additive",
) -> Dict[str, Any]:
exercise = enrich_exercise_detail(exercise_id, cur)
if not exercise:
return {"exercise_id": exercise_id, "ok": False, "error": "Übung nicht gefunden"}
skip_reason = validate_exercise_for_enrichment(
exercise,
want_skills=want_skills,
want_summary=want_summary,
want_instructions=want_instructions,
)
if skip_reason:
return {
"exercise_id": exercise_id,
"ok": False,
"skipped": True,
"error": skip_reason,
"title": exercise.get("title"),
"status": exercise.get("status"),
}
existing = exercise.get("skills") or []
suggested: List[Dict[str, Any]] = []
ai_meta: Dict[str, Any] = {}
payload: Dict[str, Any] = {}
suggested_summary: Optional[str] = None
suggested_instructions: Dict[str, str] = {}
if want_skills or want_summary or want_instructions:
ctx = build_form_context_from_exercise(exercise)
payload = run_exercise_form_ai_suggestion(
cur,
ctx,
want_summary=want_summary,
want_skills=want_skills,
want_instructions=want_instructions,
)
if want_skills:
suggested = _skills_from_ai_payload(payload)
if want_summary:
suggested_summary = _summary_from_ai_payload(payload)
if want_instructions:
suggested_instructions = _instructions_from_ai_payload(payload)
ai_meta = {
"models": payload.get("models_by_slug") or {},
"llm_calls": sum([want_skills, want_summary, want_instructions]),
}
merged = merge_skills(existing, suggested, merge_mode) if want_skills else list(existing)
diff = compute_skill_diff(existing, merged) if want_skills else None
existing_summary = (exercise.get("summary") or "").strip() or None
instr_before = _instruction_snapshot(exercise)
instr_after_plain = {
k: strip_html_to_plain(v, max_len=400) for k, v in suggested_instructions.items()
}
instruction_diff = (
compute_instruction_diff(instr_before, instr_after_plain) if want_instructions else None
)
return {
"exercise_id": exercise_id,
"ok": True,
"title": exercise.get("title"),
"status": exercise.get("status"),
"visibility": exercise.get("visibility"),
"primary_focus_name": _primary_focus_from_exercise(exercise),
"existing_skills": existing,
"suggested_skills": suggested,
"merged_skills": merged,
"diff": diff,
"existing_summary": existing_summary,
"suggested_summary": suggested_summary,
"existing_instructions": instr_before,
"suggested_instructions": suggested_instructions,
"instruction_diff": instruction_diff,
"ai_meta": ai_meta,
}
def _primary_focus_from_exercise(exercise: Dict[str, Any]) -> Optional[str]:
for row in exercise.get("focus_areas") or []:
if isinstance(row, dict) and row.get("is_primary"):
return (row.get("name") or "").strip() or None
for row in exercise.get("focus_areas") or []:
if isinstance(row, dict):
nm = (row.get("name") or "").strip()
if nm:
return nm
return None
def persist_merged_skills(cur, exercise_id: int, merged: List[Dict[str, Any]], merge_mode: SkillMergeMode) -> None:
if merge_mode == "replace_all":
cur.execute("DELETE FROM exercise_skills WHERE exercise_id = %s", (exercise_id,))
elif merge_mode == "replace_ai_only":
cur.execute(
"DELETE FROM exercise_skills WHERE exercise_id = %s AND ai_suggested = true",
(exercise_id,),
)
for sk in merged:
cur.execute(
"""
INSERT INTO exercise_skills
(exercise_id, skill_id, is_primary, intensity, required_level, target_level, ai_suggested)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (exercise_id, skill_id) DO UPDATE SET
intensity = CASE
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
THEN exercise_skills.intensity ELSE EXCLUDED.intensity END,
required_level = CASE
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
THEN exercise_skills.required_level ELSE EXCLUDED.required_level END,
target_level = CASE
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
THEN exercise_skills.target_level ELSE EXCLUDED.target_level END,
is_primary = CASE
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
THEN exercise_skills.is_primary ELSE EXCLUDED.is_primary END,
ai_suggested = CASE
WHEN exercise_skills.ai_suggested = false AND %s = 'additive'
THEN exercise_skills.ai_suggested ELSE EXCLUDED.ai_suggested END
""",
(
exercise_id,
int(sk["skill_id"]),
bool(sk.get("is_primary")),
normalize_exercise_skill_intensity(sk.get("intensity")),
normalize_exercise_skill_level(sk.get("required_level")),
normalize_exercise_skill_level(sk.get("target_level")),
bool(sk.get("ai_suggested")),
merge_mode,
merge_mode,
merge_mode,
merge_mode,
merge_mode,
),
)
def _normalize_instruction_fields(fields: Optional[Dict[str, Any]]) -> Dict[str, str]:
if not fields:
return {}
out: Dict[str, str] = {}
for key in _INSTRUCTION_FIELDS:
if key not in fields:
continue
raw = fields.get(key)
if raw is None or not str(raw).strip():
continue
out[key] = normalize_inline_exercise_media_markup(str(raw).strip())
return out
def apply_exercise_enrichment(
cur,
exercise_id: int,
*,
merged_skills: Optional[List[Dict[str, Any]]] = None,
merge_mode: SkillMergeMode = "additive",
set_status: Optional[str] = DEFAULT_SET_STATUS,
apply_skills: bool = False,
summary_text: Optional[str] = None,
apply_summary: bool = False,
instruction_fields: Optional[Dict[str, Any]] = None,
apply_instructions: bool = False,
) -> Dict[str, Any]:
exercise = enrich_exercise_detail(exercise_id, cur)
if not exercise:
return {"exercise_id": exercise_id, "ok": False, "error": "Übung nicht gefunden"}
skip_reason = validate_exercise_for_enrichment(
exercise,
want_skills=apply_skills,
want_summary=apply_summary,
want_instructions=apply_instructions,
)
if skip_reason:
return {
"exercise_id": exercise_id,
"ok": False,
"skipped": True,
"error": skip_reason,
}
skills_list = merged_skills or []
if apply_skills:
if not skills_list and merge_mode != "replace_all":
return {
"exercise_id": exercise_id,
"ok": False,
"error": "Keine Skills zum Anwenden",
}
persist_merged_skills(cur, exercise_id, skills_list, merge_mode)
sets: List[str] = []
vals: List[Any] = []
if apply_summary and summary_text is not None:
text = str(summary_text).strip()
if text:
sets.extend(["summary = %s", "summary_ai_generated = true"])
vals.append(text[:220])
if apply_instructions:
norm = _normalize_instruction_fields(instruction_fields)
for key, val in norm.items():
sets.append(f"{key} = %s")
vals.append(val)
new_status = (set_status or "").strip().lower() or None
if new_status:
if new_status == "approved":
return {
"exercise_id": exercise_id,
"ok": False,
"error": "Automatisches Freigeben (approved) ist nicht erlaubt",
}
if new_status not in ("draft", "in_review", "archived"):
return {"exercise_id": exercise_id, "ok": False, "error": "Ungültiger Ziel-Status"}
sets.append("status = %s")
vals.append(new_status)
if sets:
sets.append("updated_at = NOW()")
vals.append(exercise_id)
cur.execute(
f"UPDATE exercises SET {', '.join(sets)} WHERE id = %s",
tuple(vals),
)
elif not apply_skills:
return {"exercise_id": exercise_id, "ok": False, "error": "Nichts anzuwenden"}
return {
"exercise_id": exercise_id,
"ok": True,
"status": new_status or exercise.get("status"),
"skills_applied": len(skills_list) if apply_skills else 0,
"summary_applied": apply_summary and bool(summary_text and str(summary_text).strip()),
"instructions_applied": apply_instructions and bool(_normalize_instruction_fields(instruction_fields)),
}
def estimate_llm_calls(
*,
exercise_count: int,
want_skills: bool,
want_summary: bool,
want_instructions: bool = False,
) -> Dict[str, Any]:
per_skills = exercise_count if want_skills else 0
per_summary = exercise_count if want_summary else 0
per_instructions = exercise_count if want_instructions else 0
total = per_skills + per_summary + per_instructions
return {
"total": total,
"per_exercise": sum([want_skills, want_summary, want_instructions]),
"skills": per_skills,
"summary": per_summary,
"instructions": per_instructions,
}

View File

@ -52,6 +52,28 @@ else:
print(f"[FAIL] Migration-Laufzeitfehler: {e}") print(f"[FAIL] Migration-Laufzeitfehler: {e}")
sys.exit(1) sys.exit(1)
# Registry-first: Module → DB (nur registrierte Rechte/Kontingente in Admin-Matrix)
if os.getenv("SKIP_DB_MIGRATE", "").strip().lower() not in ("1", "true", "yes"):
try:
from rights_registry import sync_rights_registry_to_db
counts = sync_rights_registry_to_db()
print(
f"[OK] Rights registry sync: {counts['capabilities']} capabilities, "
f"{counts['features']} features"
)
except Exception as e:
print(f"[FAIL] Rights registry sync: {e}")
sys.exit(1)
from club_features import club_feature_enforcement_enabled
_cfe = os.getenv("CLUB_FEATURE_ENFORCE", "0")
print(
f"[OK] CLUB_FEATURE_ENFORCE raw={_cfe!r} "
f"active={club_feature_enforcement_enabled()}"
)
from routers.auth import limiter as auth_rate_limiter from routers.auth import limiter as auth_rate_limiter
# OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1 # OpenAPI: in Produktion standardmäßig aus (Schema nicht öffentlich). Notfall: PUBLIC_OPENAPI=1
@ -87,6 +109,34 @@ app.add_middleware(
) )
@app.middleware("http")
async def account_onboarding_api_gate(request: Request, call_next):
"""
Phase A: Domänen-APIs für unverified / verified_pending_club sperren.
Siehe account_onboarding_gate.py und MEMBERSHIP_RBAC_DECISIONS_2026-06.md §1.1
"""
from account_onboarding_gate import evaluate_request_gate
token = request.headers.get("x-auth-token") or request.headers.get("X-Auth-Token")
allowed, reason, _state = evaluate_request_gate(
token,
request.url.path,
request.method,
)
if not allowed:
return JSONResponse(
status_code=403,
content={
"detail": (
"Zugriff erst nach E-Mail-Bestätigung und Vereinsmitgliedschaft möglich. "
"Du kannst einen Beitrittsantrag stellen oder dein Konto in den Einstellungen verwalten."
),
"reason": reason,
},
)
return await call_next(request)
@app.middleware("http") @app.middleware("http")
async def add_api_security_headers(request: Request, call_next): async def add_api_security_headers(request: Request, call_next):
"""Konsistente Basis-Header auch für rein JSON-Responses (MIME-Sniffing).""" """Konsistente Basis-Header auch für rein JSON-Responses (MIME-Sniffing)."""
@ -193,7 +243,7 @@ def read_root():
return out return out
# Register routers # Register routers
from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, admin_users, platform_media_storage, media_assets, skills, training_planning, dashboard, training_modules, training_framework_programs, catalogs, maturity_models, matrix_stack_bundle, import_wiki, import_wiki_admin, legal_documents, content_reports from routers import auth, profiles, exercises, exercise_progression_graphs, clubs, club_memberships, club_join_requests, club_creation_requests, admin_users, admin_user_content, admin_rights, me_entitlements, platform_media_storage, media_assets, skills, skill_profiles, training_planning, planning_exercise_suggest, dashboard, training_modules, training_framework_programs, catalogs, catalog_prompt_slots, maturity_models, matrix_stack_bundle, matrix_editor, import_wiki, import_wiki_admin, legal_documents, content_reports, ai_prompts_admin, ai_skill_retrieval_admin, exercise_enrichment_admin
app.include_router(auth.router) app.include_router(auth.router)
app.include_router(profiles.router) app.include_router(profiles.router)
@ -202,23 +252,34 @@ app.include_router(exercise_progression_graphs.router)
app.include_router(clubs.router) app.include_router(clubs.router)
app.include_router(club_memberships.router) app.include_router(club_memberships.router)
app.include_router(club_join_requests.router) app.include_router(club_join_requests.router)
app.include_router(club_creation_requests.router)
app.include_router(admin_users.router) app.include_router(admin_users.router)
app.include_router(admin_user_content.router)
app.include_router(admin_rights.router)
app.include_router(me_entitlements.router)
app.include_router(platform_media_storage.router) app.include_router(platform_media_storage.router)
app.include_router(media_assets.router) app.include_router(media_assets.router)
app.include_router(media_assets.admin_rights_router) app.include_router(media_assets.admin_rights_router)
app.include_router(media_assets.admin_legal_hold_router) app.include_router(media_assets.admin_legal_hold_router)
app.include_router(skills.router) app.include_router(skills.router)
app.include_router(skill_profiles.router)
app.include_router(training_planning.router) app.include_router(training_planning.router)
app.include_router(planning_exercise_suggest.router)
app.include_router(dashboard.router) app.include_router(dashboard.router)
app.include_router(training_modules.router) app.include_router(training_modules.router)
app.include_router(training_framework_programs.router) app.include_router(training_framework_programs.router)
app.include_router(catalogs.router) app.include_router(catalogs.router)
app.include_router(catalog_prompt_slots.router)
app.include_router(maturity_models.router) app.include_router(maturity_models.router)
app.include_router(matrix_stack_bundle.router) app.include_router(matrix_stack_bundle.router)
app.include_router(matrix_editor.router)
app.include_router(import_wiki.router) app.include_router(import_wiki.router)
app.include_router(import_wiki_admin.router) app.include_router(import_wiki_admin.router)
app.include_router(legal_documents.router) app.include_router(legal_documents.router)
app.include_router(content_reports.router) app.include_router(content_reports.router)
app.include_router(ai_prompts_admin.router)
app.include_router(ai_skill_retrieval_admin.router)
app.include_router(exercise_enrichment_admin.router)
# Lokale Übungs-Medien: standardmäßig nur über geschützten API-Pfad # Lokale Übungs-Medien: standardmäßig nur über geschützten API-Pfad
# GET /api/exercises/{id}/media/{mid}/file (?ssetoken für <img>/<video>). # GET /api/exercises/{id}/media/{mid}/file (?ssetoken für <img>/<video>).

View File

@ -0,0 +1,8 @@
-- Vorlagen: Phasen/Parallel-Streams wie im Einheiten-Editor (planLoc-Abbild)
ALTER TABLE training_plan_template_sections
ADD COLUMN IF NOT EXISTS phase_kind VARCHAR(20) NOT NULL DEFAULT 'whole_group',
ADD COLUMN IF NOT EXISTS phase_order_index INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS parallel_stream_order_index INT NULL;
COMMENT ON COLUMN training_plan_template_sections.parallel_stream_order_index IS
'NULL = Ganzgruppen-Abschnitt; 0..n = Stream innerhalb paralleler Phase';

View File

@ -0,0 +1,11 @@
-- Migration 065: Wiki-spezifische Felder fuer Fähigkeiten (KarateRelevanz, RelevanzLevel)
-- SMW karatetrainer.net; Import mappt in strukturierte Spalten statt nur Freitext in description
ALTER TABLE skills
ADD COLUMN IF NOT EXISTS karate_relevance TEXT;
ALTER TABLE skills
ADD COLUMN IF NOT EXISTS relevance_level SMALLINT CHECK (relevance_level IS NULL OR relevance_level BETWEEN 1 AND 3);
COMMENT ON COLUMN skills.karate_relevance IS 'Wiki Karate-Relevanz (Plaintext aus SMW Property KarateRelevanz)';
COMMENT ON COLUMN skills.relevance_level IS 'Wiki-RelevanzLevel 13 (Semantic MediaWiki)';

View File

@ -0,0 +1,36 @@
-- Geplante Gesamt- und Abschnittsdauer; Rahmenprogramm: Fokus/Stil als M:N (wie Trainingsarten/Zielgruppen)
ALTER TABLE training_units
ADD COLUMN IF NOT EXISTS planned_duration_min INT;
ALTER TABLE training_unit_sections
ADD COLUMN IF NOT EXISTS planned_duration_min INT;
ALTER TABLE training_plan_template_sections
ADD COLUMN IF NOT EXISTS planned_duration_min INT;
CREATE TABLE IF NOT EXISTS training_framework_program_focus_areas (
framework_program_id INT NOT NULL REFERENCES training_framework_programs(id) ON DELETE CASCADE,
focus_area_id INT NOT NULL REFERENCES focus_areas(id) ON DELETE CASCADE,
PRIMARY KEY (framework_program_id, focus_area_id)
);
CREATE INDEX IF NOT EXISTS idx_tfpfa_focus ON training_framework_program_focus_areas(focus_area_id);
CREATE TABLE IF NOT EXISTS training_framework_program_style_directions (
framework_program_id INT NOT NULL REFERENCES training_framework_programs(id) ON DELETE CASCADE,
style_direction_id INT NOT NULL REFERENCES style_directions(id) ON DELETE CASCADE,
PRIMARY KEY (framework_program_id, style_direction_id)
);
CREATE INDEX IF NOT EXISTS idx_tfpsd_style ON training_framework_program_style_directions(style_direction_id);
INSERT INTO training_framework_program_focus_areas (framework_program_id, focus_area_id)
SELECT id, focus_area_id FROM training_framework_programs
WHERE focus_area_id IS NOT NULL
ON CONFLICT DO NOTHING;
INSERT INTO training_framework_program_style_directions (framework_program_id, style_direction_id)
SELECT id, style_direction_id FROM training_framework_programs
WHERE style_direction_id IS NOT NULL
ON CONFLICT DO NOTHING;

View File

@ -0,0 +1,141 @@
-- Migration 067: Konfigurierbare KI-Prompts + Tracking-Feld fuer Uebungs-Zusammenfassung
-- Datum: 2026-05-22
-- Spec: technical/KI_FEATURES_SPEC.md, AI_PROMPT_SYSTEM_SPEC.md
-- ============================================================================
-- AI PROMPTS
-- ============================================================================
CREATE TABLE IF NOT EXISTS ai_prompts (
id SERIAL PRIMARY KEY,
slug VARCHAR(100) NOT NULL UNIQUE,
display_name VARCHAR(200) NOT NULL,
description TEXT,
template TEXT NOT NULL,
category VARCHAR(50) DEFAULT 'exercise'
CHECK (category IN ('exercise', 'training', 'matrix', 'import', 'admin')),
output_format VARCHAR(10) DEFAULT 'text'
CHECK (output_format IN ('text', 'json')),
output_schema JSONB,
is_system_default BOOLEAN DEFAULT false,
default_template TEXT,
active BOOLEAN DEFAULT true,
sort_order INT DEFAULT 0,
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ai_prompts_slug ON ai_prompts(slug);
CREATE INDEX IF NOT EXISTS idx_ai_prompts_category ON ai_prompts(category);
CREATE INDEX IF NOT EXISTS idx_ai_prompts_active ON ai_prompts(active, sort_order);
DROP TRIGGER IF EXISTS ai_prompts_update ON ai_prompts;
CREATE TRIGGER ai_prompts_update
BEFORE UPDATE ON ai_prompts
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
-- ============================================================================
-- TRACKING SUMMARY (KI)
-- ============================================================================
ALTER TABLE exercises ADD COLUMN IF NOT EXISTS summary_ai_generated BOOLEAN DEFAULT false;
COMMENT ON COLUMN exercises.summary_ai_generated IS 'TRUE wenn Kurzbeschreibung zuletzt von KI vorgeschlagen und uebernommen (UI setzt bei manueller Aenderung false)';
-- ============================================================================
-- SEED PROMPTS (idempotent)
-- ============================================================================
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, is_system_default, default_template, active, sort_order
)
SELECT
'pipeline',
'Mehrstufige Gesamtanalyse',
'Master-Schalter fuer die Pipeline-Anzeige.',
'PIPELINE_MASTER',
'admin',
'text',
false,
'PIPELINE_MASTER',
true,
-10
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'pipeline');
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, is_system_default, default_template, active, sort_order
)
SELECT
'exercise_summary',
'Uebungs-Zusammenfassung',
'Erzeugt eine kurze Kurzbeschreibung fuer Listen/Galerie.',
$s$Du bist Assistent fuer Kampfsport-Trainer.
Erstelle eine kurze Kurzbeschreibung fuer Listen und Trainingsplaene.
Anforderungen:
- Hochstens etwa 200 Zeichen (bei Bedarf gekuerzt fuer Mobile)
- Kern: Welche Trainingsqualitaeten? Wie fuehrt man die Uebung kurz aus?
- Sachlich, auf Deutsch
Uebung: {{exercise_title}}
Fokuskontext: {{exercise_focus_area}}
Ziel (Fliesstext, kann HTML sein): {{exercise_goal}}
Durchfuehrung (Fliesstext, kann HTML sein): {{exercise_execution}}
Antworte NUR mit der Kurzbeschreibung als einfachen Text (keine Markdown-Codeblocks, keine Anfuehrungszeichen um den ganzen Text).$s$,
'exercise',
'text',
true,
NULL,
true,
1
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'exercise_summary');
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, is_system_default, default_template, active, sort_order
)
SELECT
'exercise_skill_suggestions',
'Faehigkeiten-Empfehlungen',
'Schlaegt passende Skills mit Stufen/Intensitaet vor (JSON-Ausgabe-Prompt).',
$j$Du bist Assistent fuer Kampfsport-Trainer.
Ordne diese Uebung dem globalen Skill-Katalog zu.
Daten zur Uebung:
Titel: {{exercise_title}}
Fokuskontext (optional): {{exercise_focus_area}}
Ziel (gekuerzt_plain): {{exercise_goal}}
Durchfuehrung (gekuerzt_plain): {{exercise_execution}}
Verfuegbare Faehigkeiten (Auswahl NUR ueber diese IDs keine anderen IDs verwenden):
{{skills_catalog}}
Waehle hoechstens 5 passende Skills. Für jede Faehigkeit:
- skill_id: ganze Zahl aus der Liste
- required_level: eines von basis, grundlagen, aufbau, fortgeschritten, optimierung
- target_level: derselbe Wertvorrat
- intensity: eines von niedrig, mittel, hoch
- is_primary (optional): true fuer die Hauptfaehigkeit der Uebung, sondern false/weglassen
Antworte NUR mit einem JSON-Array ohne Erklaertext, keine Markdown-Fences.
Beispielformat:
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
Wenn nichts gut passt, antworte mit [].$j$,
'exercise',
'json',
true,
NULL,
true,
2
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'exercise_skill_suggestions');

View File

@ -0,0 +1,125 @@
-- Migration 068: KI Skill-Retrieval-Profile pro Fokusbereich (+ Standardprofil)
-- Purpose: Gewichtungen/Quota fuer exercise_ai Skill-Katalog (OpenRouter Kontext)
CREATE TABLE IF NOT EXISTS ai_skill_retrieval_profiles (
id SERIAL PRIMARY KEY,
focus_area_id INT REFERENCES focus_areas(id) ON DELETE CASCADE,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
name VARCHAR(200) NOT NULL,
description TEXT,
active BOOLEAN NOT NULL DEFAULT TRUE,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_ai_skill_retrieval_profile_focus_area
ON ai_skill_retrieval_profiles (focus_area_id)
WHERE focus_area_id IS NOT NULL AND active = TRUE;
CREATE UNIQUE INDEX IF NOT EXISTS ux_ai_skill_retrieval_profile_default_only
ON ai_skill_retrieval_profiles (is_default)
WHERE is_default IS TRUE AND active = TRUE;
COMMENT ON TABLE ai_skill_retrieval_profiles IS
'Gewichte/Quota fuer Skill-Katalog in exercise_ai; optional gebunden an focus_areas, genau eine is_default=TRUE';
INSERT INTO ai_skill_retrieval_profiles (focus_area_id, is_default, name, description, active, config)
VALUES (
NULL,
TRUE,
'Standard',
'Kein/Undefinierter Fokusbereich: neutrale Gewichte mit sanften Caps auf sehr breite Unterkategorien.',
TRUE,
'{
"version": 1,
"importance_multiplier": 1,
"text_overlap_bonus": 2,
"main_slug_weights": { "karate": 1, "allgemeine": 1 },
"category_slug_weights": {},
"category_max_share": {
"kondition": 0.38,
"koordination": 0.35
},
"main_min_share": {},
"description_plain_max_len": 160,
"karate_relevance_max_len": 72,
"keyword_overrides": [
{
"keywords_any": ["rollenspiel", "szenario", "deesk", "diskussion"],
"case_insensitive": true,
"patch": {
"category_slug_weights": {
"psychische_faehigkeiten": 1.65,
"soziale_faehigkeiten": 1.65,
"kognition": 1.4
},
"category_max_share": {
"kondition": 0.08,
"koordination": 0.1
}
}
},
{
"keywords_any": ["befreiung", "haltegriff", "greifer", "umklammer"],
"case_insensitive": true,
"patch": {
"category_slug_weights": {
"selbstverteidigung": 2.2,
"koordination": 0.9
},
"main_slug_weights": { "karate": 1.35 }
}
}
]
}'::jsonb
);
INSERT INTO ai_skill_retrieval_profiles (focus_area_id, is_default, name, description, active, config)
SELECT
fa.id,
FALSE,
'Gewaltschutz',
'Kaum klassische Sportfaehigkeit; Gewicht auf Deeskalation, Kognition/Soziales; SV-Schwerpunkt per Keywords verstaerken.',
TRUE,
'{
"version": 1,
"importance_multiplier": 1,
"text_overlap_bonus": 2.25,
"main_slug_weights": { "karate": 1.08, "allgemeine": 1.06 },
"category_slug_weights": {
"kognition": 1.72,
"psychische_faehigkeiten": 1.78,
"soziale_faehigkeiten": 1.78,
"selbstverteidigung": 1.82,
"kondition": 0.32,
"koordination": 0.4
},
"category_max_share": {
"kondition": 0.12,
"koordination": 0.16
},
"main_min_share": {},
"description_plain_max_len": 150,
"karate_relevance_max_len": 0,
"keyword_overrides": [
{
"keywords_any": ["befreiung", "haltegriff", "greifer"],
"case_insensitive": true,
"patch": {
"category_slug_weights": {
"selbstverteidigung": 3.25,
"koordination": 1.08
},
"main_slug_weights": { "karate": 1.5 }
}
}
]
}'::jsonb
FROM focus_areas fa
WHERE fa.name = 'Gewaltschutz'
AND (fa.status IS NULL OR fa.status = 'active')
AND NOT EXISTS (
SELECT 1 FROM ai_skill_retrieval_profiles p
WHERE p.focus_area_id = fa.id AND p.active = TRUE
)
LIMIT 1;

View File

@ -0,0 +1,10 @@
-- Migration 069: ai_prompts default_template fuer Ruecksetzen & Transparenz
-- Setzt fuer bestehende System-Prompt-Zeilen default_template aus dem aktuellen template,
-- sofern noch kein Referenzinhalt gespeichert war (Migration 067 hatte NULL fuer exercise_*).
UPDATE ai_prompts
SET default_template = template,
updated_at = NOW()
WHERE default_template IS NULL
AND template IS NOT NULL
AND LENGTH(TRIM(template)) > 0;

View File

@ -0,0 +1,7 @@
-- Migration 070: optionales OpenRouter-Modell pro Prompt-Zeile
-- Leer/NULL → Umgebungsvariable OPENROUTER_MODEL (wie bisher).
ALTER TABLE ai_prompts ADD COLUMN IF NOT EXISTS openrouter_model VARCHAR(200);
COMMENT ON COLUMN ai_prompts.openrouter_model IS
'Optional: OpenRouter model id (z.B. anthropic/claude-3.5-haiku); NULL = OPENROUTER_MODEL aus Env';

View File

@ -0,0 +1,59 @@
-- Migration 071: KI-Prompt fuer Anleitungs-Ueberarbeitung (Ziel, Durchfuehrung, Vorbereitung, Trainer-Hinweise)
-- JSON-Ausgabe; praezise HTML-Fragmente fuer RichTextEditor.
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'exercise_instruction_rewrite',
'Anleitung ueberarbeiten',
'Ueberarbeitet Ziel, Durchfuehrung, Vorbereitung und Trainer-Hinweise — praezise, strukturiert, ohne Aufblaehen.',
$t$Du bist Assistent fuer Kampfsport-Trainer.
Ueberarbeite die Anleitung dieser Uebung: verbessere Formulierung, ergaenze fehlende Kernpunkte, kuerze ueberfluessige Passagen.
Wichtig: Texte sollen praezise und nachvollziehbar bleiben keine Fuellsaetze, keine Wiederholungen, kein Marketing.
Stil:
- Deutsch, sachlich, direkt an Trainer gerichtet (Durchfuehrung: Imperativ oder klare Schritte)
- Ziel: 13 kurze Absaetze (Kern des Trainingsziels)
- Durchfuehrung: klare Schritte (nummerierte Liste oder kurze Absaetze)
- Vorbereitung/Aufbau: nur wenn noetig (Raum, Material, Aufbau) sonst leerer String
- Trainer-Hinweise: Sicherheit, typische Fehler, Coaching-Tipps knapp, Stichpunkte oder kurze Absaetze
Format (HTML fuer Rich-Text-Editor):
- Erlaubt: <p>, <ul>, <ol>, <li>, <strong>, <em>, <br>
- Keine Ueberschriften (h1h6), keine Tabellen, kein Markdown, keine Code-Fences
- Medienverweise {{exerciseMedia:ID}} aus den Eingabetexten UNVERAENDERT an passender Stelle uebernehmen
Eingabe:
Titel: {{exercise_title}}
Fokuskontext: {{exercise_focus_area}}
Ziel (Plaintext, Ausgang): {{exercise_goal}}
Durchfuehrung (Plaintext, Ausgang): {{exercise_execution}}
Vorbereitung/Aufbau (Plaintext, Ausgang): {{exercise_preparation}}
Trainer-Hinweise (Plaintext, Ausgang): {{exercise_trainer_notes}}
Antworte NUR mit einem JSON-Objekt (kein Text davor/danach):
{
"goal": "<p>…</p>",
"execution": "<ol><li>…</li></ol>",
"preparation": "<p>…</p> oder \"\"",
"trainer_notes": "<ul><li>…</li></ul> oder \"\""
}
Leere Felder als leerer String "" wenn nichts Sinnvolles ergibt.$t$,
'exercise',
'json',
'{"type":"object","required":["goal","execution","preparation","trainer_notes"],"properties":{"goal":{"type":"string"},"execution":{"type":"string"},"preparation":{"type":"string"},"trainer_notes":{"type":"string"}}}'::jsonb,
true,
NULL,
true,
3
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'exercise_instruction_rewrite');
-- Referenztext fuer Admin-Ruecksetzen (wie 069)
UPDATE ai_prompts
SET default_template = template
WHERE slug = 'exercise_instruction_rewrite'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,54 @@
-- Migration 072: KI-Prompt Planungs-Übungssuche — LLM-Rerank (Phase 2)
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §14
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_exercise_search_rank',
'Planungs-Übungssuche Rerank',
'Ordnet Kandidaten für die Trainingsplanung nach Intent und Kontext; nur IDs aus candidates_json.',
$t$Du bist Assistent für Kampfsport-Trainer bei der Trainingsplanung.
Ordne die vorgegebenen Übungs-Kandidaten nach Eignung für die aktuelle Planungssituation.
Regeln:
- Verwende NUR exercise_id-Werte aus candidates_json (keine erfundenen IDs).
- Berücksichtige search_query, intent, planning_context_json und target_profile_json.
- Bewerte anhand von Titel, summary, goal und skills jedes Kandidaten.
- Gib maximal {{result_limit}} IDs in sinnvoller Reihenfolge zurück (beste zuerst).
- Kurze Begründung pro Top-Treffer auf Deutsch (1 Satz, sachlich).
Intent-Hinweise:
- suggest_next / progression_next: logische Fortsetzung, Progression, passende Skills
- deepen_exercise: Vertiefung zum Anker, ähnlicher Fokus
- continue_plan_goal: schließt an bisherigen Plan und Skill-Lücken an
- free_search: Freitext-Relevanz
Kontext:
Intent: {{intent}}
Suchanfrage: {{search_query}}
Planung: {{planning_context_json}}
Zielprofil: {{target_profile_json}}
Kandidaten (JSON):
{{candidates_json}}
Antworte NUR mit JSON (kein Text davor/danach):
{
"ranked_ids": [123, 456],
"reasons": { "123": "", "456": "" }
}$t$,
'training',
'json',
'{"type":"object","required":["ranked_ids"],"properties":{"ranked_ids":{"type":"array","items":{"type":"integer"}},"reasons":{"type":"object"}}}'::jsonb,
true,
NULL,
true,
10
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_search_rank');
UPDATE ai_prompts
SET default_template = template
WHERE slug = 'planning_exercise_search_rank'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,74 @@
-- Migration 073: KI-Prompt Planungs-Übungssuche — Intent/Query-Overlay (P1)
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §16
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_exercise_search_intent',
'Planungs-Übungssuche Intent',
'Strukturiert Freitext-Anfrage in Intent, Szenario und Katalog-Hints für Erwartungsprofil-Overlay.',
$t$Du bist Assistent für Kampfsport-Trainer in der Trainingsplanung.
Analysiere die Suchanfrage im Kontext der Einheit und des bisherigen Plans.
Ziel: JSON für ein Erwartungsprofil-Overlay (Fähigkeiten, Fokus, Stil ) NICHT Übungs-IDs erfinden.
Szenario-Klassen (scenario):
- preset_next: nur nächste Übung ohne Zusatz selten bei Freitext
- progression: Progressionsgraph / Pfad / Folgeübung im Graph
- deepen: Vertiefung zur Anker-Übung
- continue_plan: baut auf bisherigem Plan der Einheit auf
- additive_constraint: Plan beibehalten UND zusätzliche Anforderung (z. B. außerdem Schnellkraft)
- free_search: offene Stichwortsuche / neues Thema
Intent (intent): suggest_next | progression_next | deepen_exercise | continue_plan_goal | free_search
emphasis:
- additive: Zusatz zur bestehenden Planung (Default bei zusätzlich/auch/dazu)
- replace: Suchanfrage soll Schwerpunkt eher ersetzen
- neutral: nur leichte Gewichtung
Nutze skill_hints/focus_hints etc. mit Namen aus den Katalog-JSONs (beste Übereinstimmung).
Bei requires_partner: true/false/null wenn Partnerbezug erkennbar.
Eingabe:
Suchanfrage: {{search_query}}
Heuristik-Intent: {{heuristic_intent}}
Szenario-Hinweis (Server): {{scenario_hint}}
Planungskontext: {{planning_context_json}}
Basis-Zielprofil (deterministisch): {{target_profile_json}}
Kataloge (Auszug nur diese Namen/IDs verwenden):
Skills: {{skills_catalog_json}}
Fokus: {{focus_areas_catalog_json}}
Trainingsstil: {{training_types_catalog_json}}
Stilrichtung: {{style_directions_catalog_json}}
Zielgruppe: {{target_groups_catalog_json}}
Antworte NUR mit JSON:
{
"intent": "continue_plan_goal",
"scenario": "additive_constraint",
"skill_hints": [{"name": "Schnellkraft", "weight": 1.0}],
"focus_hints": [],
"style_hints": [],
"training_type_hints": [],
"target_group_hints": [],
"requires_partner": null,
"emphasis": "additive",
"rationale": "Kurz auf Deutsch, 1 Satz"
}$t$,
'training',
'json',
'{"type":"object","required":["intent","scenario"],"properties":{"intent":{"type":"string"},"scenario":{"type":"string"},"skill_hints":{"type":"array"},"emphasis":{"type":"string"},"rationale":{"type":"string"}}}'::jsonb,
true,
NULL,
true,
11
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_search_intent');
UPDATE ai_prompts
SET default_template = template
WHERE slug = 'planning_exercise_search_intent'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,70 @@
-- Migration 074: KI-Prompt Planungs-Übungssuche — Erwartungsprofil aus Planungskontext (Preset)
-- Spec: .claude/docs/working/PLANNING_EXERCISE_SUGGEST_CONTEXT.md §16
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_exercise_expectation_profile',
'Planungs-Übungssuche Erwartungsprofil',
'Leitet aus Einheit, Abschnitt, Anker und bisherigem Plan ein Erwartungsprofil für die nächste Übung ab (ohne Freitext-Anfrage).',
$t$Du bist Assistent für Kampfsport-Trainer in der Trainingsplanung.
Der Trainer wählt nächste Übung aus Kontext es gibt KEINE zusätzliche Freitext-Suchanfrage.
Deine Aufgabe: Aus dem Planungskontext und dem deterministischen Basis-Zielprofil ein präzises Erwartungsprofil ableiten:
- Was soll die nächste Übung fachlich leisten (Fortsetzen, Vertiefen, Lücke schließen, Abwechslung)?
- Welche Fähigkeiten, Fokus-Bereiche, Trainingsstile passen dazu?
- Berücksichtige: Rahmen/Einheit, Abschnittsziel (guidance_notes), letzte Übung im Abschnitt, Anker-Übung, Skill-Profile Einheit vs. Abschnitt, Skill-Lücken im Basisprofil.
Intent (intent): meist suggest_next oder continue_plan_goal; progression_next nur wenn Progressionsgraph/Anker klar nahelegt; deepen_exercise nur bei klarer Vertiefungslage.
continuation (optional, Kurzlabel):
- build_on_section: nahtlos an Abschnitt/letzte Übung anknüpfen
- close_skill_gap: fehlende Fähigkeiten aus Plan/Rahmen nachziehen
- deepen_anchor: Anker-Übung vertiefen
- variety: bewusst variieren nach bisherigem Block
- balance_load: Belastung ausgleichen / Tempo wechseln
Nutze skill_hints/focus_hints etc. mit Namen aus den Katalog-JSONs (beste Übereinstimmung).
emphasis: fast immer additive (baut auf Basisprofil auf), nur replace wenn Kontext eindeutig neuen Schwerpunkt verlangt.
Eingabe:
Heuristik-Intent: {{heuristic_intent}}
Planungskontext: {{planning_context_json}}
Basis-Zielprofil (deterministisch): {{target_profile_json}}
Kataloge (Auszug nur diese Namen/IDs verwenden):
Skills: {{skills_catalog_json}}
Fokus: {{focus_areas_catalog_json}}
Trainingsstil: {{training_types_catalog_json}}
Stilrichtung: {{style_directions_catalog_json}}
Zielgruppe: {{target_groups_catalog_json}}
Antworte NUR mit JSON:
{
"intent": "suggest_next",
"scenario": "preset_next",
"continuation": "build_on_section",
"skill_hints": [{"name": "Kime", "weight": 0.9}],
"focus_hints": [],
"style_hints": [],
"training_type_hints": [],
"target_group_hints": [],
"requires_partner": null,
"emphasis": "additive",
"rationale": "Kurz auf Deutsch, 12 Sätze: warum diese nächste Übung sinnvoll ist"
}$t$,
'training',
'json',
'{"type":"object","required":["intent","scenario","rationale"],"properties":{"intent":{"type":"string"},"scenario":{"type":"string"},"continuation":{"type":"string"},"skill_hints":{"type":"array"},"emphasis":{"type":"string"},"rationale":{"type":"string"}}}'::jsonb,
true,
NULL,
true,
12
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_expectation_profile');
UPDATE ai_prompts
SET default_template = template
WHERE slug = 'planning_exercise_expectation_profile'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,89 @@
-- Migration 075: Planungs-KI Phase E — Semantik-Enrichment + Pfad-QA Prompts
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_exercise_query_semantics',
'Planungs-Übungssuche Semantik',
'Erweitert deterministisches Semantic Brief um must/exclude phrases und Entwicklungsbogen.',
$t$Du bist Assistent für Kampfsport-Trainer bei der semantischen Analyse von Planungs-Anfragen.
Ziel: JSON für ein Semantic Brief präzise Kernbegriffe, Ausschlüsse, Entwicklungsbogen.
Nutze das bestehende Brief als Basis; ergänze/verfeinere, ersetze aber keine eindeutige Technik-Identität.
Anfrage: {{search_query}}
Bestehendes Brief (deterministisch): {{semantic_brief_json}}
Regeln:
- must_phrases: konkrete Technik-/Themen-Phrasen aus der Anfrage (z. B. "mae geri", nicht nur "geri")
- exclude_phrases: konkurrierende Techniken/Themen, die NICHT gemeint sind
- development_arc: geordnete Phasen aus: einstieg, grundlage, vertiefung, anwendung, perfektion
- semantic_strength: 0.01.0 (höher bei spezifischer Technik/Thema)
- primary_topic: Hauptthema in wenigen Worten
- topic_type: technique | focus | method | skill | general
Antworte NUR mit JSON:
{
"primary_topic": "Mae Geri",
"topic_type": "technique",
"must_phrases": ["mae geri"],
"exclude_phrases": ["mawashi geri", "sakuto geri"],
"development_arc": ["einstieg", "grundlage", "vertiefung", "perfektion"],
"semantic_strength": 0.9,
"rationale": "Kurz auf Deutsch"
}$t$,
'training',
'json',
'{"type":"object","properties":{"must_phrases":{"type":"array"},"exclude_phrases":{"type":"array"},"development_arc":{"type":"array"},"semantic_strength":{"type":"number"}}}'::jsonb,
true,
NULL,
true,
12
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_query_semantics');
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_exercise_path_qa',
'Planungs-Pfad QA',
'Semantische Qualitätsprüfung eines vorgeschlagenen Übungspfads inkl. Lücken und Brücken.',
$t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte?
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"issues": [""],
"sequence_notes": [""],
"recommendations": [""]
}$t$,
'training',
'json',
'{"type":"object","required":["overall_ok"],"properties":{"overall_ok":{"type":"boolean"},"quality_score":{"type":"number"},"issues":{"type":"array"},"sequence_notes":{"type":"array"},"recommendations":{"type":"array"}}}'::jsonb,
true,
NULL,
true,
13
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_exercise_path_qa');
UPDATE ai_prompts SET default_template = template
WHERE slug IN ('planning_exercise_query_semantics', 'planning_exercise_path_qa')
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,60 @@
-- Migration 076: Planungs-Pfad-QA — Neuordnung + KI-Lückenfüller (Phase E2)
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
Nur Indizes aus dem steps_json verwenden Länge muss exakt der Schrittzahl entsprechen.
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""]
}$t$,
default_template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
Nur Indizes aus dem steps_json verwenden Länge muss exakt der Schrittzahl entsprechen.
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""]
}$t$
WHERE slug = 'planning_exercise_path_qa';

View File

@ -0,0 +1,85 @@
-- Migration 077: Planungs-Pfad-QA — strukturierte Neuanlage-Vorschläge (Phase E3)
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte (Kraft, Geschwindigkeit, Anwendung, Perfektion)?
6. Gibt es Schritte ohne Bezug zum Hauptthema (z. B. reine Kraftübungen bei einer Technik)?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
Nur Indizes aus dem steps_json verwenden Länge muss exakt der Schrittzahl entsprechen.
Wenn wichtige Zwischenschritte fehlen oder Schritte themenfremd sind: suggested_new_exercises mit konkreten Übungs-Ideen (Titel + Kurzskizze), jeweils mit insert_after_step_index (0-basiert: nach welchem Schritt einfügen).
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""],
"suggested_new_exercises": [
{
"title_hint": "Mae Geri Kraftentwicklung am Sandsack",
"sketch": "Gezielte Kraft- und Schnelligkeitsentwicklung für Mae Geri …",
"phase": "vertiefung",
"insert_after_step_index": 2,
"rationale": "Schließt Lücke zwischen Grundlagen und Gleichgewichtstritt"
}
]
}$t$,
default_template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte (Kraft, Geschwindigkeit, Anwendung, Perfektion)?
6. Gibt es Schritte ohne Bezug zum Hauptthema (z. B. reine Kraftübungen bei einer Technik)?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
Nur Indizes aus dem steps_json verwenden Länge muss exakt der Schrittzahl entsprechen.
Wenn wichtige Zwischenschritte fehlen oder Schritte themenfremd sind: suggested_new_exercises mit konkreten Übungs-Ideen (Titel + Kurzskizze), jeweils mit insert_after_step_index (0-basiert: nach welchem Schritt einfügen).
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""],
"suggested_new_exercises": [
{
"title_hint": "Mae Geri Kraftentwicklung am Sandsack",
"sketch": "Gezielte Kraft- und Schnelligkeitsentwicklung für Mae Geri …",
"phase": "vertiefung",
"insert_after_step_index": 2,
"rationale": "Schließt Lücke zwischen Grundlagen und Gleichgewichtstritt"
}
]
}$t$,
output_schema = '{"type":"object","required":["overall_ok"],"properties":{"overall_ok":{"type":"boolean"},"quality_score":{"type":"number"},"issues":{"type":"array"},"sequence_notes":{"type":"array"},"recommendations":{"type":"array"},"ordered_step_indices":{"type":"array"},"suggested_new_exercises":{"type":"array"}}}'::jsonb
WHERE slug = 'planning_exercise_path_qa';

View File

@ -0,0 +1,74 @@
-- Migration 078: Planungs-KI Phase F — Progressions-Roadmap Prompts (Zielanalyse + Roadmap)
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_progression_goal_analysis',
'Progressions-Roadmap Zielanalyse',
'Phase A: Ist-/Soll-Zustand und Erfolgskriterien für einen Progressionsgraphen (ohne Gruppenkontext).',
$t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Wichtig: Keine Gruppenanalyse nur didaktischer Pfad für die Technik/das Thema.
Antworte NUR mit JSON:
{
"primary_topic": "Mae Geri",
"start_assumption": "Welche Voraussetzungen werden für den Einstieg angenommen",
"target_state": "Konkreter Zielzustand der Progression",
"success_criteria": ["messbare Kriterien"],
"constraints": { "partner_required": false }
}$t$,
'training',
'json',
'{"type":"object","properties":{"primary_topic":{"type":"string"},"target_state":{"type":"string"},"success_criteria":{"type":"array"}}}'::jsonb,
true,
NULL,
true,
14
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_goal_analysis');
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_progression_roadmap',
'Progressions-Roadmap Major Steps',
'Phase B: 812 micro_objectives, Konsolidierung auf N major_steps.',
$t$Du bist Assistent für Kampfsport-Trainer und erstellst eine didaktische Roadmap für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Semantic Brief: {{semantic_brief_json}}
Anzahl Major Steps (N): {{max_steps}}
Erzeuge zuerst 812 micro_objectives (phase, title, weight, depends_on), dann konsolidiere auf genau N major_steps.
Phasen: einstieg, grundlage, vertiefung, anwendung, perfektion in sinnvoller Reihenfolge (Grundlagen vor Perfektion).
Antworte NUR mit JSON:
{
"micro_objectives": [
{ "id": "m1", "phase": "grundlage", "title": "", "weight": 0.9, "depends_on": [] }
],
"major_steps": [
{ "index": 0, "phase": "grundlage", "learning_goal": "", "consolidates": ["m1","m2"], "rationale": "" }
],
"consolidation_notes": [""]
}$t$,
'training',
'json',
'{"type":"object","properties":{"micro_objectives":{"type":"array"},"major_steps":{"type":"array"},"consolidation_notes":{"type":"array"}}}'::jsonb,
true,
NULL,
true,
15
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_roadmap');
UPDATE ai_prompts SET default_template = template
WHERE slug IN ('planning_progression_goal_analysis', 'planning_progression_roadmap')
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,286 @@
-- Migration 078: Vereins-Feature-Registry (Mitai-v9c-Pattern) + club_plans/subscriptions
-- Spez: .claude/docs/technical/CLUB_MEMBERSHIP_AND_FEATURES.v1.md (M1)
-- Legacy 001 (SERIAL features, profile tier_limits) wird archiviert, nicht gelöscht.
-- ── 1. Legacy-Tabellen archivieren (nur alte Struktur) ─────────────────────
DO $migration$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'features'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'name'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'limit_type'
) THEN
-- Nach abgebrochenem Erstversuch kann features_legacy_001 schon existieren
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'features_legacy_001'
) THEN
DROP TABLE features;
ELSE
ALTER TABLE features RENAME TO features_legacy_001;
END IF;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'tier_limits'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tier_limits' AND column_name = 'tier'
) THEN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'tier_limits_legacy_001'
) THEN
DROP TABLE tier_limits;
ELSE
ALTER TABLE tier_limits RENAME TO tier_limits_legacy_001;
END IF;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'user_feature_usage'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'user_feature_usage' AND column_name = 'profile_id'
) THEN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'user_feature_usage_legacy_001'
) THEN
DROP TABLE user_feature_usage;
ELSE
ALTER TABLE user_feature_usage RENAME TO user_feature_usage_legacy_001;
END IF;
END IF;
END
$migration$;
-- ── 2. Feature-Registry (TEXT-PK, app=shinkan) ────────────────────────────
CREATE TABLE IF NOT EXISTS features (
id TEXT PRIMARY KEY,
app TEXT NOT NULL DEFAULT 'shinkan',
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL DEFAULT 'content',
limit_type TEXT NOT NULL DEFAULT 'count'
CHECK (limit_type IN ('count', 'boolean')),
reset_period TEXT NOT NULL DEFAULT 'never'
CHECK (reset_period IN ('never', 'daily', 'monthly')),
default_limit INTEGER,
enforcement_subject TEXT NOT NULL DEFAULT 'club'
CHECK (enforcement_subject IN ('club', 'profile', 'portal')),
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_features_app ON features(app) WHERE active = true;
-- ── 3. Vereins-Produkte ─────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS club_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
price_monthly_cents INTEGER,
price_yearly_cents INTEGER,
stripe_price_id_monthly TEXT,
stripe_price_id_yearly TEXT,
active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS club_plan_limits (
id SERIAL PRIMARY KEY,
plan_id TEXT NOT NULL REFERENCES club_plans(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER,
UNIQUE (plan_id, feature_id)
);
CREATE INDEX IF NOT EXISTS idx_club_plan_limits_plan ON club_plan_limits(plan_id);
CREATE TABLE IF NOT EXISTS club_subscriptions (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT NOT NULL REFERENCES club_plans(id),
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'trial', 'past_due', 'cancelled')),
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ends_at TIMESTAMPTZ,
trial_ends_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (club_id)
);
CREATE INDEX IF NOT EXISTS idx_club_subscriptions_plan ON club_subscriptions(plan_id);
CREATE TABLE IF NOT EXISTS club_feature_overrides (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER NOT NULL,
reason TEXT,
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (club_id, feature_id)
);
CREATE TABLE IF NOT EXISTS club_access_grants (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
plan_id TEXT REFERENCES club_plans(id) ON DELETE SET NULL,
feature_id TEXT REFERENCES features(id) ON DELETE SET NULL,
grant_limit INTEGER,
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
reason TEXT,
created_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_club_access_grants_club ON club_access_grants(club_id);
CREATE INDEX IF NOT EXISTS idx_club_access_grants_window ON club_access_grants(club_id, starts_at, ends_at);
CREATE TABLE IF NOT EXISTS club_feature_usage (
id SERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
usage_count INTEGER NOT NULL DEFAULT 0,
reset_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (club_id, feature_id)
);
CREATE INDEX IF NOT EXISTS idx_club_feature_usage_club ON club_feature_usage(club_id);
CREATE TABLE IF NOT EXISTS club_feature_usage_events (
id BIGSERIAL PRIMARY KEY,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
action TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_club_feature_usage_events_club
ON club_feature_usage_events(club_id, created_at DESC);
-- ── 4. Seed: Features ─────────────────────────────────────────────────────
INSERT INTO features (id, app, name, description, category, limit_type, reset_period, default_limit, enforcement_subject)
VALUES
('exercises', 'shinkan', 'Übungen', 'Anzahl Übungen im Verein (Bestand)', 'content', 'count', 'never', 100, 'club'),
('exercise_media', 'shinkan', 'Medien-Uploads', 'Medien-Uploads pro Monat', 'content', 'count', 'monthly', 20, 'club'),
('training_units', 'shinkan', 'Trainingseinheiten', 'Trainingseinheiten pro Monat', 'planning', 'count', 'monthly', 40, 'club'),
('training_programs', 'shinkan', 'Trainingsprogramme', 'Module und Rahmenprogramme (Bestand)', 'planning', 'count', 'never', 5, 'club'),
('training_groups', 'shinkan', 'Trainingsgruppen', 'Anzahl Trainingsgruppen', 'org', 'count', 'never', 10, 'club'),
('active_members', 'shinkan', 'Aktive Mitglieder', 'Anzahl aktiver Vereinsmitglieder', 'org', 'count', 'never', 25, 'club'),
('ai_calls', 'shinkan', 'KI-Aufrufe', 'KI-Aufrufe pro Monat (Suggest, Regenerate, Planung)', 'ai', 'count', 'monthly', 0, 'club'),
('ai_pipeline', 'shinkan', 'KI-Pipeline', 'Erweiterte KI-Batch-Pipelines', 'ai', 'boolean', 'never', 0, 'club'),
('wiki_import', 'shinkan', 'Wiki-Import', 'MediaWiki-Import (Plattform)', 'integration', 'boolean', 'never', 0, 'portal'),
('data_export', 'shinkan', 'Daten-Export', 'Export-Funktionen', 'integration', 'boolean', 'never', 0, 'club')
ON CONFLICT (id) DO NOTHING;
-- ── 5. Seed: Pläne ──────────────────────────────────────────────────────────
INSERT INTO club_plans (id, name, description, sort_order, active)
VALUES
('free', 'Free', 'Einstieg für Vereine', 0, true),
('verein_starter', 'Verein Starter', 'Erweiterte Kontingente', 10, true),
('verein_pro', 'Verein Pro', 'Hohe Limits und KI-Kontingent', 20, true),
('pilot', 'Pilot', 'Pilotverein mit großzügigen Limits', 5, true)
ON CONFLICT (id) DO NOTHING;
-- Plan-Limits: free
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
SELECT 'free', f.id,
CASE f.id
WHEN 'exercises' THEN 100
WHEN 'exercise_media' THEN 20
WHEN 'training_units' THEN 40
WHEN 'training_programs' THEN 5
WHEN 'training_groups' THEN 10
WHEN 'active_members' THEN 25
WHEN 'ai_calls' THEN 0
WHEN 'ai_pipeline' THEN 0
WHEN 'wiki_import' THEN 0
WHEN 'data_export' THEN 0
END
FROM features f
WHERE f.app = 'shinkan'
ON CONFLICT (plan_id, feature_id) DO NOTHING;
-- Plan-Limits: verein_starter
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
SELECT 'verein_starter', f.id,
CASE f.id
WHEN 'exercises' THEN 500
WHEN 'exercise_media' THEN 80
WHEN 'training_units' THEN 200
WHEN 'training_programs' THEN 30
WHEN 'training_groups' THEN 30
WHEN 'active_members' THEN 80
WHEN 'ai_calls' THEN 30
WHEN 'ai_pipeline' THEN 0
WHEN 'wiki_import' THEN 0
WHEN 'data_export' THEN 1
END
FROM features f
WHERE f.app = 'shinkan'
ON CONFLICT (plan_id, feature_id) DO NOTHING;
-- Plan-Limits: verein_pro (NULL = unbegrenzt wo sinnvoll)
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
SELECT 'verein_pro', f.id,
CASE f.id
WHEN 'exercises' THEN NULL
WHEN 'exercise_media' THEN 300
WHEN 'training_units' THEN NULL
WHEN 'training_programs' THEN NULL
WHEN 'training_groups' THEN NULL
WHEN 'active_members' THEN NULL
WHEN 'ai_calls' THEN 200
WHEN 'ai_pipeline' THEN 1
WHEN 'wiki_import' THEN 0
WHEN 'data_export' THEN 1
END
FROM features f
WHERE f.app = 'shinkan'
ON CONFLICT (plan_id, feature_id) DO NOTHING;
-- Plan-Limits: pilot
INSERT INTO club_plan_limits (plan_id, feature_id, limit_value)
SELECT 'pilot', f.id,
CASE f.id
WHEN 'exercises' THEN NULL
WHEN 'exercise_media' THEN NULL
WHEN 'training_units' THEN NULL
WHEN 'training_programs' THEN NULL
WHEN 'training_groups' THEN NULL
WHEN 'active_members' THEN NULL
WHEN 'ai_calls' THEN 100
WHEN 'ai_pipeline' THEN 1
WHEN 'wiki_import' THEN 0
WHEN 'data_export' THEN 1
END
FROM features f
WHERE f.app = 'shinkan'
ON CONFLICT (plan_id, feature_id) DO NOTHING;
-- ── 6. Backfill: bestehende Vereine → Plan free ───────────────────────────
INSERT INTO club_subscriptions (club_id, plan_id, status)
SELECT c.id, 'free', 'active'
FROM clubs c
WHERE NOT EXISTS (
SELECT 1 FROM club_subscriptions cs WHERE cs.club_id = c.id
);

View File

@ -0,0 +1,43 @@
-- Migration 079: Planungs-KI Phase F — Stufenspezifikation (Prompt in ai_prompts, nicht im Code)
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_progression_stage_spec',
'Progressions-Roadmap Stufenspezifikation',
'Phase C: Belastungsprofil, Übungstyp und Erfolgskriterien je Major Step.',
$t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Major Steps: {{major_steps_json}}
Für jeden Major Step: messbares Lernziel, load_profile (z. B. koordination, präzision, kraft), exercise_type (kihon_einzel, partner_drill, kombination, kraft_auxiliary), success_criteria, anti_patterns (z. B. reine Kraft ohne Technikbezug).
Antworte NUR mit JSON:
{
"stage_specs": [
{
"major_step_index": 0,
"learning_goal": "",
"load_profile": ["koordination", "gleichgewicht"],
"exercise_type": "kihon_einzel",
"success_criteria": [""],
"anti_patterns": [""]
}
]
}$t$,
'training',
'json',
'{"type":"object","properties":{"stage_specs":{"type":"array"}}}'::jsonb,
true,
NULL,
true,
16
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_stage_spec');
UPDATE ai_prompts SET default_template = template
WHERE slug = 'planning_progression_stage_spec'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,225 @@
-- Migration 079: Capability-Registry + Rollen-Grants (M3 / CAPABILITY_CATALOG.v1.md C1)
-- Account-Gates und Enforcement in Python (account_lifecycle.py, capabilities.py).
-- Voraussetzung: Migration 078 (features.id TEXT). Kein FK auf features — vermeidet
-- Startup-Abbruch wenn 078 noch aussteht oder features-Schema driftet (001 vs v9c).
DO $migration$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'features' AND column_name = 'limit_type'
) THEN
RAISE EXCEPTION
'Migration 079: features-Tabelle nicht v9c (limit_type fehlt). Zuerst 078_club_features_and_plans anwenden.';
END IF;
END
$migration$;
CREATE TABLE IF NOT EXISTS capabilities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
domain TEXT NOT NULL,
min_account_state TEXT NOT NULL DEFAULT 'active_member'
CHECK (min_account_state IN (
'unverified', 'verified_pending_club', 'active_member', 'platform_admin'
)),
linked_feature_id TEXT,
active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_capabilities_domain ON capabilities(domain) WHERE active = true;
CREATE TABLE IF NOT EXISTS club_role_capability_grants (
role_code TEXT NOT NULL,
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (role_code, capability_id)
);
CREATE INDEX IF NOT EXISTS idx_club_role_cap_grants_cap ON club_role_capability_grants(capability_id);
CREATE TABLE IF NOT EXISTS portal_role_capability_grants (
portal_role TEXT NOT NULL,
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
PRIMARY KEY (portal_role, capability_id)
);
-- ── Seed: Capabilities (v1 Katalog §5) ───────────────────────────────────────
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id) VALUES
('account.settings.read', 'Einstellungen lesen', 'account', 'unverified', NULL),
('account.settings.update', 'Einstellungen ändern', 'account', 'unverified', NULL),
('account.password.change', 'Passwort ändern', 'account', 'unverified', NULL),
('account.resend_verification', 'Verifizierung erneut senden', 'account', 'unverified', NULL),
('club.directory.read', 'Vereinsverzeichnis', 'club', 'verified_pending_club', NULL),
('club.join_request.create', 'Vereinsbeitritt beantragen', 'club', 'verified_pending_club', NULL),
('club.join_request.withdraw', 'Beitrittsantrag zurückziehen', 'club', 'verified_pending_club', NULL),
('club.join_request.read_own', 'Eigene Beitrittsanträge', 'club', 'verified_pending_club', NULL),
('org.club.read', 'Vereine lesen', 'org', 'active_member', NULL),
('org.club.create', 'Verein anlegen', 'org', 'platform_admin', NULL),
('org.club.update', 'Verein bearbeiten', 'org', 'active_member', NULL),
('org.club.delete', 'Verein löschen', 'org', 'platform_admin', NULL),
('org.structure.manage', 'Vereinsstruktur verwalten', 'org', 'active_member', 'training_groups'),
('org.members.read', 'Mitgliederliste', 'org', 'active_member', NULL),
('org.members.manage', 'Mitglieder verwalten', 'org', 'active_member', 'active_members'),
('org.members.directory', 'Mitglieder-Verzeichnis', 'org', 'active_member', NULL),
('org.join_request.review', 'Beitrittsanträge prüfen', 'org', 'active_member', NULL),
('org.inbox.read', 'Posteingang', 'org', 'active_member', NULL),
('exercises.read', 'Übungen lesen', 'exercises', 'active_member', NULL),
('exercises.create', 'Übung anlegen', 'exercises', 'active_member', 'exercises'),
('exercises.update', 'Übung bearbeiten', 'exercises', 'active_member', NULL),
('exercises.delete', 'Übung löschen', 'exercises', 'active_member', NULL),
('exercises.bulk_metadata', 'Übungen Stapel-Metadaten', 'exercises', 'active_member', NULL),
('exercises.ai.suggest', 'KI-Vorschlag Übung', 'exercises', 'active_member', 'ai_calls'),
('exercises.ai.regenerate', 'KI neu generieren', 'exercises', 'active_member', 'ai_calls'),
('exercises.media.read', 'Übungsmedien lesen', 'exercises', 'active_member', NULL),
('exercises.media.upload', 'Übungsmedien hochladen', 'exercises', 'active_member', 'exercise_media'),
('exercises.variants.manage', 'Übungsvarianten', 'exercises', 'active_member', NULL),
('media.library.read', 'Medienbibliothek lesen', 'media', 'active_member', NULL),
('media.library.upload', 'Medienbibliothek Upload', 'media', 'active_member', 'exercise_media'),
('media.library.update', 'Medienbibliothek bearbeiten', 'media', 'active_member', NULL),
('media.library.lifecycle', 'Medien-Lifecycle', 'media', 'active_member', NULL),
('media.rights.declare', 'Medienrechte erklären', 'media', 'active_member', NULL),
('media.admin.rights_review', 'Medienrechte Review (Plattform)', 'media', 'platform_admin', NULL),
('modules.read', 'Trainingsmodule lesen', 'modules', 'active_member', NULL),
('modules.create', 'Trainingsmodul anlegen', 'modules', 'active_member', 'training_programs'),
('modules.update', 'Trainingsmodul bearbeiten', 'modules', 'active_member', NULL),
('modules.delete', 'Trainingsmodul löschen', 'modules', 'active_member', NULL),
('framework.read', 'Rahmenprogramme lesen', 'framework', 'active_member', NULL),
('framework.create', 'Rahmenprogramm anlegen', 'framework', 'active_member', 'training_programs'),
('framework.update', 'Rahmenprogramm bearbeiten', 'framework', 'active_member', NULL),
('framework.delete', 'Rahmenprogramm löschen', 'framework', 'active_member', NULL),
('plan_templates.read', 'Planungsvorlagen lesen', 'planning', 'active_member', NULL),
('plan_templates.manage', 'Planungsvorlagen verwalten', 'planning', 'active_member', NULL),
('progression.read', 'Progressionspfade lesen', 'progression', 'active_member', NULL),
('progression.manage', 'Progressionspfade verwalten', 'progression', 'active_member', NULL),
('planning.calendar.read', 'Planungskalender lesen', 'planning', 'active_member', NULL),
('planning.units.create', 'Trainingseinheit anlegen', 'planning', 'active_member', 'training_units'),
('planning.units.update', 'Trainingseinheit bearbeiten', 'planning', 'active_member', NULL),
('planning.units.delete', 'Trainingseinheit löschen', 'planning', 'active_member', NULL),
('planning.units.run', 'Training durchführen', 'planning', 'active_member', NULL),
('planning.coach.execute', 'Coach ausführen', 'planning', 'active_member', NULL),
('planning.ai.suggest', 'Planungs-KI Suggest', 'planning', 'active_member', 'ai_calls'),
('planning.ai.progression_path', 'Planungs-KI Progressionspfad', 'planning', 'active_member', 'ai_calls'),
('skills.catalog.read', 'Fähigkeitenkatalog', 'skills', 'active_member', NULL),
('skills.discovery.read', 'Fähigkeiten-Discovery', 'skills', 'active_member', NULL),
('skill_profiles.read', 'Skill-Profile lesen', 'skills', 'active_member', NULL),
('governance.content_report.create', 'Inhalt melden', 'governance', 'active_member', NULL),
('governance.content_report.review', 'Meldungen prüfen', 'governance', 'active_member', NULL),
('platform.admin.access', 'Plattform-Admin-Bereich', 'platform', 'platform_admin', NULL),
('platform.users.manage', 'Nutzer verwalten', 'platform', 'platform_admin', NULL),
('platform.catalogs.manage', 'Kataloge verwalten', 'platform', 'platform_admin', NULL),
('platform.maturity_models.manage', 'Reifegradmodelle', 'platform', 'platform_admin', NULL),
('platform.wiki_import.execute', 'Wiki-Import', 'platform', 'platform_admin', 'wiki_import'),
('platform.ai_prompts.manage', 'KI-Prompts verwalten', 'platform', 'platform_admin', NULL),
('platform.exercise_enrichment.execute', 'Übungs-Anreicherung KI', 'platform', 'platform_admin', 'ai_calls'),
('platform.user_content.moderate', 'Nutzer-Inhalte moderieren', 'platform', 'platform_admin', NULL),
('platform.legal_documents.manage', 'Rechtstexte verwalten', 'platform', 'platform_admin', NULL),
('platform.media_storage.manage', 'Medienspeicher verwalten', 'platform', 'platform_admin', NULL),
('platform.club_creation.approve', 'Vereinsgründung freigeben', 'platform', 'platform_admin', NULL)
ON CONFLICT (id) DO NOTHING;
-- ── Vereinsrollen-Grants (§6 — nur eingeschränkte Capabilities) ─────────────
-- Konvention: keine Grant-Zeile = alle aktiven Mitglieder (min_account_state reicht).
INSERT INTO club_role_capability_grants (role_code, capability_id)
SELECT r.role_code, c.id
FROM (VALUES
('club_admin', 'org.structure.manage'),
('division_lead', 'org.structure.manage'),
('club_admin', 'org.members.manage'),
('club_admin', 'org.join_request.review'),
('club_admin', 'org.inbox.read'),
('club_admin', 'exercises.create'),
('trainer', 'exercises.create'),
('content_editor', 'exercises.create'),
('division_lead', 'exercises.create'),
('club_admin', 'exercises.update'),
('trainer', 'exercises.update'),
('content_editor', 'exercises.update'),
('division_lead', 'exercises.update'),
('club_admin', 'exercises.delete'),
('club_admin', 'exercises.bulk_metadata'),
('content_editor', 'exercises.bulk_metadata'),
('club_admin', 'exercises.ai.suggest'),
('trainer', 'exercises.ai.suggest'),
('content_editor', 'exercises.ai.suggest'),
('division_lead', 'exercises.ai.suggest'),
('club_admin', 'exercises.ai.regenerate'),
('trainer', 'exercises.ai.regenerate'),
('content_editor', 'exercises.ai.regenerate'),
('division_lead', 'exercises.ai.regenerate'),
('club_admin', 'exercises.media.upload'),
('trainer', 'exercises.media.upload'),
('content_editor', 'exercises.media.upload'),
('club_admin', 'exercises.variants.manage'),
('trainer', 'exercises.variants.manage'),
('content_editor', 'exercises.variants.manage'),
('club_admin', 'media.library.upload'),
('trainer', 'media.library.upload'),
('content_editor', 'media.library.upload'),
('club_admin', 'media.library.update'),
('trainer', 'media.library.update'),
('content_editor', 'media.library.update'),
('club_admin', 'media.library.lifecycle'),
('trainer', 'media.library.lifecycle'),
('club_admin', 'media.rights.declare'),
('trainer', 'media.rights.declare'),
('club_admin', 'modules.create'),
('trainer', 'modules.create'),
('content_editor', 'modules.create'),
('club_admin', 'modules.update'),
('trainer', 'modules.update'),
('content_editor', 'modules.update'),
('club_admin', 'modules.delete'),
('club_admin', 'framework.create'),
('trainer', 'framework.create'),
('club_admin', 'framework.update'),
('trainer', 'framework.update'),
('club_admin', 'framework.delete'),
('club_admin', 'plan_templates.manage'),
('trainer', 'plan_templates.manage'),
('club_admin', 'progression.manage'),
('trainer', 'progression.manage'),
('content_editor', 'progression.manage'),
('club_admin', 'planning.units.create'),
('trainer', 'planning.units.create'),
('division_lead', 'planning.units.create'),
('club_admin', 'planning.units.update'),
('trainer', 'planning.units.update'),
('division_lead', 'planning.units.update'),
('club_admin', 'planning.units.delete'),
('trainer', 'planning.units.delete'),
('club_admin', 'planning.units.run'),
('trainer', 'planning.units.run'),
('division_lead', 'planning.units.run'),
('club_admin', 'planning.coach.execute'),
('trainer', 'planning.coach.execute'),
('club_admin', 'planning.ai.suggest'),
('trainer', 'planning.ai.suggest'),
('division_lead', 'planning.ai.suggest'),
('club_admin', 'planning.ai.progression_path'),
('trainer', 'planning.ai.progression_path'),
('division_lead', 'planning.ai.progression_path'),
('club_admin', 'skills.discovery.read'),
('trainer', 'skills.discovery.read'),
('content_editor', 'skills.discovery.read'),
('club_admin', 'governance.content_report.review')
) AS r(role_code, cap_id)
JOIN capabilities c ON c.id = r.cap_id
ON CONFLICT DO NOTHING;
-- org.club.update: club_admin (zusätzlich zu platform_admin via Bypass)
INSERT INTO club_role_capability_grants (role_code, capability_id)
VALUES ('club_admin', 'org.club.update')
ON CONFLICT DO NOTHING;
-- ── Portal-Rollen ───────────────────────────────────────────────────────────
INSERT INTO portal_role_capability_grants (portal_role, capability_id)
SELECT 'admin', id FROM capabilities WHERE id = 'platform.admin.access'
ON CONFLICT DO NOTHING;
INSERT INTO portal_role_capability_grants (portal_role, capability_id)
SELECT 'superadmin', id FROM capabilities WHERE domain = 'platform'
ON CONFLICT DO NOTHING;

View File

@ -0,0 +1,41 @@
-- Migration 080: Antrag auf Vereinsgründung (M7)
-- Nutzer verified_pending_club stellt Antrag; Plattform-Admin legt Verein + Abo an.
CREATE TABLE IF NOT EXISTS club_creation_requests (
id SERIAL PRIMARY KEY,
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
proposed_name VARCHAR(200) NOT NULL,
proposed_abbreviation VARCHAR(50),
proposed_description TEXT,
message TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn')),
decided_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
decided_at TIMESTAMP,
created_club_id INT REFERENCES clubs(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_club_creation_requests_pending
ON club_creation_requests (profile_id)
WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_club_creation_requests_status
ON club_creation_requests (status, created_at);
CREATE INDEX IF NOT EXISTS idx_club_creation_requests_profile
ON club_creation_requests (profile_id);
DROP TRIGGER IF EXISTS club_creation_requests_update ON club_creation_requests;
CREATE TRIGGER club_creation_requests_update
BEFORE UPDATE ON club_creation_requests
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
-- Capabilities (CAPABILITY_CATALOG.v1.md — club.creation_request.*)
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
VALUES
('club.creation_request.create', 'Vereinsgründung beantragen', 'club', 'verified_pending_club', NULL),
('club.creation_request.read_own', 'Eigene Gründungsanträge', 'club', 'verified_pending_club', NULL),
('club.creation_request.withdraw', 'Gründungsantrag zurückziehen', 'club', 'verified_pending_club', NULL)
ON CONFLICT (id) DO NOTHING;

View File

@ -0,0 +1,13 @@
-- Migration 081: Status superseded wenn freigegebener Verein gelöscht wurde
ALTER TABLE club_creation_requests
DROP CONSTRAINT IF EXISTS club_creation_requests_status_check;
ALTER TABLE club_creation_requests
ADD CONSTRAINT club_creation_requests_status_check
CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn', 'superseded'));
-- Bestehende Drift: approved ohne Verein (ON DELETE SET NULL auf created_club_id)
UPDATE club_creation_requests
SET status = 'superseded', updated_at = NOW()
WHERE status = 'approved' AND created_club_id IS NULL;

View File

@ -0,0 +1,36 @@
-- Migration 082: Plattform-/Profil-Ausnahmen vom Vereins-Kontingent (M5+)
-- Superadmin & konfigurierbare Rollen/Profile verbrauchen kein club_feature_usage.
CREATE TABLE IF NOT EXISTS platform_role_club_feature_exemptions (
id SERIAL PRIMARY KEY,
portal_role TEXT NOT NULL,
feature_id TEXT REFERENCES features(id) ON DELETE CASCADE,
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_platform_role_club_feat_exempt
ON platform_role_club_feature_exemptions (portal_role, COALESCE(feature_id, '*'));
CREATE TABLE IF NOT EXISTS profile_club_feature_exemptions (
id SERIAL PRIMARY KEY,
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
feature_id TEXT REFERENCES features(id) ON DELETE CASCADE,
reason TEXT,
set_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_profile_club_feat_exempt
ON profile_club_feature_exemptions (profile_id, COALESCE(feature_id, '*'));
CREATE INDEX IF NOT EXISTS idx_profile_club_feat_exempt_profile
ON profile_club_feature_exemptions (profile_id);
-- Superadmin: alle Vereins-Features ohne Kontingent-Verbrauch
INSERT INTO platform_role_club_feature_exemptions (portal_role, feature_id, note)
SELECT 'superadmin', NULL, 'Plattform-Administrator: kein Vereins-Kontingent'
WHERE NOT EXISTS (
SELECT 1 FROM platform_role_club_feature_exemptions
WHERE portal_role = 'superadmin' AND feature_id IS NULL
);

View File

@ -0,0 +1,103 @@
-- Migration 083: Vereins-Kontingent-Bypass über Capability-System (kein Parallel-Schema)
-- Ersetzt platform_role_club_feature_exemptions / profile_club_feature_exemptions aus 082.
-- Einzelprofil-Grants (ergänzt portal_role_capability_grants)
CREATE TABLE IF NOT EXISTS profile_capability_grants (
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
capability_id TEXT NOT NULL REFERENCES capabilities(id) ON DELETE CASCADE,
reason TEXT,
granted_by_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (profile_id, capability_id)
);
CREATE INDEX IF NOT EXISTS idx_profile_capability_grants_cap
ON profile_capability_grants(capability_id);
-- Bypass-Capabilities (CAPABILITY_CATALOG — konfigurierbar via portal/profile grants)
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
VALUES
(
'platform.club_quota.bypass',
'Vereins-Kontingent umgehen (alle Features)',
'platform',
'platform_admin',
NULL
)
ON CONFLICT (id) DO NOTHING;
-- Superadmin: alle Plattform-Capabilities inkl. bypass (079-Seed deckt domain=platform ab)
INSERT INTO portal_role_capability_grants (portal_role, capability_id)
SELECT 'superadmin', 'platform.club_quota.bypass'
WHERE NOT EXISTS (
SELECT 1 FROM portal_role_capability_grants
WHERE portal_role = 'superadmin' AND capability_id = 'platform.club_quota.bypass'
);
-- ── Daten aus 082 übernehmen (falls vorhanden) ─────────────────────────────
DO $migrate082$
DECLARE
r RECORD;
cap_id TEXT;
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'platform_role_club_feature_exemptions'
) THEN
RETURN;
END IF;
FOR r IN
SELECT portal_role, feature_id, note
FROM platform_role_club_feature_exemptions
LOOP
IF r.feature_id IS NULL THEN
cap_id := 'platform.club_quota.bypass';
ELSE
cap_id := 'platform.club_quota.bypass.' || r.feature_id;
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
VALUES (
cap_id,
'Vereins-Kontingent umgehen: ' || r.feature_id,
'quota_bypass',
'active_member',
r.feature_id
)
ON CONFLICT (id) DO NOTHING;
END IF;
INSERT INTO portal_role_capability_grants (portal_role, capability_id)
VALUES (lower(trim(r.portal_role)), cap_id)
ON CONFLICT DO NOTHING;
END LOOP;
FOR r IN
SELECT profile_id, feature_id, reason, set_by_profile_id
FROM profile_club_feature_exemptions
LOOP
IF r.feature_id IS NULL THEN
cap_id := 'platform.club_quota.bypass';
ELSE
cap_id := 'platform.club_quota.bypass.' || r.feature_id;
INSERT INTO capabilities (id, name, domain, min_account_state, linked_feature_id)
VALUES (
cap_id,
'Vereins-Kontingent umgehen: ' || r.feature_id,
'quota_bypass',
'active_member',
r.feature_id
)
ON CONFLICT (id) DO NOTHING;
END IF;
INSERT INTO profile_capability_grants (
profile_id, capability_id, reason, granted_by_profile_id
)
VALUES (r.profile_id, cap_id, r.reason, r.set_by_profile_id)
ON CONFLICT DO NOTHING;
END LOOP;
DROP TABLE IF EXISTS profile_club_feature_exemptions;
DROP TABLE IF EXISTS platform_role_club_feature_exemptions;
END
$migrate082$;

View File

@ -0,0 +1,15 @@
-- Migration 084: Modul-Registrierung für Rechte & Kontingente (Registry-first)
-- capabilities/features mit module=NULL = Legacy-Katalog-Seed (nicht in Admin-Matrix).
-- module IS NOT NULL = vom Modul bei Implementierung registriert.
ALTER TABLE capabilities
ADD COLUMN IF NOT EXISTS module TEXT;
ALTER TABLE features
ADD COLUMN IF NOT EXISTS module TEXT;
CREATE INDEX IF NOT EXISTS idx_capabilities_module
ON capabilities(module) WHERE module IS NOT NULL AND active = true;
CREATE INDEX IF NOT EXISTS idx_features_module
ON features(module) WHERE module IS NOT NULL AND active = true;

View File

@ -0,0 +1,181 @@
-- Migration 085: Planungskontext in Übungs-KI-Prompts (Phase D)
-- Platzhalter: {{planning_context_json}}, {{#has_planning_context}} … {{/has_planning_context}}
UPDATE ai_prompts
SET template = $s$Du bist Assistent fuer Kampfsport-Trainer.
Erstelle eine kurze Kurzbeschreibung fuer Listen und Trainingsplaene.
Anforderungen:
- Hochstens etwa 200 Zeichen (bei Bedarf gekuerzt fuer Mobile)
- Kern: Welche Trainingsqualitaeten? Wie fuehrt man die Uebung kurz aus?
- Sachlich, auf Deutsch
Uebung: {{exercise_title}}
Fokuskontext: {{exercise_focus_area}}
Ziel (Fliesstext, kann HTML sein): {{exercise_goal}}
Durchfuehrung (Fliesstext, kann HTML sein): {{exercise_execution}}
{{#has_planning_context}}
Planungskontext (JSON Einordnung in Trainingsplan oder Progressionspfad):
{{planning_context_json}}
{{/has_planning_context}}
Antworte NUR mit der Kurzbeschreibung als einfachen Text (keine Markdown-Codeblocks, keine Anfuehrungszeichen um den ganzen Text).$s$,
default_template = $s$Du bist Assistent fuer Kampfsport-Trainer.
Erstelle eine kurze Kurzbeschreibung fuer Listen und Trainingsplaene.
Anforderungen:
- Hochstens etwa 200 Zeichen (bei Bedarf gekuerzt fuer Mobile)
- Kern: Welche Trainingsqualitaeten? Wie fuehrt man die Uebung kurz aus?
- Sachlich, auf Deutsch
Uebung: {{exercise_title}}
Fokuskontext: {{exercise_focus_area}}
Ziel (Fliesstext, kann HTML sein): {{exercise_goal}}
Durchfuehrung (Fliesstext, kann HTML sein): {{exercise_execution}}
{{#has_planning_context}}
Planungskontext (JSON Einordnung in Trainingsplan oder Progressionspfad):
{{planning_context_json}}
{{/has_planning_context}}
Antworte NUR mit der Kurzbeschreibung als einfachen Text (keine Markdown-Codeblocks, keine Anfuehrungszeichen um den ganzen Text).$s$
WHERE slug = 'exercise_summary';
UPDATE ai_prompts
SET template = $j$Du bist Assistent fuer Kampfsport-Trainer.
Ordne diese Uebung dem globalen Skill-Katalog zu.
Daten zur Uebung:
Titel: {{exercise_title}}
Fokuskontext (optional): {{exercise_focus_area}}
Ziel (gekuerzt_plain): {{exercise_goal}}
Durchfuehrung (gekuerzt_plain): {{exercise_execution}}
{{#has_planning_context}}
Planungskontext (JSON):
{{planning_context_json}}
{{/has_planning_context}}
Verfuegbare Faehigkeiten (Auswahl NUR ueber diese IDs keine anderen IDs verwenden):
{{skills_catalog}}
Waehle hoechstens 5 passende Skills. Für jede Faehigkeit:
- skill_id: ganze Zahl aus der Liste
- required_level: eines von basis, grundlagen, aufbau, fortgeschritten, optimierung
- target_level: derselbe Wertvorrat
- intensity: eines von niedrig, mittel, hoch
- is_primary (optional): true fuer die Hauptfaehigkeit der Uebung, sondern false/weglassen
Antworte NUR mit einem JSON-Array ohne Erklaertext, keine Markdown-Fences.
Beispielformat:
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
Wenn nichts gut passt, antworte mit [].$j$,
default_template = $j$Du bist Assistent fuer Kampfsport-Trainer.
Ordne diese Uebung dem globalen Skill-Katalog zu.
Daten zur Uebung:
Titel: {{exercise_title}}
Fokuskontext (optional): {{exercise_focus_area}}
Ziel (gekuerzt_plain): {{exercise_goal}}
Durchfuehrung (gekuerzt_plain): {{exercise_execution}}
{{#has_planning_context}}
Planungskontext (JSON):
{{planning_context_json}}
{{/has_planning_context}}
Verfuegbare Faehigkeiten (Auswahl NUR ueber diese IDs keine anderen IDs verwenden):
{{skills_catalog}}
Waehle hoechstens 5 passende Skills. Für jede Faehigkeit:
- skill_id: ganze Zahl aus der Liste
- required_level: eines von basis, grundlagen, aufbau, fortgeschritten, optimierung
- target_level: derselbe Wertvorrat
- intensity: eines von niedrig, mittel, hoch
- is_primary (optional): true fuer die Hauptfaehigkeit der Uebung, sondern false/weglassen
Antworte NUR mit einem JSON-Array ohne Erklaertext, keine Markdown-Fences.
Beispielformat:
[{"skill_id": 1, "required_level": "grundlagen", "target_level": "aufbau", "intensity": "hoch", "is_primary": true}]
Wenn nichts gut passt, antworte mit [].$j$
WHERE slug = 'exercise_skill_suggestions';
UPDATE ai_prompts
SET template = $t$Du bist Assistent fuer Kampfsport-Trainer.
Ueberarbeite die Anleitung dieser Uebung: verbessere Formulierung, ergaenze fehlende Kernpunkte, kuerze ueberfluessige Passagen.
Wichtig: Texte sollen praezise und nachvollziehbar bleiben keine Fuellsaetze, keine Wiederholungen, kein Marketing.
Stil:
- Deutsch, sachlich, direkt an Trainer gerichtet (Durchfuehrung: Imperativ oder klare Schritte)
- Ziel: 13 kurze Absaetze (Kern des Trainingsziels)
- Durchfuehrung: klare Schritte (nummerierte Liste oder kurze Absaetze)
- Vorbereitung/Aufbau: nur wenn noetig (Raum, Material, Aufbau) sonst leerer String
- Trainer-Hinweise: Sicherheit, typische Fehler, Coaching-Tipps knapp, Stichpunkte oder kurze Absaetze
Format (HTML fuer Rich-Text-Editor):
- Erlaubt: <p>, <ul>, <ol>, <li>, <strong>, <em>, <br>
- Keine Ueberschriften (h1h6), keine Tabellen, kein Markdown, keine Code-Fences
- Medienverweise {{exerciseMedia:ID}} aus den Eingabetexten UNVERAENDERT an passender Stelle uebernehmen
Eingabe:
Titel: {{exercise_title}}
Fokuskontext: {{exercise_focus_area}}
Ziel (Plaintext, Ausgang): {{exercise_goal}}
Durchfuehrung (Plaintext, Ausgang): {{exercise_execution}}
Vorbereitung/Aufbau (Plaintext, Ausgang): {{exercise_preparation}}
Trainer-Hinweise (Plaintext, Ausgang): {{exercise_trainer_notes}}
{{#has_planning_context}}
Planungskontext (JSON):
{{planning_context_json}}
{{/has_planning_context}}
Antworte NUR mit einem JSON-Objekt (kein Text davor/danach):
{
"goal": "<p>…</p>",
"execution": "<ol><li>…</li></ol>",
"preparation": "<p>…</p> oder \"\"",
"trainer_notes": "<ul><li>…</li></ul> oder \"\""
}
Leere Felder als leerer String "" wenn nichts Sinnvolles ergibt.$t$,
default_template = $t$Du bist Assistent fuer Kampfsport-Trainer.
Ueberarbeite die Anleitung dieser Uebung: verbessere Formulierung, ergaenze fehlende Kernpunkte, kuerze ueberfluessige Passagen.
Wichtig: Texte sollen praezise und nachvollziehbar bleiben keine Fuellsaetze, keine Wiederholungen, kein Marketing.
Stil:
- Deutsch, sachlich, direkt an Trainer gerichtet (Durchfuehrung: Imperativ oder klare Schritte)
- Ziel: 13 kurze Absaetze (Kern des Trainingsziels)
- Durchfuehrung: klare Schritte (nummerierte Liste oder kurze Absaetze)
- Vorbereitung/Aufbau: nur wenn noetig (Raum, Material, Aufbau) sonst leerer String
- Trainer-Hinweise: Sicherheit, typische Fehler, Coaching-Tipps knapp, Stichpunkte oder kurze Absaetze
Format (HTML fuer Rich-Text-Editor):
- Erlaubt: <p>, <ul>, <ol>, <li>, <strong>, <em>, <br>
- Keine Ueberschriften (h1h6), keine Tabellen, kein Markdown, keine Code-Fences
- Medienverweise {{exerciseMedia:ID}} aus den Eingabetexten UNVERAENDERT an passender Stelle uebernehmen
Eingabe:
Titel: {{exercise_title}}
Fokuskontext: {{exercise_focus_area}}
Ziel (Plaintext, Ausgang): {{exercise_goal}}
Durchfuehrung (Plaintext, Ausgang): {{exercise_execution}}
Vorbereitung/Aufbau (Plaintext, Ausgang): {{exercise_preparation}}
Trainer-Hinweise (Plaintext, Ausgang): {{exercise_trainer_notes}}
{{#has_planning_context}}
Planungskontext (JSON):
{{planning_context_json}}
{{/has_planning_context}}
Antworte NUR mit einem JSON-Objekt (kein Text davor/danach):
{
"goal": "<p>…</p>",
"execution": "<ol><li>…</li></ol>",
"preparation": "<p>…</p> oder \"\"",
"trainer_notes": "<ul><li>…</li></ul> oder \"\""
}
Leere Felder als leerer String "" wenn nichts Sinnvolles ergibt.$t$
WHERE slug = 'exercise_instruction_rewrite';

View File

@ -0,0 +1,52 @@
-- Migration 087: Planungs-KI — LLM Start/Ziel-Extraktion aus Trainer-Anfrage (Alternative zu Regex)
INSERT INTO ai_prompts (
slug, display_name, description, template,
category, output_format, output_schema, is_system_default, default_template, active, sort_order
)
SELECT
'planning_progression_start_target',
'Progressions-Roadmap Start/Ziel-Extraktion',
'Versteht die Trainer-Anfrage und formuliert dedizierte Ausgangslage, Zielzustand und Ergänzungen (ohne Gruppen-Tracking).',
$t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen didaktischen Progressionsgraphen.
Trainer-Anfrage (Ursprungstext):
{{goal_query}}
Semantic Brief (heuristisch): {{semantic_brief_json}}
Bereits vom Trainer eingegebene Ergänzungen (falls vorhanden): {{user_notes}}
Aufgabe:
1. **primary_topic** Kern-Thema/Technik in kurzer, präziser Bezeichnung (z. B. Kumite Beinarbeit, Mae Geri).
2. **start_situation** Ausgangslage in eigenen Worten: Was kann der Athlet/die Gruppe *jetzt* (laut Anfrage oder sinnvoll ableitbar)? Konkret, beobachtbar, ohne Gruppenanalyse aus der Datenbank.
3. **target_state** Zielzustand in eigenen Worten: Was soll am Ende der Progression erreicht sein? Konkret, didaktisch nutzbar.
4. **roadmap_notes** Ergänzungen aus dem Ursprungstext: Fokus, Kontext (z. B. Kumite), besondere Anforderungen, Einschränkungen, die der Trainer erwähnt hat oder die für die Roadmap relevant sind. Nicht wiederholen, was bereits in start_situation/target_state steht.
5. **extraction_notes** Kurz (12 Sätze): Was war explizit vs. abgeleitet? Wo war die Anfrage unklar?
Regeln:
- Keine Gruppenanalyse nur das, was aus dem Text hervorgeht oder didaktisch naheliegend formuliert ist.
- Formuliere start_situation und target_state **eigenständig und verständlich**, nicht nur Textfragmente kopieren.
- Bei von bis : Start und Ziel aus diesem Bogen schärfen und präzise beschreiben.
- Bei nur einem Thema ohne Bogen: start_situation und target_state didaktisch sinnvoll formulieren oder leer lassen, wenn nicht ableitbar dann in extraction_notes erklären.
- Antworte NUR mit JSON.
{
"primary_topic": "",
"start_situation": "",
"target_state": "",
"roadmap_notes": "",
"extraction_notes": ""
}$t$,
'training',
'json',
'{"type":"object","properties":{"primary_topic":{"type":"string"},"start_situation":{"type":"string"},"target_state":{"type":"string"},"roadmap_notes":{"type":"string"},"extraction_notes":{"type":"string"}}}'::jsonb,
true,
NULL,
true,
13
WHERE NOT EXISTS (SELECT 1 FROM ai_prompts WHERE slug = 'planning_progression_start_target');
UPDATE ai_prompts SET default_template = template
WHERE slug = 'planning_progression_start_target'
AND (default_template IS NULL OR TRIM(default_template) = '');

View File

@ -0,0 +1,8 @@
-- Migration 088: Planungs-Roadmap-Artefakt am Progressionsgraph (JSONB, optional).
-- Speichert Ziel, Start/Ziel, progression_roadmap + stage_specs für Wiederaufnahme der KI-Planung.
ALTER TABLE exercise_progression_graphs
ADD COLUMN IF NOT EXISTS planning_roadmap JSONB;
COMMENT ON COLUMN exercise_progression_graphs.planning_roadmap IS
'Optionales Planungs-Artefakt (goal_query, resolved_structured, progression_roadmap, stage_specs) — Schema v1 im App-Code.';

View File

@ -0,0 +1,67 @@
-- Migration 089: Planungs-Intent — Zielanalyse + Stufenspecs (anti_patterns, success_criteria)
UPDATE ai_prompts SET
description = 'Phase A: Ist-/Soll, Erfolgskriterien und explizite Ausschlüsse (ohne Gruppenkontext).',
template = $t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Wichtig:
- Keine Gruppenanalyse nur didaktischer Pfad für die Technik/das Thema.
- Explizite Negationen aus der Anfrage (ohne/kein/nicht ) in constraints.excluded_themes übernehmen nicht raten.
- success_criteria: messbar, für späteres Übungs-Matching (Titel + Kurzbeschreibung + Übungsziel).
Antworte NUR mit JSON:
{
"primary_topic": "Hauptthema",
"start_assumption": "Voraussetzungen für den Einstieg",
"target_state": "Konkreter Zielzustand der Progression",
"success_criteria": ["messbare Kriterien entlang des Pfads"],
"constraints": {
"partner_required": false,
"excluded_themes": ["wörtliche Negationen, z. B. keine Kumite-Anwendung"],
"trainer_notes": "optional: Fokus aus Ergänzungen"
}
}$t$,
default_template = template
WHERE slug = 'planning_progression_goal_analysis';
UPDATE ai_prompts SET
description = 'Phase C: Belastung, Übungstyp, Erfolgskriterien und anti_patterns je Major Step — für Retrieval-Matching.',
template = $t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Major Steps: {{major_steps_json}}
Planungs-Intent (Pfadweite Regeln): {{intent_context_json}}
Semantic Brief: {{semantic_brief_json}}
Aufgabe je Major Step Felder für automatisches Übungs-Matching (nicht nur Titel):
- learning_goal: messbares Stufen-Lernziel (was die Übung bringen soll)
- load_profile: z. B. koordination, präzision, kraft, athletik
- exercise_type: kihon_einzel | partner_drill | kombination | kraft_auxiliary
- success_criteria: 24 prüfbare Kriterien an Kurzbeschreibung + Übungsziel (nicht nur Technikname im Titel)
- anti_patterns: 25 Dinge, die für diese Stufe unpassend sind
Regeln:
1. Jede explicit_exclusions / excluded_themes aus intent_context und Zielanalyse MUSS in anti_patterns jeder Stufe vorkommen (umformuliert ok).
2. Keine neuen Ausschlüsse erfinden, die nicht in Anfrage/Intent/Zielanalyse stehen.
3. success_criteria Pfad-weit + stufenspezifisch kombinieren.
4. partner_drill nur wenn Partner/Kumite nicht ausgeschlossen ist.
Antworte NUR mit JSON:
{
"stage_specs": [
{
"major_step_index": 0,
"learning_goal": "",
"load_profile": ["koordination"],
"exercise_type": "kihon_einzel",
"success_criteria": [""],
"anti_patterns": [""]
}
]
}$t$,
default_template = template
WHERE slug = 'planning_progression_stage_spec';

View File

@ -0,0 +1,43 @@
-- Migration 090: Stufenspecs — start_state / target_state pro Major Step (Soll-Verkettung)
UPDATE ai_prompts SET
description = 'Phase C: Stufenspezifikation inkl. Soll-Start und Stufen-Ziel je Major Step.',
template = $t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Major Steps: {{major_steps_json}}
Planungs-Intent (Pfadweite Regeln): {{intent_context_json}}
Semantic Brief: {{semantic_brief_json}}
Jede Stufe ist ein Übergang im Gesamtpfad:
- start_state: Soll-Zustand zu Beginn (= Ziel der vorherigen Stufe; Stufe 0 = Pfad-Start)
- target_state: Zielzustand nach dieser Stufe (= Soll für die nächste Stufe)
- learning_goal: messbares Lernziel der Übungssuche (was die Übung bringen soll)
Felder je Major Step:
- load_profile, exercise_type, success_criteria, anti_patterns (wie bisher)
Regeln:
1. start_state/target_state aus Zielanalyse und Major Steps ableiten konsistente Kette.
2. explicit_exclusions aus intent_context in anti_patterns jeder Stufe.
3. success_criteria: prüfbar an Kurzbeschreibung + Übungsziel.
4. Keine erfundenen Ausschlüsse.
Antworte NUR mit JSON:
{
"stage_specs": [
{
"major_step_index": 0,
"start_state": "",
"target_state": "",
"learning_goal": "",
"load_profile": ["koordination"],
"exercise_type": "kihon_einzel",
"success_criteria": [""],
"anti_patterns": [""]
}
]
}$t$,
default_template = template
WHERE slug = 'planning_progression_stage_spec';

View File

@ -0,0 +1,172 @@
-- Migration 091: Planungs-KI H1 — Katalog-Guidance-Platzhalter in Progressions-Prompts
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
{{catalog_guidance_block}}
{{catalog_context_json}}
Wichtig: Wenn Katalog-Kontext gesetzt ist, haben dessen QS-Kriterien Vorrang vor allgemeinen Technik-/Wettkampf-Maßstäben.
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte (Kraft, Geschwindigkeit, Anwendung, Perfektion)?
6. Gibt es Schritte ohne Bezug zum Hauptthema (z. B. reine Kraftübungen bei einer Technik)?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
Nur Indizes aus dem steps_json verwenden Länge muss exakt der Schrittzahl entsprechen.
Wenn wichtige Zwischenschritte fehlen oder Schritte themenfremd sind: suggested_new_exercises mit konkreten Übungs-Ideen (Titel + Kurzskizze), jeweils mit insert_after_step_index (0-basiert: nach welchem Schritt einfügen).
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""],
"suggested_new_exercises": [
{
"title_hint": "Mae Geri Kraftentwicklung am Sandsack",
"sketch": "Gezielte Kraft- und Schnelligkeitsentwicklung für Mae Geri …",
"phase": "vertiefung",
"insert_after_step_index": 2,
"rationale": "Schließt Lücke zwischen Grundlagen und Gleichgewichtstritt"
}
]
}$t$,
default_template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
{{catalog_guidance_block}}
{{catalog_context_json}}
Wichtig: Wenn Katalog-Kontext gesetzt ist, haben dessen QS-Kriterien Vorrang vor allgemeinen Technik-/Wettkampf-Maßstäben.
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte (Kraft, Geschwindigkeit, Anwendung, Perfektion)?
6. Gibt es Schritte ohne Bezug zum Hauptthema (z. B. reine Kraftübungen bei einer Technik)?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes (beste didaktische Reihenfolge).
Nur Indizes aus dem steps_json verwenden Länge muss exakt der Schrittzahl entsprechen.
Wenn wichtige Zwischenschritte fehlen oder Schritte themenfremd sind: suggested_new_exercises mit konkreten Übungs-Ideen (Titel + Kurzskizze), jeweils mit insert_after_step_index (0-basiert: nach welchem Schritt einfügen).
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""],
"suggested_new_exercises": [
{
"title_hint": "Mae Geri Kraftentwicklung am Sandsack",
"sketch": "Gezielte Kraft- und Schnelligkeitsentwicklung für Mae Geri …",
"phase": "vertiefung",
"insert_after_step_index": 2,
"rationale": "Schließt Lücke zwischen Grundlagen und Gleichgewichtstritt"
}
]
}$t$
WHERE slug = 'planning_exercise_path_qa';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
{{catalog_guidance_block}}
Wichtig: Keine Gruppenanalyse nur didaktischer Pfad für die Technik/das Thema.
Antworte NUR mit JSON:
{
"primary_topic": "Mae Geri",
"start_assumption": "Welche Voraussetzungen werden für den Einstieg angenommen",
"target_state": "Konkreter Zielzustand der Progression",
"success_criteria": ["messbare Kriterien"],
"constraints": { "partner_required": false }
}$t$,
default_template = template
WHERE slug = 'planning_progression_goal_analysis';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und erstellst eine didaktische Roadmap für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Semantic Brief: {{semantic_brief_json}}
Anzahl Major Steps (N): {{max_steps}}
{{catalog_guidance_block}}
{{catalog_context_json}}
Erzeuge zuerst 812 micro_objectives (phase, title, weight, depends_on), dann konsolidiere auf genau N major_steps.
Phasen: einstieg, grundlage, vertiefung, anwendung, perfektion in sinnvoller Reihenfolge (Grundlagen vor Perfektion).
Beachte Katalog-Roadmap-Hinweise, falls gesetzt.
Antworte NUR mit JSON:
{
"micro_objectives": [
{ "id": "m1", "phase": "grundlage", "title": "", "weight": 0.9, "depends_on": [] }
],
"major_steps": [
{ "index": 0, "phase": "grundlage", "learning_goal": "", "consolidates": ["m1","m2"], "rationale": "" }
],
"consolidation_notes": [""]
}$t$,
default_template = template
WHERE slug = 'planning_progression_roadmap';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Major Steps: {{major_steps_json}}
{{catalog_guidance_block}}
{{catalog_context_json}}
Für jeden Major Step: messbares Lernziel, load_profile (z. B. koordination, präzision, kraft), exercise_type (kihon_einzel, partner_drill, kombination, kraft_auxiliary), success_criteria, anti_patterns (z. B. reine Kraft ohne Technikbezug).
Beachte Katalog-QS-Kriterien und Anti-Patterns, falls gesetzt.
Antworte NUR mit JSON:
{
"stage_specs": [
{
"major_step_index": 0,
"learning_goal": "",
"load_profile": ["koordination", "gleichgewicht"],
"exercise_type": "kihon_einzel",
"success_criteria": [""],
"anti_patterns": [""]
}
]
}$t$,
default_template = template
WHERE slug = 'planning_progression_stage_spec';

View File

@ -0,0 +1,176 @@
-- Migration 092: Katalog-Prompt-Slots (H2) — Slot-Typ-Vokabular + Werte pro Stammdaten-Zeile
CREATE TABLE IF NOT EXISTS catalog_prompt_slot_types (
slot_key VARCHAR(64) PRIMARY KEY,
display_name VARCHAR(200) NOT NULL,
description TEXT,
applicable_kinds TEXT[] NOT NULL DEFAULT '{}',
sort_order INT DEFAULT 99,
for_llm BOOLEAN NOT NULL DEFAULT true,
for_code BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS catalog_prompt_slots (
id SERIAL PRIMARY KEY,
catalog_kind VARCHAR(32) NOT NULL,
catalog_id INT NOT NULL,
slot_key VARCHAR(64) NOT NULL REFERENCES catalog_prompt_slot_types(slot_key) ON DELETE CASCADE,
content TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (catalog_kind, catalog_id, slot_key)
);
CREATE INDEX IF NOT EXISTS idx_catalog_prompt_slots_kind_id
ON catalog_prompt_slots (catalog_kind, catalog_id);
INSERT INTO catalog_prompt_slot_types (slot_key, display_name, description, applicable_kinds, sort_order, for_llm, for_code)
VALUES
(
'description',
'Allgemeine Beschreibung',
'Fachliche Einordnung des Katalog-Eintrags für Planungs-KI.',
ARRAY['focus_area', 'training_type', 'target_group', 'style_direction'],
10,
true,
false
),
(
'hints_on_progression',
'Hinweise Progressionsgraph',
'Didaktik für Roadmap, Major Steps und Stufenspezifikation.',
ARRAY['focus_area', 'training_type', 'target_group', 'style_direction'],
20,
true,
false
),
(
'hints_on_exercise',
'Hinweise Übungsanlage',
'Kontext für Gap-Fill, Übungs-KI und Schnellanlage.',
ARRAY['focus_area', 'training_type', 'target_group', 'style_direction'],
30,
true,
false
),
(
'hints_on_path_qa',
'Hinweise Pfad-QS',
'Bewertungsmaßstäbe für Pfad-Qualitätssicherung.',
ARRAY['focus_area', 'training_type', 'target_group', 'style_direction'],
40,
true,
false
),
(
'anti_patterns',
'Anti-Patterns',
'Explizite Fehlbewertungen vermeiden.',
ARRAY['focus_area', 'training_type', 'target_group', 'style_direction'],
50,
true,
false
),
(
'rematch_guard',
'Rematch-Guard',
'Wann kein Auto-Rematch sinnvoll ist (primär Code-Logik).',
ARRAY['focus_area', 'training_type', 'target_group', 'style_direction'],
60,
false,
true
)
ON CONFLICT (slot_key) DO NOTHING;
-- Seed aus H1-Registry (Name-Match auf Stammdaten)
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'focus_area', fa.id, 'description',
'Planung zielt auf Prävention, Deeskalation, Grenzen und sichere Übungsformen — nicht auf Wettkampf-Perfektion oder Technik-Show.'
FROM focus_areas fa WHERE fa.name ILIKE 'Gewaltschutz'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'focus_area', fa.id, 'hints_on_path_qa',
'Gute Pfade bauen Sicherheit, Kommunikation und Alternativen auf; „Lücken“ sind fehlende Deeskalations- oder Rollenspiel-Stufen, nicht fehlende Kick-Varianten.'
FROM focus_areas fa WHERE fa.name ILIKE 'Gewaltschutz'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'focus_area', fa.id, 'anti_patterns',
'Nicht nach Kumite-Tiefe, Explosivität oder Wettkampf-Belastung bewerten.'
FROM focus_areas fa WHERE fa.name ILIKE 'Gewaltschutz'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'training_type', tt.id, 'description',
'Partizipation, Verständlichkeit, Freude am Bewegen; weniger maximale Spezialisierung.'
FROM training_types tt WHERE tt.name ILIKE 'Breitensport'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'training_type', tt.id, 'hints_on_path_qa',
'Hohe OK-Rate bei moderatem Schwierigkeitsanstieg; „Perfektion“-Stufen nur optional, nicht als Pflicht-Lücke.'
FROM training_types tt WHERE tt.name ILIKE 'Breitensport'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'training_type', tt.id, 'rematch_guard',
'Keine leeren Slots erzwingen, nur um eine Leistungs-Perfektionsstufe zu füllen.'
FROM training_types tt WHERE tt.name ILIKE 'Breitensport'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'target_group', tg.id, 'description',
'Kinder: kurze Einheiten, spielerische Einstiege, Sicherheit und altersgerechte Komplexität.'
FROM target_groups tg WHERE tg.name ILIKE 'Kinder'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'target_group', tg.id, 'hints_on_path_qa',
'Didaktik ohne Überforderung; klare Regeln und Sicherheit vor Perfektion; Lücken bei Spiel-/Rollenelementen wichtiger als Wettkampftiefe.'
FROM target_groups tg WHERE tg.name ILIKE 'Kinder'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'target_group', tg.id, 'anti_patterns',
'Keine Erwachsenen-Wettkampf-Perfektion als QS-Maßstab.'
FROM target_groups tg WHERE tg.name ILIKE 'Kinder'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'target_group', tg.id, 'description',
'Leistungsgruppe: höhere Anspruchskurven und Spezialisierung sind fachlich passend.'
FROM target_groups tg WHERE tg.name ILIKE 'Leistungssportler'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'target_group', tg.id, 'hints_on_path_qa',
'Höhere Anspruchskurven, Belastungs- und Kombinationsprogressionen sind relevant; Lücken in Spezialisierung können echte Hinweise sein.'
FROM target_groups tg WHERE tg.name ILIKE 'Leistungssportler'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'style_direction', sd.id, 'description',
'Shotokan-Linie: klare Kihon-Struktur, Hüft- und Standarbeit als wiederkehrende Qualitätsanker.'
FROM style_directions sd WHERE sd.name ILIKE 'Shotokan'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'style_direction', sd.id, 'hints_on_progression',
'Nuancen in Stellung und Hüfttechnik, kein neuer Planungstyp.'
FROM style_directions sd WHERE sd.name ILIKE 'Shotokan'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'training_type', tt.id, 'description',
'Wettkampforientiertes Training mit höherer Anspruchskurve und belastungsnahen Phasen.'
FROM training_types tt WHERE tt.name ILIKE 'Wettkampf'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'training_type', tt.id, 'hints_on_path_qa',
'Spezialisierung, Kombination und Belastung unter Druck sind relevant; Lücken in Anwendungs- oder Perfektionsphasen können echte Hinweise sein.'
FROM training_types tt WHERE tt.name ILIKE 'Wettkampf'
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();

View File

@ -0,0 +1,199 @@
-- Migration 093: Planungs-KI — granulare Katalog-Slot-Platzhalter in Prompt-Templates
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und prüfst einen vorgeschlagenen Übungspfad.
Ziel-Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Schritte (JSON): {{steps_json}}
Erkannte Lücken: {{gaps_json}}
Eingefügte Brücken: {{bridge_inserts_json}}
Katalog-Kontext für Bewertung (Trainer-Auswahl leere Zeilen ignorieren):
Primärfokus:
{{focus_area_description}}
QS: {{focus_area_hints_on_path_qa}}
Vermeiden: {{focus_area_anti_patterns}}
Trainingsstil:
{{training_type_description}}
QS: {{training_type_hints_on_path_qa}}
Zielgruppe:
{{target_group_description}}
QS: {{target_group_hints_on_path_qa}}
Stilrichtung:
{{style_direction_description}}
QS: {{style_direction_hints_on_path_qa}}
{{catalog_context_json}}
Wichtig: Wenn Katalog-Slots gesetzt sind, haben diese Vorrang vor allgemeinen Technik-/Wettkampf-Maßstäben.
Prüfe:
1. Deckt der Pfad das Hauptthema der Anfrage ab (nicht nur Oberbegriffe)?
2. Ist die Reihenfolge didaktisch sinnvoll (Einstieg Vertiefung Ziel)?
3. Sind Sprünge zwischen benachbarten Schritten zu groß?
4. Sind Brücken-Übungen sinnvoll oder überflüssig?
5. Fehlen wichtige Zwischenschritte gemäß Katalog-QS-Hinweisen, nicht pauschal Perfektion?
6. Gibt es Schritte ohne Bezug zum Hauptthema?
Wenn die Reihenfolge verbessert werden sollte: ordered_step_indices = Permutation der aktuellen 0-basierten Schritt-Indizes.
Wenn wichtige Zwischenschritte fehlen: suggested_new_exercises mit Titel + Kurzskizze und insert_after_step_index.
Antworte NUR mit JSON:
{
"overall_ok": true,
"quality_score": 0.85,
"topic_coverage": "Kurz: wie gut das Hauptthema abgedeckt ist",
"ordered_step_indices": [0, 1, 2, 3],
"issues": [""],
"sequence_notes": [""],
"recommendations": [""],
"suggested_new_exercises": []
}$t$,
default_template = template
WHERE slug = 'planning_exercise_path_qa';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und analysierst eine Anfrage für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Katalog-Kontext (Primärfokus / Trainingsstil / Zielgruppe / Stil leere Zeilen ignorieren):
Primärfokus: {{focus_area_description}}
Progression: {{focus_area_hints_on_progression}}
Trainingsstil: {{training_type_description}}
Progression: {{training_type_hints_on_progression}}
Zielgruppe: {{target_group_description}}
Stilrichtung: {{style_direction_description}}
Wichtig: Keine Gruppenanalyse nur didaktischer Pfad. Katalog-Hinweise beachten.
Antworte NUR mit JSON:
{
"primary_topic": "Mae Geri",
"start_assumption": "Welche Voraussetzungen werden für den Einstieg angenommen",
"target_state": "Konkreter Zielzustand der Progression",
"success_criteria": ["messbare Kriterien"],
"constraints": { "partner_required": false }
}$t$,
default_template = template
WHERE slug = 'planning_progression_goal_analysis';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und erstellst eine didaktische Roadmap für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Semantic Brief: {{semantic_brief_json}}
Anzahl Major Steps (N): {{max_steps}}
Katalog-Kontext für Stufenlogik:
Primärfokus:
{{focus_area_description}}
Roadmap: {{focus_area_hints_on_progression}}
Vermeiden: {{focus_area_anti_patterns}}
Trainingsstil:
{{training_type_description}}
Roadmap: {{training_type_hints_on_progression}}
Zielgruppe:
{{target_group_description}}
Roadmap: {{target_group_hints_on_progression}}
Stilrichtung:
{{style_direction_description}}
Roadmap: {{style_direction_hints_on_progression}}
{{catalog_context_json}}
Erzeuge zuerst 812 micro_objectives, dann konsolidiere auf genau N major_steps.
Phasen: einstieg, grundlage, vertiefung, anwendung, perfektion Katalog-Roadmap-Hinweise beachten.
Antworte NUR mit JSON:
{
"micro_objectives": [
{ "id": "m1", "phase": "grundlage", "title": "", "weight": 0.9, "depends_on": [] }
],
"major_steps": [
{ "index": 0, "phase": "grundlage", "learning_goal": "", "consolidates": ["m1","m2"], "rationale": "" }
],
"consolidation_notes": [""]
}$t$,
default_template = template
WHERE slug = 'planning_progression_roadmap';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und spezifizierst didaktische Stufen eines Progressionsgraphen.
Anfrage: {{goal_query}}
Zielanalyse: {{goal_analysis_json}}
Major Steps: {{major_steps_json}}
Intent-Kontext: {{intent_context_json}}
Semantic Brief: {{semantic_brief_json}}
Katalog-Kontext je Stufe:
Primärfokus Progression: {{focus_area_hints_on_progression}}
Primärfokus Vermeiden: {{focus_area_anti_patterns}}
Trainingsstil Progression: {{training_type_hints_on_progression}}
Trainingsstil Vermeiden: {{training_type_anti_patterns}}
Zielgruppe Progression: {{target_group_hints_on_progression}}
Zielgruppe Vermeiden: {{target_group_anti_patterns}}
Stilrichtung Progression: {{style_direction_hints_on_progression}}
{{catalog_context_json}}
Für jeden Major Step: messbares Lernziel, load_profile, exercise_type, success_criteria, anti_patterns Katalog-Slots beachten.
Antworte NUR mit JSON:
{
"stage_specs": [
{
"major_step_index": 0,
"learning_goal": "",
"load_profile": ["koordination", "gleichgewicht"],
"exercise_type": "kihon_einzel",
"success_criteria": [""],
"anti_patterns": [""]
}
]
}$t$,
default_template = template
WHERE slug = 'planning_progression_stage_spec';
UPDATE ai_prompts
SET template = $t$Du bist Assistent für Kampfsport-Trainer und extrahierst Start, Ziel und Ergänzungen für einen Progressionsgraphen.
Anfrage: {{goal_query}}
Semantic Brief: {{semantic_brief_json}}
Trainer-Notizen: {{user_notes}}
Katalog-Einordnung:
Primärfokus: {{focus_area_description}}
Trainingsstil: {{training_type_description}}
Zielgruppe: {{target_group_description}}
Antworte NUR mit JSON:
{
"primary_topic": "",
"start_situation": "",
"target_state": "",
"roadmap_notes": "",
"extraction_notes": ""
}$t$,
default_template = template
WHERE slug = 'planning_progression_start_target';

View File

@ -0,0 +1,167 @@
-- Migration 094: Vollständige Befüllung catalog_prompt_slots (H1-Inhalte + Defaults für alle Stammdaten)
CREATE TEMP TABLE IF NOT EXISTS _catalog_slot_seed (
catalog_kind VARCHAR(32) NOT NULL,
name_pattern TEXT NOT NULL,
slot_key VARCHAR(64) NOT NULL,
content TEXT NOT NULL
);
TRUNCATE _catalog_slot_seed;
-- Primärfokus Karate (häufigster Technik-Pfad)
INSERT INTO _catalog_slot_seed (catalog_kind, name_pattern, slot_key, content) VALUES
('focus_area', 'Karate', 'description',
'Technik-Curriculum im Karate-Kontext: aufeinander aufbauende Kihon-Progression mit klaren Qualitätsankern (Stand, Hüfte, Kime).'),
('focus_area', 'Karate', 'hints_on_progression',
'Typische Phasen: Einstieg → Grundlagen → Koordination/Kraft → Anwendung → optional Vertiefung; Grundlagen vor Perfektion.'),
('focus_area', 'Karate', 'hints_on_exercise',
'Kihon und Partnerübungen mit Technikbezug; reine Kraft-/Ausdauer-Inseln nur mit klarer Begründung.'),
('focus_area', 'Karate', 'hints_on_path_qa',
'Kohärente Progression Grundlagen → Anwendung → Vertiefung; Übergänge ohne Sprünge; themenfremde Kraft-/Ausdauer-Inseln abwerten.'),
('focus_area', 'Karate', 'anti_patterns',
'Keine pauschale Perfektions-Stufe verlangen, wenn der Trainingsstil Breitensport ist.');
-- Selbstverteidigung
INSERT INTO _catalog_slot_seed VALUES
('focus_area', 'Selbstverteidigung', 'description',
'Praktische Selbstverteidigung: realistische Szenarien, Sicherheit und anwendungsnahe Progression — nicht Show-Technik oder Wettkampf-Kata.'),
('focus_area', 'Selbstverteidigung', 'hints_on_progression',
'Von Wahrnehmung und Distanz zu einfachen Abwehrmustern und kontrollierter Anwendung.'),
('focus_area', 'Selbstverteidigung', 'hints_on_exercise',
'Partnerübungen mit klaren Sicherheitsregeln; Szenario-Bezug wichtiger als Stil-Show.'),
('focus_area', 'Selbstverteidigung', 'hints_on_path_qa',
'Lücken bei Szenario- oder Sicherheitsstufen sind relevant; fehlende Kick-Varianten oder Wettkampftiefe sind kein Mangel.'),
('focus_area', 'Selbstverteidigung', 'anti_patterns',
'Keine Wettkampf- oder Kata-Perfektion als QS-Maßstab.');
-- Gewaltschutz (ergänzt 092)
INSERT INTO _catalog_slot_seed VALUES
('focus_area', 'Gewaltschutz', 'hints_on_progression',
'Phasen: Wahrnehmung → Grenzen → Deeskalation → sichere Übungsformen; keine Kumite-Perfektionsstufen erzwingen.'),
('focus_area', 'Gewaltschutz', 'hints_on_exercise',
'Übungen mit Rollen, Kommunikation, Ausweichen; keine rein technischen Kick-Fokus-Inseln ohne Bezug.');
-- Fitness (falls vorhanden)
INSERT INTO _catalog_slot_seed VALUES
('focus_area', 'Fitness', 'description',
'Fitness- und Konditionsorientierung mit sicherer Belastungssteuerung; Technikbezug nur wo fachlich sinnvoll.'),
('focus_area', 'Fitness', 'hints_on_progression',
'Progression von niedriger zu moderater Belastung; klare Pausen und Technikhygiene.'),
('focus_area', 'Fitness', 'hints_on_path_qa',
'Keine Wettkampf-Spezialisierung als Pflicht-Kriterium; Belastungssteigerung ohne Technikbezug abwerten.'),
('focus_area', 'Fitness', 'anti_patterns',
'Keine Kumite-Perfektion oder Wettkampf-Kombinationen als QS-Maßstab verlangen.');
-- Trainingsstile (global)
INSERT INTO _catalog_slot_seed VALUES
('training_type', 'Breitensport', 'hints_on_progression',
'Moderater Schwierigkeitsanstieg; Perfektionsphasen optional.'),
('training_type', 'Breitensport', 'anti_patterns',
'Keine Leistungssport-Perfektion als Pflicht-Lücke.'),
('training_type', 'Leistungssport', 'description',
'Leistungsorientiertes Training mit höherer Anspruchskurve und Spezialisierung.'),
('training_type', 'Leistungssport', 'hints_on_progression',
'Belastungs- und Kombinationsprogressionen sind erwünscht.'),
('training_type', 'Leistungssport', 'hints_on_path_qa',
'Höhere Anspruchskurven sind passend; Lücken in Spezialisierung können echte Hinweise sein.'),
('training_type', 'Wettkampf', 'hints_on_progression',
'Anwendungs- und Druckphasen zeitig einplanen.');
-- Zielgruppen
INSERT INTO _catalog_slot_seed VALUES
('target_group', 'Breitensportler', 'description',
'Breitensport: Partizipation und Verständlichkeit vor maximaler Spezialisierung.'),
('target_group', 'Breitensportler', 'hints_on_path_qa',
'Moderate Progression; Perfektions-Lücken sind selten echte Mängel.'),
('target_group', 'Breitensportler', 'anti_patterns',
'Keine Leistungssport-Perfektion als Pflicht-Kriterium.'),
('target_group', 'Kinder', 'hints_on_progression',
'Spielerische Einstiege; kurze Abschnitte; Sicherheit vor Perfektion.'),
('target_group', 'Leistungssportler', 'hints_on_progression',
'Anspruchskurve und Spezialisierung dürfen steiler sein.');
-- Stilrichtungen (generisch + Shotokan-Details via 092)
INSERT INTO _catalog_slot_seed VALUES
('style_direction', 'Goju-Ryu', 'hints_on_progression',
'Stil-Nuancen (Stand, Atem, Kime) einbeziehen — kein Stilwechsel erzwingen.'),
('style_direction', 'Wado-Ryu', 'hints_on_progression',
'Stil-Nuancen (Stand, Atem, Kime) einbeziehen — kein Stilwechsel erzwingen.'),
('style_direction', 'Shito-Ryu', 'hints_on_progression',
'Stil-Nuancen (Stand, Atem, Kime) einbeziehen — kein Stilwechsel erzwingen.'),
('style_direction', 'Kyokushin', 'hints_on_progression',
'Stil-Nuancen (Stand, Belastung, Kime) einbeziehen — kein Stilwechsel erzwingen.');
-- Fokusbereiche: aus Seed-Tabelle
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT s.catalog_kind, fa.id, s.slot_key, s.content
FROM _catalog_slot_seed s
JOIN focus_areas fa ON fa.name ILIKE s.name_pattern
WHERE s.catalog_kind = 'focus_area'
ON CONFLICT (catalog_kind, catalog_id, slot_key)
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT s.catalog_kind, tt.id, s.slot_key, s.content
FROM _catalog_slot_seed s
JOIN training_types tt ON tt.name ILIKE s.name_pattern
WHERE s.catalog_kind = 'training_type'
ON CONFLICT (catalog_kind, catalog_id, slot_key)
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT s.catalog_kind, tg.id, s.slot_key, s.content
FROM _catalog_slot_seed s
JOIN target_groups tg ON tg.name ILIKE s.name_pattern
WHERE s.catalog_kind = 'target_group'
ON CONFLICT (catalog_kind, catalog_id, slot_key)
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT s.catalog_kind, sd.id, s.slot_key, s.content
FROM _catalog_slot_seed s
JOIN style_directions sd ON sd.name ILIKE s.name_pattern
WHERE s.catalog_kind = 'style_direction'
ON CONFLICT (catalog_kind, catalog_id, slot_key)
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW();
-- Default-Technik-Pack für Fokusbereiche ohne hints_on_path_qa (außer Gewaltschutz/Fitness)
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'focus_area', fa.id, 'hints_on_path_qa',
'Kohärente Progression zum Anfrage-Thema; Lücken sind fehlende Zwischenstufen im Lernpfad, nicht fehlende Nebenthemen.'
FROM focus_areas fa
WHERE fa.name NOT ILIKE 'Gewaltschutz'
AND fa.name NOT ILIKE 'Fitness'
AND NOT EXISTS (
SELECT 1 FROM catalog_prompt_slots cps
WHERE cps.catalog_kind = 'focus_area' AND cps.catalog_id = fa.id AND cps.slot_key = 'hints_on_path_qa'
AND TRIM(cps.content) <> ''
)
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO NOTHING;
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'focus_area', fa.id, 'hints_on_progression',
'Grundlagen vor Anwendung; moderate Sprünge zwischen Stufen vermeiden.'
FROM focus_areas fa
WHERE fa.name NOT ILIKE 'Gewaltschutz'
AND fa.name NOT ILIKE 'Fitness'
AND NOT EXISTS (
SELECT 1 FROM catalog_prompt_slots cps
WHERE cps.catalog_kind = 'focus_area' AND cps.catalog_id = fa.id AND cps.slot_key = 'hints_on_progression'
AND TRIM(cps.content) <> ''
)
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO NOTHING;
-- Stilrichtungen ohne Eintrag: generischer Progressions-Hinweis
INSERT INTO catalog_prompt_slots (catalog_kind, catalog_id, slot_key, content)
SELECT 'style_direction', sd.id, 'hints_on_progression',
'Stil-spezifische Nuancen (Stand, Hüfte, Rhythmus) einbeziehen — ohne Stilwechsel zu erzwingen.'
FROM style_directions sd
WHERE NOT EXISTS (
SELECT 1 FROM catalog_prompt_slots cps
WHERE cps.catalog_kind = 'style_direction' AND cps.catalog_id = sd.id AND cps.slot_key = 'hints_on_progression'
AND TRIM(cps.content) <> ''
)
ON CONFLICT (catalog_kind, catalog_id, slot_key) DO NOTHING;
DROP TABLE IF EXISTS _catalog_slot_seed;

View File

@ -117,6 +117,8 @@ class SkillCreate(BaseModel):
description: Optional[str] = None description: Optional[str] = None
importance: Optional[int] = Field(None, ge=1, le=5) importance: Optional[int] = Field(None, ge=1, le=5)
keywords: Optional[List[str]] = [] keywords: Optional[List[str]] = []
karate_relevance: Optional[str] = None
relevance_level: Optional[int] = Field(None, ge=1, le=3)
class SkillResponse(BaseModel): class SkillResponse(BaseModel):
id: int id: int
@ -125,6 +127,8 @@ class SkillResponse(BaseModel):
description: Optional[str] description: Optional[str]
importance: Optional[int] importance: Optional[int]
keywords: Optional[List[str]] keywords: Optional[List[str]]
karate_relevance: Optional[str] = None
relevance_level: Optional[int] = None
status: str status: str
created_at: datetime created_at: datetime

231
backend/openrouter_chat.py Normal file
View File

@ -0,0 +1,231 @@
"""
Minimal OpenRouter REST client (sync). Reads OPENROUTER_API_KEY / OPENROUTER_MODEL / OPENROUTER_BASE_URL from env.
"""
from __future__ import annotations
import json
import logging
import os
from typing import Any, Dict, List, Mapping, Optional
import httpx
_logger = logging.getLogger("shinkan.openrouter")
_SKIP_ANTHROPIC_BLOCK_TYPES = frozenset(
{
"thinking",
"redacted_thinking",
"reasoning",
"tool_use",
"tool_calls",
}
)
def _shinkan_ai_debug() -> bool:
return os.getenv("SHINKAN_AI_DEBUG", "").strip().lower() in ("1", "true", "yes", "full")
def _coerce_nested_text(val: Any) -> str:
if val is None:
return ""
if isinstance(val, str):
return val.strip()
if isinstance(val, bool) or isinstance(val, (int, float)):
return str(val).strip()
if isinstance(val, list):
return "".join(_coerce_nested_text(x) for x in val).strip()
if isinstance(val, dict):
# OpenRouter/Anthropic: verschachtelte text/content-Hüllen
for key in ("text", "content", "value"):
if key in val:
nested = _coerce_nested_text(val.get(key))
if nested:
return nested
return ""
return str(val).strip()
def _flatten_message_content(content: Any) -> str:
"""
Chat-Completion: `content` als String oder als Liste strukturierter Blöcke
(Anthropic Claude über OpenRouter/Bedrock, teils verschachtelt).
"""
if content is None:
return ""
if isinstance(content, str):
return content.strip()
if isinstance(content, list):
parts: List[str] = []
for block in content:
if isinstance(block, str):
bits = _coerce_nested_text(block)
if bits:
parts.append(bits)
elif isinstance(block, dict):
t_raw = block.get("type")
ts = str(t_raw or "").strip().lower()
if ts and (ts in _SKIP_ANTHROPIC_BLOCK_TYPES or ts.endswith("_thinking")):
continue
txt = None
if ts in ("text", "output_text", ""):
txt = block.get("text")
if txt is None:
txt = block.get("content")
else:
# unbekannten Typ weiter versuchen (Provider-Varianten), aber tool-use überspringen
low = ts
if "tool_use" in low or low.startswith("tool_"):
continue
txt = block.get("text") if block.get("text") is not None else block.get("content")
bits = _coerce_nested_text(txt)
if bits:
parts.append(bits)
return "".join(parts).strip()
if isinstance(content, dict):
return _coerce_nested_text(content)
return str(content).strip()
class OpenRouterError(Exception):
"""Upstream or transport failure."""
def openrouter_chat_completion(
*,
api_key: str,
model: str,
user_content: str,
system_content: Optional[str] = None,
timeout_sec: float = 120.0,
temperature: float = 0.25,
site_url: Optional[str] = None,
app_title: Optional[str] = None,
) -> str:
"""
Returns assistant message content (plain string). Caller validates empty responses.
"""
base = (os.getenv("OPENROUTER_BASE_URL") or "").strip().rstrip("/") or "https://openrouter.ai/api/v1"
url = f"{base}/chat/completions"
headers: Dict[str, str] = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
referer = (site_url or os.getenv("APP_URL") or "").strip()
if referer:
headers["HTTP-Referer"] = referer
title = (app_title or os.getenv("OPENROUTER_APP_TITLE") or "Shinkan Jinkendo").strip()
if title:
headers["X-Title"] = title
messages: List[Dict[str, str]] = []
if system_content and str(system_content).strip():
messages.append({"role": "system", "content": str(system_content).strip()})
messages.append({"role": "user", "content": user_content})
payload: Dict[str, Any] = {
"model": model,
"messages": messages,
"temperature": temperature,
}
try:
with httpx.Client(timeout=timeout_sec) as client:
resp = client.post(url, headers=headers, json=payload)
except httpx.RequestError as e:
raise OpenRouterError(str(e)) from e
if resp.status_code >= 400:
detail = ""
try:
j = resp.json()
detail = (
str(j.get("error", {}).get("message"))
if isinstance(j.get("error"), dict)
else str(j.get("message") or j)
)
except Exception:
detail = (resp.text or "")[:600]
raise OpenRouterError(f"HTTP {resp.status_code}: {detail}".strip())
try:
data = resp.json()
except json.JSONDecodeError as e:
raise OpenRouterError("Ungueltige JSON-Antwort von OpenRouter") from e
choices = data.get("choices") if isinstance(data, dict) else None
if not choices or not isinstance(choices, list):
raise OpenRouterError("OpenRouter: keine choices in Antwort")
msg0 = choices[0] if choices else {}
inner = msg0.get("message") if isinstance(msg0, dict) else None
blobs: List[Any] = []
if isinstance(inner, dict):
if inner.get("content") is not None:
blobs.append(inner.get("content"))
if inner.get("refusal") is not None:
blobs.append(inner.get("refusal"))
elif isinstance(inner, str):
blobs.append(inner)
if isinstance(msg0, dict) and msg0.get("content") is not None and msg0.get("content") not in blobs:
blobs.append(msg0.get("content"))
pieces = [_flatten_message_content(b).strip() for b in blobs if b is not None]
joined = ("\n".join(p for p in pieces if p)).strip()
if _shinkan_ai_debug():
fr = str(msg0.get("finish_reason") or "") if isinstance(msg0, dict) else ""
fu = data.get("usage") if isinstance(data.get("usage"), dict) else {}
pu = str(fu.get("prompt_tokens") or "")
pc = str(fu.get("completion_tokens") or "")
pt = str(fu.get("total_tokens") or "")
raw_cls = type(blobs[0]).__name__ if blobs else "none"
cc = str(len(joined))
_logger.warning(
"[AI_DEBUG/openrouter] model=%s finish_reason=%s usage_prompt=%s usage_completion=%s usage_total=%s "
"raw_content_cls=%s out_chars=%s",
model,
fr,
pu,
pc,
pt,
raw_cls,
cc,
)
try:
from planning_llm_usage import record_planning_llm_call
record_planning_llm_call(1)
except Exception:
pass
return joined
def normalize_openrouter_env() -> tuple[str, str]:
key = (os.getenv("OPENROUTER_API_KEY") or "").strip()
model = (os.getenv("OPENROUTER_MODEL") or "anthropic/claude-sonnet-4").strip()
return key, model
def default_openrouter_model_id() -> str:
"""Standard-Modell aus OPENROUTER_MODEL (ohne API-Key zu pruefen)."""
_, model = normalize_openrouter_env()
return model
def effective_openrouter_model_for_prompt_row(row: Optional[Mapping[str, Any]]) -> str:
"""
Pro-Prompt-Override in ai_prompts.openrouter_model, sonst Env-Default.
`row` kann eine partial Row aus load_ai_prompt_row sein (Felder slug, openrouter_model, ).
"""
if row:
custom = str(row.get("openrouter_model") or "").strip()
if custom:
return custom
return default_openrouter_model_id()

View File

@ -0,0 +1,147 @@
"""
Katalog-Kontext für Progressionsgraph-Planung Fokusbereich, Stil, Trainingsstil, Zielgruppe.
Explizite Trainer-Auswahl ergänzt Freitext/LLM; ersetzt kein Roadmap-Didaktik-Modell.
"""
from __future__ import annotations
from typing import Any, Dict, List, Mapping, Optional, Sequence
from pydantic import BaseModel, Field
from planning_exercise_profiles import PlanningTargetProfile, _normalize_weight_map
from planning_exercise_target_pipeline import (
SCENARIO_FREE_SEARCH,
merge_query_overlay_into_target,
)
from planning_exercise_text_signals import resolve_planning_text_to_catalog_weights
class PlanningCatalogContextItem(BaseModel):
id: int = Field(..., ge=1)
is_primary: bool = False
weight: float = Field(default=1.0, ge=0.1, le=1.0)
class ProgressionPlanningCatalogContext(BaseModel):
focus_areas: List[PlanningCatalogContextItem] = Field(default_factory=list)
style_directions: List[PlanningCatalogContextItem] = Field(default_factory=list)
training_types: List[PlanningCatalogContextItem] = Field(default_factory=list)
target_groups: List[PlanningCatalogContextItem] = Field(default_factory=list)
def catalog_context_has_items(catalog: Optional[ProgressionPlanningCatalogContext]) -> bool:
if catalog is None:
return False
return bool(
catalog.focus_areas
or catalog.style_directions
or catalog.training_types
or catalog.target_groups
)
def catalog_items_to_weight_map(
items: Sequence[PlanningCatalogContextItem],
*,
primary_weight: float = 0.95,
secondary_weight: float = 0.78,
) -> Dict[int, float]:
out: Dict[int, float] = {}
for item in items or []:
base = primary_weight if item.is_primary else secondary_weight
w = base * float(item.weight)
iid = int(item.id)
out[iid] = max(out.get(iid, 0.0), w)
return _normalize_weight_map(out) if out else out
def merge_catalog_context_into_target(
target: PlanningTargetProfile,
catalog: Optional[ProgressionPlanningCatalogContext],
*,
emphasis: str = "replace",
) -> PlanningTargetProfile:
"""Trainer-Katalog-Kontext ins Erwartungsprofil — beeinflusst Retrieval-Scoring."""
if not catalog_context_has_items(catalog):
return target
focus = catalog_items_to_weight_map(catalog.focus_areas)
style = catalog_items_to_weight_map(catalog.style_directions, primary_weight=0.9, secondary_weight=0.72)
tt = catalog_items_to_weight_map(catalog.training_types, primary_weight=0.9, secondary_weight=0.72)
tg = catalog_items_to_weight_map(catalog.target_groups, primary_weight=0.88, secondary_weight=0.7)
merged = merge_query_overlay_into_target(
target,
focus=focus,
style=style,
tt=tt,
tg=tg,
skills={},
emphasis=emphasis,
scenario=SCENARIO_FREE_SEARCH,
)
sources = list(merged.sources or [])
if "catalog_context" not in sources:
sources.append("catalog_context")
merged.sources = sources
return merged
def enrich_target_from_planning_text_blobs(
cur,
target: PlanningTargetProfile,
*text_blobs: Optional[str],
) -> PlanningTargetProfile:
"""Additive Katalog-Signale aus Freitext (Anfrage, Start/Ziel, Notizen)."""
combined = " ".join(str(t or "").strip() for t in text_blobs if (t or "").strip())
if len(combined) < 4:
return target
focus, style, tt, tg, skills = resolve_planning_text_to_catalog_weights(cur, combined)
if not (focus or style or tt or tg or skills):
return target
merged = merge_query_overlay_into_target(
target,
focus=focus,
style=style,
tt=tt,
tg=tg,
skills=skills,
emphasis="additive",
scenario=SCENARIO_FREE_SEARCH,
)
sources = list(merged.sources or [])
if "text_catalog_signals" not in sources:
sources.append("text_catalog_signals")
merged.sources = sources
return merged
def catalog_context_from_mapping(raw: Any) -> Optional[ProgressionPlanningCatalogContext]:
if not raw or not isinstance(raw, Mapping):
return None
try:
ctx = ProgressionPlanningCatalogContext.model_validate(dict(raw))
except Exception:
return None
return ctx if catalog_context_has_items(ctx) else None
def load_catalog_context_from_graph_row(
planning_roadmap: Any,
) -> Optional[ProgressionPlanningCatalogContext]:
if not isinstance(planning_roadmap, dict):
return None
return catalog_context_from_mapping(planning_roadmap.get("planning_catalog_context"))
__all__ = [
"PlanningCatalogContextItem",
"ProgressionPlanningCatalogContext",
"catalog_context_from_mapping",
"catalog_context_has_items",
"catalog_items_to_weight_map",
"enrich_target_from_planning_text_blobs",
"load_catalog_context_from_graph_row",
"merge_catalog_context_into_target",
]

View File

@ -0,0 +1,16 @@
"""
Katalog-Prompt-Snippets Abwärtskompatibilität (H1-Importpfade).
Implementierung: catalog_prompt_slots.py (H2).
"""
from catalog_prompt_slots import (
build_catalog_guidance_for_prompt,
get_rematch_guard_for_catalog,
pick_active_catalog_item,
)
__all__ = [
"build_catalog_guidance_for_prompt",
"get_rematch_guard_for_catalog",
"pick_active_catalog_item",
]

View File

@ -0,0 +1,69 @@
"""
Preset Nächste aus Kontext: LLM leitet Erwartungsprofil aus Planungskontext ab.
Prompt: planning_exercise_expectation_profile (Migration 074)
"""
from __future__ import annotations
import logging
from typing import Any, Dict, Mapping, Optional, Tuple
from planning_exercise_intent import (
PlanningQueryIntentParsed,
_compact_json,
_load_compact_catalog,
_load_skills_catalog_compact,
parse_planning_query_intent_response,
)
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
from openrouter_chat import (
effective_openrouter_model_for_prompt_row,
normalize_openrouter_env,
openrouter_chat_completion,
)
_logger = logging.getLogger("shinkan.planning_exercise_expectation")
def try_build_planning_expectation_from_context(
cur,
*,
heuristic_intent: str,
context_summary: Mapping[str, Any],
target_profile_summary: Mapping[str, Any],
) -> Tuple[Optional[PlanningQueryIntentParsed], bool]:
"""
LLM-Erwartungsprofil für preset_next / leere Anfrage mit Planungsbezug.
Returns (parsed overlay, applied).
"""
api_key, _ = normalize_openrouter_env()
if not api_key:
return None, False
variables = {
"heuristic_intent": heuristic_intent or "suggest_next",
"planning_context_json": _compact_json(dict(context_summary or {})),
"target_profile_json": _compact_json(dict(target_profile_summary or {})),
"skills_catalog_json": _compact_json(_load_skills_catalog_compact(cur)),
"focus_areas_catalog_json": _compact_json(_load_compact_catalog(cur, "focus_areas", "id")),
"training_types_catalog_json": _compact_json(_load_compact_catalog(cur, "training_types", "id")),
"style_directions_catalog_json": _compact_json(_load_compact_catalog(cur, "style_directions", "id")),
"target_groups_catalog_json": _compact_json(_load_compact_catalog(cur, "target_groups", "id")),
}
try:
prow, rendered = load_and_render_ai_prompt(cur, "planning_exercise_expectation_profile", variables)
model = effective_openrouter_model_for_prompt_row(prow)
raw = openrouter_chat_completion(api_key=api_key, model=model, user_content=rendered.text)
parsed = parse_planning_query_intent_response(raw)
if parsed.scenario not in ("preset_next", "continue_plan", "free_search"):
parsed = parsed.model_copy(update={"scenario": "preset_next"})
return parsed, True
except AiPromptUnavailableError:
return None, False
except Exception as exc:
_logger.warning("Planungs-Erwartungsprofil-LLM fehlgeschlagen: %s", exc)
return None, False
__all__ = ["try_build_planning_expectation_from_context"]

View File

@ -0,0 +1,395 @@
"""
Planungs-KI Phase D: strukturierter Planungskontext für POST /exercises/ai/suggest.
Wird als ``planning_context_json`` in Übungs-Prompts (summary, skills, instructions) injiziert.
"""
from __future__ import annotations
import json
from typing import Any, Dict, List, Mapping, Optional, Sequence
_MAX_JSON_CHARS = 6000
_MAX_STRING = 800
def compact_planning_context_json(obj: Any) -> str:
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def _trim_str(val: Any, *, limit: int = _MAX_STRING) -> Optional[str]:
if val is None:
return None
s = str(val).strip()
if not s:
return None
if len(s) > limit:
return s[: limit - 1] + ""
return s
def sanitize_planning_context_for_ai(ctx: Optional[Mapping[str, Any]]) -> Dict[str, Any]:
"""Reduziert Client-Payload auf prompt-taugliche, begrenzte Felder."""
if not ctx:
return {}
out: Dict[str, Any] = {}
for key, val in dict(ctx).items():
if val is None:
continue
k = str(key).strip()
if not k:
continue
if isinstance(val, str):
t = _trim_str(val)
if t:
out[k] = t
elif isinstance(val, (int, float, bool)):
out[k] = val
elif isinstance(val, list):
items = []
for item in val[:12]:
if isinstance(item, str):
t = _trim_str(item, limit=200)
if t:
items.append(t)
elif isinstance(item, (int, float, bool)):
items.append(item)
elif isinstance(item, dict):
sub = sanitize_planning_context_for_ai(item)
if sub:
items.append(sub)
if items:
out[k] = items
elif isinstance(val, dict):
sub = sanitize_planning_context_for_ai(val)
if sub:
out[k] = sub
raw = compact_planning_context_json(out)
if len(raw) > _MAX_JSON_CHARS:
out["truncated"] = True
out.pop("path_steps_preview", None)
raw = compact_planning_context_json(out)
if len(raw) > _MAX_JSON_CHARS:
return {"source": out.get("source"), "truncated": True, "goal_query": out.get("goal_query")}
return out
def planning_context_prompt_variables(
planning_context: Optional[Mapping[str, Any]],
) -> Dict[str, str]:
cleaned = sanitize_planning_context_for_ai(planning_context)
if not cleaned:
return {"planning_context_json": "-", "has_planning_context": ""}
return {
"planning_context_json": compact_planning_context_json(cleaned),
"has_planning_context": "true",
}
def _major_index_from_step(step: Mapping[str, Any]) -> Optional[int]:
for key in ("roadmap_major_step_index", "major_step_index"):
raw = step.get(key)
if raw is None:
continue
try:
return int(raw)
except (TypeError, ValueError):
continue
return None
def prior_path_steps_before_major(
steps: Sequence[Mapping[str, Any]],
major_idx: int,
) -> List[Dict[str, Any]]:
"""Pfadschritte mit kleinerem roadmap_major_step_index, sortiert."""
prior: List[Dict[str, Any]] = []
for step in steps:
mi = _major_index_from_step(step)
if mi is not None and mi < major_idx:
prior.append(dict(step))
prior.sort(key=lambda s: _major_index_from_step(s) or 0)
return prior
def _step_display_fields(step: Mapping[str, Any]) -> Dict[str, Any]:
title = _trim_str(
step.get("title") or step.get("exercise_title"),
limit=200,
)
learning_goal = _trim_str(
step.get("roadmap_learning_goal") or step.get("learning_goal"),
limit=500,
)
summary = _trim_str(step.get("summary"), limit=400)
start_state = _trim_str(step.get("roadmap_start_state") or step.get("start_state"))
target_state = _trim_str(step.get("roadmap_target_state") or step.get("target_state"))
phase = _trim_str(step.get("roadmap_phase") or step.get("phase"))
criteria_raw = step.get("stage_success_criteria") or step.get("success_criteria") or []
criteria = [
t
for x in criteria_raw
if (t := _trim_str(x, limit=200))
][:4]
out: Dict[str, Any] = {
"title": title,
"learning_goal": learning_goal,
"summary": summary,
"start_state": start_state,
"target_state": target_state,
"phase": phase,
"success_criteria": criteria or None,
"major_step_index": _major_index_from_step(step),
}
return {k: v for k, v in out.items() if v is not None and v != "" and v != []}
def build_progression_entry_state(
*,
major_step_index: Optional[int] = None,
prior_steps: Sequence[Mapping[str, Any]] = (),
start_situation: Optional[str] = None,
current_stage_start: Optional[str] = None,
) -> Dict[str, Any]:
"""
Eingangszustand für eine Roadmap-Stufe: erreichte Voraussetzungen aus Vorstufen.
"""
prior_compact = [_step_display_fields(s) for s in prior_steps]
prior_compact = [
p
for p in prior_compact
if any(p.get(k) for k in ("title", "learning_goal", "summary", "success_criteria"))
]
achievements: List[str] = []
detail_lines: List[str] = []
for p in prior_compact:
if p.get("success_criteria"):
achievements.extend(p["success_criteria"])
elif p.get("learning_goal"):
achievements.append(p["learning_goal"])
label_parts: List[str] = []
if p.get("major_step_index") is not None:
label_parts.append(f"Stufe {int(p['major_step_index']) + 1}")
if p.get("phase"):
label_parts.append(f"({p['phase']})")
if p.get("title"):
label_parts.append(f"{p['title']}\"")
prefix = " ".join(label_parts) if label_parts else "Vorstufe"
achieved = ""
if p.get("target_state"):
achieved = p["target_state"]
elif p.get("success_criteria"):
achieved = "; ".join(p["success_criteria"])
elif p.get("learning_goal"):
achieved = p["learning_goal"]
elif p.get("summary"):
achieved = p["summary"]
if achieved:
detail_lines.append(f"{prefix}: erreicht — {achieved}")
immediate_entry: Optional[str] = _trim_str(current_stage_start)
if not immediate_entry and prior_compact:
immediate = prior_compact[-1]
if immediate.get("target_state"):
immediate_entry = immediate["target_state"]
elif immediate.get("success_criteria"):
immediate_entry = "; ".join(immediate["success_criteria"])
elif immediate.get("learning_goal"):
immediate_entry = immediate["learning_goal"]
elif immediate.get("summary"):
immediate_entry = immediate["summary"]
elif not immediate_entry and start_situation:
immediate_entry = start_situation
entry_state = immediate_entry or start_situation
if prior_compact and start_situation and not immediate_entry:
detail_lines.insert(0, f"Ausgangsbasis Pfad: {start_situation}")
out: Dict[str, Any] = {}
if entry_state:
out["entry_state"] = _trim_str(entry_state, limit=1200)
if detail_lines:
out["entry_state_detail"] = _trim_str("\n".join(detail_lines), limit=2000)
if prior_compact:
out["prior_steps"] = prior_compact[:6]
if achievements:
out["prior_achievements"] = list(dict.fromkeys(achievements))[:8]
return out
def enrich_gap_snapshot_with_entry_state(
snapshot: Mapping[str, Any],
*,
steps: Sequence[Mapping[str, Any]],
major_step_index: Optional[int],
) -> Dict[str, Any]:
snap = dict(snapshot)
if major_step_index is None:
return snap
try:
mi = int(major_step_index)
except (TypeError, ValueError):
return snap
prior = prior_path_steps_before_major(steps, mi)
entry = build_progression_entry_state(
major_step_index=mi,
prior_steps=prior,
start_situation=snap.get("start_situation"),
current_stage_start=snap.get("stage_start_state"),
)
snap.update(entry)
return snap
def build_progression_gap_snapshot(
*,
goal_analysis: Optional[Mapping[str, Any]] = None,
resolved_structured: Optional[Mapping[str, Any]] = None,
stage_spec: Optional[Mapping[str, Any]] = None,
semantic_brief: Optional[Mapping[str, Any]] = None,
) -> Dict[str, Any]:
"""Kompakter Roadmap-Kontext für Lücken-Übungen (Start, Ziel, Stufe, Fähigkeiten-Hinweise)."""
ga = dict(goal_analysis or {})
rs = dict(resolved_structured or {})
spec = dict(stage_spec or {})
brief = dict(semantic_brief or {})
start = _trim_str(rs.get("start_situation") or ga.get("start_assumption"))
target = _trim_str(rs.get("target_state") or ga.get("target_state"))
notes = _trim_str(rs.get("roadmap_notes"))
topic = _trim_str(ga.get("primary_topic") or brief.get("primary_topic"))
skill_hints: List[str] = []
for item in (brief.get("must_phrases") or [])[:4]:
t = _trim_str(item, limit=120)
if t:
skill_hints.append(t)
arc = brief.get("development_arc")
if isinstance(arc, list) and arc:
skill_hints.append(f"Entwicklungsbogen: {''.join(str(x) for x in arc[:5])}")
success_path = [
_trim_str(x, limit=200)
for x in (ga.get("success_criteria") or [])
if _trim_str(x, limit=200)
][:4]
stage_success = [
_trim_str(x, limit=200)
for x in (spec.get("success_criteria") or [])
if _trim_str(x, limit=200)
][:4]
load_profile = [
_trim_str(x, limit=80)
for x in (spec.get("load_profile") or [])
if _trim_str(x, limit=80)
][:6]
anti_patterns = [
_trim_str(x, limit=200)
for x in (spec.get("anti_patterns") or [])
if _trim_str(x, limit=200)
][:3]
snap: Dict[str, Any] = {
"primary_topic": topic,
"start_situation": start,
"target_state": target,
"roadmap_notes": notes,
"stage_learning_goal": _trim_str(
spec.get("learning_goal"), limit=1200
),
"stage_start_state": _trim_str(spec.get("start_state")),
"stage_target_state": _trim_str(spec.get("target_state")),
"stage_phase": _trim_str(spec.get("phase")),
"stage_exercise_type": _trim_str(spec.get("exercise_type")),
"stage_load_profile": load_profile or None,
"stage_success_criteria": stage_success or None,
"stage_anti_patterns": anti_patterns or None,
"path_success_criteria": success_path or None,
"skill_hints": skill_hints or None,
}
return {k: v for k, v in snap.items() if v is not None and v != "" and v != []}
def build_progression_path_gap_planning_context(
*,
goal_query: str,
primary_topic: Optional[str] = None,
progression_graph_id: Optional[int] = None,
offer: Optional[Mapping[str, Any]] = None,
neighbor_before: Optional[Mapping[str, Any]] = None,
neighbor_after: Optional[Mapping[str, Any]] = None,
prior_path_steps: Optional[Sequence[Mapping[str, Any]]] = None,
path_step_count: int = 0,
major_step_count: Optional[int] = None,
roadmap_phase: Optional[str] = None,
roadmap_learning_goal: Optional[str] = None,
goal_analysis: Optional[Mapping[str, Any]] = None,
resolved_structured: Optional[Mapping[str, Any]] = None,
stage_spec: Optional[Mapping[str, Any]] = None,
semantic_brief: Optional[Mapping[str, Any]] = None,
stage_learning_goal_override: Optional[str] = None,
gap_trainer_supplements: Optional[str] = None,
) -> Dict[str, Any]:
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
offer = offer or {}
gap = offer.get("gap") if isinstance(offer.get("gap"), dict) else {}
major_idx = offer.get("roadmap_major_step_index")
if major_idx is None and isinstance(gap, dict):
major_idx = gap.get("roadmap_major_step_index")
ctx: Dict[str, Any] = {
"source": "progression_path_gap_fill",
"goal_query": _trim_str(goal_query, limit=2000),
"primary_topic": _trim_str(primary_topic),
"progression_graph_id": progression_graph_id,
"gap_source": _trim_str(offer.get("source")),
"gap_phase": _trim_str(offer.get("phase") or gap.get("expected_phase")),
"roadmap_major_step_index": major_idx,
"roadmap_phase": _trim_str(roadmap_phase or offer.get("phase")),
"roadmap_learning_goal": _trim_str(
roadmap_learning_goal or offer.get("title_hint") or gap.get("learning_goal"),
limit=1200,
),
"neighbor_before_title": _trim_str(
(neighbor_before or {}).get("title") or offer.get("from_title")
),
"neighbor_after_title": _trim_str(
(neighbor_after or {}).get("title") or offer.get("to_title")
),
"path_step_count": path_step_count,
"major_step_count": major_step_count,
}
snap = build_progression_gap_snapshot(
goal_analysis=goal_analysis,
resolved_structured=resolved_structured,
stage_spec=stage_spec,
semantic_brief=semantic_brief,
)
ctx.update(snap)
if major_idx is not None and prior_path_steps:
ctx.update(
build_progression_entry_state(
major_step_index=major_idx,
prior_steps=list(prior_path_steps),
start_situation=ctx.get("start_situation"),
)
)
if stage_learning_goal_override and stage_learning_goal_override.strip():
ctx["stage_learning_goal"] = _trim_str(stage_learning_goal_override, limit=1200)
ctx["roadmap_learning_goal"] = ctx["stage_learning_goal"]
if gap_trainer_supplements and gap_trainer_supplements.strip():
ctx["gap_trainer_supplements"] = _trim_str(gap_trainer_supplements, limit=2000)
return sanitize_planning_context_for_ai(ctx)
__all__ = [
"build_progression_entry_state",
"build_progression_gap_snapshot",
"build_progression_path_gap_planning_context",
"enrich_gap_snapshot_with_entry_state",
"prior_path_steps_before_major",
"compact_planning_context_json",
"planning_context_prompt_variables",
"sanitize_planning_context_for_ai",
]

View File

@ -0,0 +1,272 @@
"""
P1: LLM-Intent aus Planungs-Suchfrage strukturiertes Query-Overlay für PlanningTargetProfile.
Prompt: planning_exercise_search_intent (Migration 073)
"""
from __future__ import annotations
import json
import logging
import re
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple
from pydantic import BaseModel, Field, field_validator
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
from openrouter_chat import (
effective_openrouter_model_for_prompt_row,
normalize_openrouter_env,
openrouter_chat_completion,
)
_logger = logging.getLogger("shinkan.planning_exercise_intent")
VALID_PARSED_INTENTS = {
"suggest_next",
"progression_next",
"deepen_exercise",
"continue_plan_goal",
"free_search",
}
VALID_SCENARIOS = {
"preset_next",
"progression",
"deepen",
"continue_plan",
"additive_constraint",
"free_search",
}
VALID_EMPHASIS = {"additive", "replace", "neutral"}
class SkillHint(BaseModel):
name: str = Field(..., min_length=1, max_length=120)
weight: float = Field(default=1.0, ge=0.1, le=1.0)
class PlanningQueryIntentParsed(BaseModel):
intent: str = "free_search"
scenario: str = "free_search"
skill_hints: List[SkillHint] = Field(default_factory=list)
focus_hints: List[str] = Field(default_factory=list)
style_hints: List[str] = Field(default_factory=list)
training_type_hints: List[str] = Field(default_factory=list)
target_group_hints: List[str] = Field(default_factory=list)
requires_partner: Optional[bool] = None
emphasis: str = "additive"
rationale: Optional[str] = Field(default=None, max_length=400)
@field_validator("intent")
@classmethod
def _intent(cls, v: str) -> str:
s = (v or "").strip().lower()
return s if s in VALID_PARSED_INTENTS else "free_search"
@field_validator("scenario")
@classmethod
def _scenario(cls, v: str) -> str:
s = (v or "").strip().lower()
return s if s in VALID_SCENARIOS else "free_search"
@field_validator("emphasis")
@classmethod
def _emphasis(cls, v: str) -> str:
s = (v or "").strip().lower()
return s if s in VALID_EMPHASIS else "additive"
@field_validator("focus_hints", "style_hints", "training_type_hints", "target_group_hints", mode="before")
@classmethod
def _str_list(cls, v: Any) -> List[str]:
if not v:
return []
if isinstance(v, str):
return [v.strip()] if v.strip() else []
out: List[str] = []
for item in v:
s = str(item or "").strip()
if s and s not in out:
out.append(s[:120])
return out[:8]
def _extract_json_object(text: str) -> Dict[str, Any]:
s = (text or "").strip()
if s.startswith("```"):
s = re.sub(r"^```[a-zA-Z0-9]*\s*", "", s)
if s.endswith("```"):
s = s[:-3].strip()
start = s.find("{")
end = s.rfind("}")
if start < 0 or end <= start:
raise ValueError("Kein JSON-Objekt in LLM-Antwort")
obj = json.loads(s[start : end + 1])
if not isinstance(obj, dict):
raise ValueError("LLM-Antwort ist kein JSON-Objekt")
return obj
def parse_planning_query_intent_response(text: str) -> PlanningQueryIntentParsed:
obj = _extract_json_object(text)
return PlanningQueryIntentParsed.model_validate(obj)
def _compact_json(obj: Any) -> str:
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def _load_compact_catalog(cur, table: str, id_col: str, name_col: str = "name", limit: int = 80) -> List[Dict[str, Any]]:
cur.execute(
f"""
SELECT {id_col} AS id, {name_col} AS name
FROM {table}
ORDER BY {name_col} ASC NULLS LAST
LIMIT %s
""",
(limit,),
)
return [{"id": int(r["id"]), "name": str(r["name"] or "")[:80]} for r in cur.fetchall()]
def _load_skills_catalog_compact(cur, limit: int = 120) -> List[Dict[str, Any]]:
cur.execute(
"""
SELECT id, name, category
FROM skills
WHERE status IS NULL OR status = 'active'
ORDER BY name ASC
LIMIT %s
""",
(limit,),
)
return [
{
"id": int(r["id"]),
"name": str(r["name"] or "")[:80],
"category": str(r.get("category") or "")[:40],
}
for r in cur.fetchall()
]
def _resolve_name_hint(cur, table: str, hint: str, *, extra_where: str = "") -> Optional[int]:
h = (hint or "").strip()
if len(h) < 2:
return None
q = h.lower()
cur.execute(
f"""
SELECT id, name
FROM {table}
WHERE LOWER(name) LIKE %s {extra_where}
ORDER BY CASE WHEN LOWER(name) = %s THEN 0 WHEN LOWER(name) LIKE %s THEN 1 ELSE 2 END,
LENGTH(name) ASC
LIMIT 1
""",
(f"%{q}%", q, f"{q}%"),
)
row = cur.fetchone()
return int(row["id"]) if row else None
def resolve_query_intent_catalog_ids(
cur,
parsed: PlanningQueryIntentParsed,
) -> Tuple[Dict[int, float], Dict[int, float], Dict[int, float], Dict[int, float], Dict[int, float], List[Dict[str, Any]]]:
"""
Mappt Text-Hints auf Katalog-IDs. Returns (focus, style, tt, tg, skills, resolved_skills_meta).
"""
focus: Dict[int, float] = {}
style: Dict[int, float] = {}
tt: Dict[int, float] = {}
tg: Dict[int, float] = {}
skills: Dict[int, float] = {}
resolved_skills: List[Dict[str, Any]] = []
for hint in parsed.focus_hints:
fid = _resolve_name_hint(cur, "focus_areas", hint)
if fid:
focus[fid] = max(focus.get(fid, 0.0), 0.9)
for hint in parsed.style_hints:
sid = _resolve_name_hint(cur, "style_directions", hint)
if sid:
style[sid] = max(style.get(sid, 0.0), 0.85)
for hint in parsed.training_type_hints:
tid = _resolve_name_hint(cur, "training_types", hint)
if tid:
tt[tid] = max(tt.get(tid, 0.0), 0.85)
for hint in parsed.target_group_hints:
gid = _resolve_name_hint(cur, "target_groups", hint)
if gid:
tg[gid] = max(tg.get(gid, 0.0), 0.85)
for sh in parsed.skill_hints[:8]:
cur.execute(
"""
SELECT id, name FROM skills
WHERE (status IS NULL OR status = 'active')
AND LOWER(name) LIKE %s
ORDER BY CASE WHEN LOWER(name) = %s THEN 0 WHEN LOWER(name) LIKE %s THEN 1 ELSE 2 END,
LENGTH(name) ASC
LIMIT 1
""",
(f"%{sh.name.lower()}%", sh.name.lower(), f"{sh.name.lower()}%"),
)
row = cur.fetchone()
if row:
sid = int(row["id"])
skills[sid] = max(skills.get(sid, 0.0), float(sh.weight))
resolved_skills.append({"skill_id": sid, "name": str(row["name"] or sh.name), "weight": skills[sid]})
return focus, style, tt, tg, skills, resolved_skills
def try_parse_planning_query_intent(
cur,
*,
query: str,
heuristic_intent: str,
scenario_hint: str,
context_summary: Mapping[str, Any],
target_profile_summary: Mapping[str, Any],
) -> Tuple[Optional[PlanningQueryIntentParsed], bool]:
api_key, _ = normalize_openrouter_env()
if not api_key or not (query or "").strip():
return None, False
variables = {
"search_query": (query or "").strip(),
"heuristic_intent": heuristic_intent or "",
"scenario_hint": scenario_hint or "",
"planning_context_json": _compact_json(dict(context_summary or {})),
"target_profile_json": _compact_json(dict(target_profile_summary or {})),
"skills_catalog_json": _compact_json(_load_skills_catalog_compact(cur)),
"focus_areas_catalog_json": _compact_json(_load_compact_catalog(cur, "focus_areas", "id")),
"training_types_catalog_json": _compact_json(_load_compact_catalog(cur, "training_types", "id")),
"style_directions_catalog_json": _compact_json(_load_compact_catalog(cur, "style_directions", "id")),
"target_groups_catalog_json": _compact_json(_load_compact_catalog(cur, "target_groups", "id")),
}
try:
prow, rendered = load_and_render_ai_prompt(cur, "planning_exercise_search_intent", variables)
model = effective_openrouter_model_for_prompt_row(prow)
raw = openrouter_chat_completion(api_key=api_key, model=model, user_content=rendered.text)
parsed = parse_planning_query_intent_response(raw)
return parsed, True
except AiPromptUnavailableError:
return None, False
except Exception as exc:
_logger.warning("Planungs-Intent-LLM fehlgeschlagen: %s", exc)
return None, False
__all__ = [
"PlanningQueryIntentParsed",
"parse_planning_query_intent_response",
"resolve_query_intent_catalog_ids",
"try_parse_planning_query_intent",
]

View File

@ -0,0 +1,223 @@
"""
Phase 2 Planungs-Übungssuche: LLM-Rerank über Hybrid-Kandidaten.
Prompt-Slug: planning_exercise_search_rank (Migration 072)
"""
from __future__ import annotations
import json
import logging
import re
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple
from ai_prompt_runtime import AiPromptUnavailableError, load_and_render_ai_prompt
from exercise_ai import strip_html_to_plain
from openrouter_chat import (
effective_openrouter_model_for_prompt_row,
normalize_openrouter_env,
openrouter_chat_completion,
)
_logger = logging.getLogger("shinkan.planning_exercise_llm_rank")
_LLM_RERANK_POOL = 32
_MAX_GOAL_PLAIN = 480
_MAX_SUMMARY_PLAIN = 320
_MAX_REASON_LEN = 160
def _compact_json(obj: Any) -> str:
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
def _extract_json_object(text: str) -> Dict[str, Any]:
s = (text or "").strip()
if s.startswith("```"):
s = re.sub(r"^```[a-zA-Z0-9]*\s*", "", s)
if s.endswith("```"):
s = s[:-3].strip()
start = s.find("{")
end = s.rfind("}")
if start < 0 or end <= start:
raise ValueError("Kein JSON-Objekt in LLM-Antwort")
obj = json.loads(s[start : end + 1])
if not isinstance(obj, dict):
raise ValueError("LLM-Antwort ist kein JSON-Objekt")
return obj
def parse_planning_exercise_rank_response(
text: str,
allowed_ids: Set[int],
) -> Tuple[List[int], Dict[int, str]]:
"""
Validiert LLM-Ranking: nur erlaubte exercise_id, dedupliziert, Reihenfolge beibehalten.
"""
obj = _extract_json_object(text)
ranked_raw = obj.get("ranked_ids") or obj.get("ranked") or obj.get("ids")
if not isinstance(ranked_raw, list):
raise ValueError("ranked_ids fehlt oder ist keine Liste")
ranked: List[int] = []
seen: Set[int] = set()
for raw in ranked_raw:
try:
eid = int(raw)
except (TypeError, ValueError):
continue
if eid < 1 or eid not in allowed_ids or eid in seen:
continue
seen.add(eid)
ranked.append(eid)
reasons_out: Dict[int, str] = {}
reasons_raw = obj.get("reasons") or obj.get("reasons_by_id") or {}
if isinstance(reasons_raw, dict):
for k, v in reasons_raw.items():
try:
eid = int(k)
except (TypeError, ValueError):
continue
if eid not in allowed_ids:
continue
txt = str(v or "").strip()
if txt:
reasons_out[eid] = txt[:_MAX_REASON_LEN]
return ranked, reasons_out
def _build_candidate_payload(
hit: Mapping[str, Any],
*,
goal_plain: str,
skill_names: Sequence[str],
) -> Dict[str, Any]:
return {
"id": int(hit["id"]),
"title": str(hit.get("title") or "").strip()[:200],
"summary": strip_html_to_plain(hit.get("summary"), max_len=_MAX_SUMMARY_PLAIN),
"goal": goal_plain,
"skills": list(skill_names)[:8],
"retrieval_score": float(hit.get("score") or 0.0),
}
def _load_exercise_goals(cur, exercise_ids: Sequence[int]) -> Dict[int, str]:
ids = [int(x) for x in exercise_ids if int(x) > 0]
if not ids:
return {}
ph = ",".join(["%s"] * len(ids))
cur.execute(
f"SELECT id, goal FROM exercises WHERE id IN ({ph})",
ids,
)
return {int(r["id"]): str(r.get("goal") or "") for r in cur.fetchall()}
def _load_skill_names(cur, skill_ids: Sequence[int]) -> Dict[int, str]:
ids = sorted({int(x) for x in skill_ids if int(x) > 0})
if not ids:
return {}
ph = ",".join(["%s"] * len(ids))
cur.execute(f"SELECT id, name FROM skills WHERE id IN ({ph})", ids)
return {int(r["id"]): str(r.get("name") or "") for r in cur.fetchall()}
def try_llm_rerank_planning_hits(
cur,
*,
hits: List[Dict[str, Any]],
skills_by_ex: Mapping[int, Set[int]],
query: str,
intent: str,
context_summary: Mapping[str, Any],
target_profile_summary: Mapping[str, Any],
limit: int,
) -> Tuple[List[Dict[str, Any]], bool]:
"""
Optionaler LLM-Rerank der Top-Kandidaten. Bei Fehler: Original-Reihenfolge, llm_applied=False.
"""
if not hits:
return hits, False
api_key, _ = normalize_openrouter_env()
if not api_key:
return hits, False
pool = hits[:_LLM_RERANK_POOL]
allowed_ids = {int(h["id"]) for h in pool}
goals = _load_exercise_goals(cur, list(allowed_ids))
all_skill_ids: Set[int] = set()
for eid in allowed_ids:
all_skill_ids.update(skills_by_ex.get(eid) or set())
skill_name_map = _load_skill_names(cur, list(all_skill_ids))
candidates: List[Dict[str, Any]] = []
for hit in pool:
eid = int(hit["id"])
sk_ids = sorted(skills_by_ex.get(eid) or set())
sk_names = [skill_name_map.get(sid, f"#{sid}") for sid in sk_ids[:8]]
goal_plain = strip_html_to_plain(goals.get(eid), max_len=_MAX_GOAL_PLAIN)
candidates.append(
_build_candidate_payload(hit, goal_plain=goal_plain, skill_names=sk_names)
)
variables = {
"search_query": query or "",
"intent": intent or "",
"planning_context_json": _compact_json(dict(context_summary or {})),
"target_profile_json": _compact_json(dict(target_profile_summary or {})),
"candidates_json": _compact_json(candidates),
"result_limit": str(max(1, min(int(limit), 50))),
}
try:
prow, rendered = load_and_render_ai_prompt(cur, "planning_exercise_search_rank", variables)
model = effective_openrouter_model_for_prompt_row(prow)
raw = openrouter_chat_completion(
api_key=api_key,
model=model,
user_content=rendered.text,
)
ranked_ids, llm_reasons = parse_planning_exercise_rank_response(raw, allowed_ids)
except AiPromptUnavailableError:
return hits, False
except Exception as exc:
_logger.warning("Planungs-LLM-Rerank fehlgeschlagen: %s", exc)
return hits, False
if not ranked_ids:
return hits, False
hit_by_id = {int(h["id"]): h for h in hits}
reranked: List[Dict[str, Any]] = []
used: Set[int] = set()
for eid in ranked_ids:
hit = hit_by_id.get(eid)
if not hit:
continue
used.add(eid)
new_hit = dict(hit)
reasons = list(hit.get("reasons") or [])
llm_reason = llm_reasons.get(eid)
if llm_reason and llm_reason not in reasons:
reasons.insert(0, llm_reason)
new_hit["reasons"] = reasons
new_hit["llm_rank"] = len(reranked) + 1
reranked.append(new_hit)
for hit in hits:
eid = int(hit["id"])
if eid in used:
continue
reranked.append(dict(hit))
return reranked[: max(int(limit), len(reranked))], True
__all__ = [
"parse_planning_exercise_rank_response",
"try_llm_rerank_planning_hits",
]

View File

@ -0,0 +1,788 @@
"""
Planungs-KI Phase E2/E3: KI-Neuanlage für Pfad-Lücken + strukturierte Angebote für die UI.
"""
from __future__ import annotations
import logging
import uuid
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
from ai_prompt_context import ExerciseFormAiPromptContext
from ai_prompt_job import run_exercise_form_ai_suggestion
from exercise_ai import strip_html_to_plain
from planning_exercise_path_qa import find_step_pair_index
from planning_exercise_form_context import (
build_progression_entry_state,
build_progression_gap_snapshot,
enrich_gap_snapshot_with_entry_state,
prior_path_steps_before_major,
)
from planning_exercise_semantics import PlanningSemanticBrief, brief_to_summary_dict
_logger = logging.getLogger("shinkan.planning_exercise_path_ai_fill")
def _resolve_neighbor_steps_by_major_index(
steps: Sequence[Mapping[str, Any]],
major_idx: int,
) -> Tuple[Optional[Mapping[str, Any]], Optional[Mapping[str, Any]]]:
"""Nachbarn im Pfad anhand roadmap_major_step_index (nicht Array-Position)."""
step_before: Optional[Mapping[str, Any]] = None
step_after: Optional[Mapping[str, Any]] = None
for step in steps:
raw = step.get("roadmap_major_step_index")
if raw is None:
continue
try:
mi = int(raw)
except (TypeError, ValueError):
continue
if mi < major_idx:
step_before = step
elif mi > major_idx and step_after is None:
step_after = step
return step_before, step_after
def _build_stage_ai_context(
*,
goal_query: str,
brief: PlanningSemanticBrief,
spec: Mapping[str, Any],
step_before: Optional[Mapping[str, Any]] = None,
step_after: Optional[Mapping[str, Any]] = None,
prior_steps: Optional[Sequence[Mapping[str, Any]]] = None,
start_situation: Optional[str] = None,
) -> ExerciseFormAiPromptContext:
"""KI-Kontext für unbesetzte Roadmap-Stufe (keine Brücke zwischen falschen Array-Indizes)."""
gap = dict(spec.get("gap") or {})
phase = spec.get("phase") or gap.get("expected_phase") or "vertiefung"
topic = (brief.primary_topic or "Technik").strip()
learning_goal = (
gap.get("learning_goal")
or spec.get("title_hint")
or spec.get("sketch")
or ""
).strip()
title = (spec.get("title_hint") or f"{topic}{phase}").strip()[:280]
major_idx = spec.get("roadmap_major_step_index")
entry: Dict[str, Any] = {}
if prior_steps is not None and major_idx is not None:
entry = build_progression_entry_state(
major_step_index=major_idx,
prior_steps=prior_steps,
start_situation=start_situation,
)
goal_parts = [
f"Planungsziel: {goal_query}",
f"Roadmap-Stufe ({phase}): {learning_goal}",
"Erstelle eine Übung, die dieses Stufen-Lernziel erfüllt — keine generische Brücken-Übung.",
]
if entry.get("entry_state"):
goal_parts.append(
f"Eingangszustand (erreichte Voraussetzungen): {entry['entry_state']}"
)
if entry.get("entry_state_detail") and entry.get("entry_state_detail") != entry.get("entry_state"):
goal_parts.append(f"Bisheriger Pfad:\n{entry['entry_state_detail']}")
if step_before:
goal_parts.append(
f"Vorherige Stufe im Pfad: „{(step_before.get('title') or '').strip()}"
)
if step_after:
goal_parts.append(
f"Nächste Stufe im Pfad: „{(step_after.get('title') or '').strip()}"
)
sketch = (spec.get("sketch") or "").strip()
if sketch and sketch != learning_goal:
goal_parts.extend(["", f"Kontext: {sketch}"])
goal = "\n".join(goal_parts)
focus_hint = topic if brief.topic_type == "technique" else None
if brief.must_phrases:
focus_hint = ", ".join(brief.must_phrases[:2])
return ExerciseFormAiPromptContext(
title=title[:280],
goal=goal[:8000],
execution=None,
focus_hint=focus_hint,
)
def try_suggest_ai_stage_step(
cur,
*,
goal_query: str,
brief: PlanningSemanticBrief,
spec: Mapping[str, Any],
steps: Sequence[Mapping[str, Any]],
) -> Optional[Dict[str, Any]]:
"""KI-Vorschlag für leere Roadmap-Stufe."""
major_idx = spec.get("roadmap_major_step_index")
if major_idx is None:
return None
try:
mi = int(major_idx)
except (TypeError, ValueError):
return None
step_before, step_after = _resolve_neighbor_steps_by_major_index(steps, mi)
prior_steps = prior_path_steps_before_major(steps, mi)
gap = dict(spec.get("gap") or {})
if not gap.get("expected_phase"):
gap["expected_phase"] = spec.get("phase") or "vertiefung"
gap["roadmap_major_step_index"] = mi
if not gap.get("learning_goal"):
gap["learning_goal"] = spec.get("title_hint") or spec.get("sketch")
ctx = _build_stage_ai_context(
goal_query=goal_query,
brief=brief,
spec=spec,
step_before=step_before,
step_after=step_after,
prior_steps=prior_steps,
)
try:
ai_payload = run_exercise_form_ai_suggestion(cur, ctx=ctx)
except Exception:
_logger.exception("roadmap_unfilled AI suggest failed")
return None
if not ai_payload:
return None
summary_text = ""
summary_obj = ai_payload.get("summary")
if isinstance(summary_obj, dict):
summary_text = str(summary_obj.get("text") or "").strip()
elif isinstance(summary_obj, str):
summary_text = summary_obj.strip()
proposal_key = f"ai-{uuid.uuid4().hex[:10]}"
title = (ctx.title or spec.get("title_hint") or "KI-Vorschlag").strip()
return {
"exercise_id": None,
"proposal_key": proposal_key,
"variant_id": None,
"title": title,
"summary": summary_text or None,
"score": None,
"semantic_score": None,
"reasons": ["KI-Neuanlage für Roadmap-Stufe ohne Bibliothekstreffer"],
"variants": [],
"is_bridge": False,
"is_ai_proposal": True,
"ai_suggestion": dict(ai_payload),
"roadmap_major_step_index": mi,
"roadmap_phase": gap.get("expected_phase"),
"roadmap_learning_goal": gap.get("learning_goal"),
}
def _build_gap_ai_context(
*,
goal_query: str,
brief: PlanningSemanticBrief,
step_a: Mapping[str, Any],
step_b: Mapping[str, Any],
gap: Mapping[str, Any],
title_hint: Optional[str] = None,
sketch_hint: Optional[str] = None,
) -> ExerciseFormAiPromptContext:
topic = (brief.primary_topic or "Technik").strip()
phase = gap.get("expected_phase") or "vertiefung"
from_title = (step_a.get("title") or f"Übung #{step_a.get('exercise_id')}").strip()
to_title = (step_b.get("title") or f"Übung #{step_b.get('exercise_id')}").strip()
title = (title_hint or f"Brücke {topic} ({phase})").strip()[:280]
sketch = (sketch_hint or "").strip()
goal_parts = [
f"Planungsziel: {goal_query}",
"",
f"Didaktische Brücken-Übung zwischen „{from_title}“ und „{to_title}“.",
f"Phase: {phase}. Thema: {topic}.",
"Die Übung schließt die Lücke im Progressionspfad und bereitet sinnvoll auf den nächsten Schritt vor.",
]
if sketch:
goal_parts.extend(["", f"Hinweis: {sketch}"])
goal = "\n".join(goal_parts)
focus_hint = topic if brief.topic_type == "technique" else None
if brief.must_phrases:
focus_hint = ", ".join(brief.must_phrases[:2])
return ExerciseFormAiPromptContext(
title=title[:280],
goal=goal[:8000],
execution=None,
focus_hint=focus_hint,
)
def ai_proposal_to_path_step(
*,
ai_payload: Mapping[str, Any],
ctx_title: str,
gap: Mapping[str, Any],
step_a: Mapping[str, Any],
step_b: Mapping[str, Any],
) -> Dict[str, Any]:
summary_text = ""
summary_obj = ai_payload.get("summary")
if isinstance(summary_obj, dict):
summary_text = str(summary_obj.get("text") or "").strip()
elif isinstance(summary_obj, str):
summary_text = summary_obj.strip()
proposal_key = f"ai-{uuid.uuid4().hex[:10]}"
title = (ctx_title or "").strip() or "KI-Vorschlag (Brücke)"
reasons = ["KI-Neuanlage-Vorschlag — Lücke ohne passende Bibliotheks-Übung"]
return {
"exercise_id": None,
"proposal_key": proposal_key,
"variant_id": None,
"title": title,
"summary": summary_text or None,
"score": None,
"semantic_score": None,
"reasons": reasons,
"variants": [],
"is_bridge": True,
"is_ai_proposal": True,
"ai_suggestion": dict(ai_payload),
"bridge_for_gap": {
"from_exercise_id": step_a.get("exercise_id"),
"to_exercise_id": step_b.get("exercise_id"),
"gap_score": gap.get("gap_score"),
"expected_phase": gap.get("expected_phase"),
},
}
def try_suggest_ai_bridge_step(
cur,
*,
goal_query: str,
brief: PlanningSemanticBrief,
step_a: Mapping[str, Any],
step_b: Mapping[str, Any],
gap: Mapping[str, Any],
title_hint: Optional[str] = None,
sketch_hint: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""Ruft exercise AI suggest auf — kein Speichern in DB."""
ctx = _build_gap_ai_context(
goal_query=goal_query,
brief=brief,
step_a=step_a,
step_b=step_b,
gap=gap,
title_hint=title_hint,
sketch_hint=sketch_hint,
)
g_plain = strip_html_to_plain(ctx.goal)
if not g_plain.strip() and not (ctx.title or "").strip():
return None
try:
payload = run_exercise_form_ai_suggestion(
cur,
ctx,
want_summary=True,
want_skills=True,
want_instructions=False,
)
except Exception as exc:
_logger.warning("KI-Lückenfüller fehlgeschlagen: %s", exc)
return None
if not payload:
return None
return ai_proposal_to_path_step(
ai_payload=payload,
ctx_title=ctx.title or "",
gap=gap,
step_a=step_a,
step_b=step_b,
)
def _default_sketch(
*,
goal_query: str,
brief: PlanningSemanticBrief,
step_a: Optional[Mapping[str, Any]],
step_b: Optional[Mapping[str, Any]],
phase: str,
rationale: str = "",
) -> str:
topic = (brief.primary_topic or "Technik").strip()
from_t = (step_a or {}).get("title") or "vorherigem Schritt"
to_t = (step_b or {}).get("title") or "nächstem Schritt"
parts = [
f"Planungsziel: {goal_query}",
f"Zwischenschritt für {topic} ({phase}) zwischen „{from_t}“ und „{to_t}“.",
]
if rationale:
parts.append(rationale)
return " ".join(parts)[:1200]
def _spec_dedupe_key(spec: Mapping[str, Any]) -> Tuple[Any, ...]:
return (
spec.get("source"),
int(spec.get("insert_after_index") or 0),
str(spec.get("title_hint") or "")[:48],
)
def _step_neighbors_at_index(
steps: Sequence[Mapping[str, Any]],
idx: int,
) -> Tuple[Optional[Mapping[str, Any]], Optional[Mapping[str, Any]]]:
"""Vorheriger/nächster Pfadschritt ohne IndexError (Rand-Slots, leere Stufen)."""
if idx < 0 or idx >= len(steps):
return None, None
step_a = steps[idx - 1] if idx > 0 else None
step_b = steps[idx + 1] if idx + 1 < len(steps) else None
return step_a, step_b
def collect_gap_fill_specs(
*,
steps: Sequence[Mapping[str, Any]],
unfilled_gaps: Sequence[Mapping[str, Any]],
off_topic_steps: Sequence[Mapping[str, Any]],
llm_specs: Sequence[Mapping[str, Any]],
brief: PlanningSemanticBrief,
goal_query: str,
) -> List[Dict[str, Any]]:
"""Sammelt alle Lücken, für die ein KI-Anlege-Angebot sinnvoll ist."""
topic = (brief.primary_topic or "Technik").strip()
specs: List[Dict[str, Any]] = []
seen: set = set()
def add(spec: Dict[str, Any]) -> None:
key = _spec_dedupe_key(spec)
if key in seen:
return
seen.add(key)
specs.append(spec)
for gap in unfilled_gaps:
idx = find_step_pair_index(
steps,
int(gap["from_exercise_id"]),
int(gap["to_exercise_id"]),
)
if idx is None or idx + 1 >= len(steps):
continue
step_a = steps[idx]
step_b = steps[idx + 1]
phase = gap.get("expected_phase") or "vertiefung"
add(
{
"source": "unfilled_gap",
"insert_after_index": idx,
"gap": dict(gap),
"phase": phase,
"title_hint": f"{topic}{phase}",
"sketch": _default_sketch(
goal_query=goal_query,
brief=brief,
step_a=step_a,
step_b=step_b,
phase=str(phase),
rationale="Bibliothek enthält keine passende Brücke.",
),
"rationale": "Lücke zwischen benachbaren Schritten — keine passende Bibliotheks-Übung.",
}
)
for ot in off_topic_steps:
major_idx = ot.get("roadmap_major_step_index")
idx: Optional[int] = None
if major_idx is not None:
try:
mi = int(major_idx)
except (TypeError, ValueError):
mi = None
if mi is not None:
idx = next(
(
i
for i, s in enumerate(steps)
if s.get("roadmap_major_step_index") is not None
and int(s["roadmap_major_step_index"]) == mi
),
None,
)
if idx is None:
idx = int(ot.get("step_index") or 0)
if idx < 0 or idx >= len(steps):
continue
step_a, step_b = _step_neighbors_at_index(steps, idx)
phase = ot.get("expected_phase") or "vertiefung"
insert_after = max(idx - 1, -1)
stage_goal = str(ot.get("roadmap_learning_goal") or "").strip()
if str(ot.get("issue") or "") == "stage_mismatch" and stage_goal:
title_hint = stage_goal[:120]
rationale = (
f"Keine passende Bibliotheks-Übung für Stufen-Lernziel „{stage_goal[:100]}“."
)
sketch_rationale = (
f"Slot braucht Übung passend zu: {stage_goal[:200]}"
)
else:
title_hint = f"{topic}{phase} (Ersatz für themenfremden Schritt)"
rationale = f"Schritt „{ot.get('title')}“ passt nicht zum Pfad-Thema."
sketch_rationale = f"Ersetzt themenfremden Schritt „{ot.get('title')}“."
add(
{
"source": "off_topic" if ot.get("issue") != "stage_mismatch" else "stage_mismatch",
"insert_after_index": insert_after,
"replace_step_index": idx,
"roadmap_major_step_index": major_idx,
"gap": {
"expected_phase": phase,
"off_topic_title": ot.get("title"),
"off_topic_exercise_id": ot.get("exercise_id"),
"roadmap_learning_goal": stage_goal or None,
},
"phase": phase,
"title_hint": title_hint,
"sketch": _default_sketch(
goal_query=goal_query,
brief=brief,
step_a=step_a,
step_b=step_b,
phase=str(phase),
rationale=sketch_rationale,
),
"rationale": rationale,
}
)
for spec in llm_specs:
add(dict(spec))
return specs[:5]
def build_gap_fill_goal_text(
*,
goal_query: str,
brief: PlanningSemanticBrief,
spec: Mapping[str, Any],
step_a: Optional[Mapping[str, Any]] = None,
step_b: Optional[Mapping[str, Any]] = None,
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
) -> str:
"""Ausführlicher Zieltext für KI-Neuanlage aus Pfad-, Roadmap- und Stufen-Kontext."""
topic = (brief.primary_topic or "Technik").strip()
phase = spec.get("phase") or "vertiefung"
from_title = (step_a or {}).get("title") or spec.get("from_title") or "vorherigem Schritt"
to_title = (step_b or {}).get("title") or spec.get("to_title") or "nächstem Schritt"
arc = ", ".join(brief.development_arc or []) or "einstieg → grundlage → vertiefung → anwendung → perfektion"
snap = dict(roadmap_snapshot or {})
if not snap:
snap = build_progression_gap_snapshot(semantic_brief=brief_to_summary_dict(brief))
parts = [
f"Planungsziel (gesamter Pfad): {goal_query}",
f"Hauptthema: {snap.get('primary_topic') or topic}",
]
if snap.get("entry_state"):
parts.append(
f"Eingangszustand (erreichte Voraussetzungen aus Vorstufen): {snap['entry_state']}"
)
if snap.get("entry_state_detail") and snap.get("entry_state_detail") != snap.get("entry_state"):
parts.append(f"Bisheriger Pfad:\n{snap['entry_state_detail']}")
if snap.get("start_situation") and not snap.get("entry_state"):
parts.append(f"Voraussetzung / Ausgangslage (Progression): {snap['start_situation']}")
elif snap.get("start_situation") and snap.get("prior_steps"):
parts.append(f"Ausgangsbasis des gesamten Pfads: {snap['start_situation']}")
if snap.get("target_state"):
parts.append(f"Gesamtziel der Progression: {snap['target_state']}")
if snap.get("roadmap_notes"):
parts.append(f"Ergänzender Kontext: {snap['roadmap_notes']}")
stage_goal = snap.get("stage_learning_goal") or spec.get("title_hint")
if stage_goal:
parts.append(f"Lernziel dieser Roadmap-Stufe: {stage_goal}")
parts.append(f"Entwicklungsphase dieser Übung: {snap.get('stage_phase') or phase}")
parts.append(f"Erwarteter Entwicklungsbogen: {arc}")
if spec.get("source") == "roadmap_unfilled":
parts.append(
"Einordnung: Übung für diese Roadmap-Stufe — das Stufen-Lernziel steht im Vordergrund."
)
if step_a:
parts.append(f"Vorherige Stufe: „{from_title}")
if step_b:
parts.append(f"Nächste Stufe: „{to_title}")
else:
parts.append(
f"Einordnung: didaktische Zwischenstufe zwischen „{from_title}“ und „{to_title}“."
)
if snap.get("stage_load_profile"):
parts.append(f"Belastungsschwerpunkte: {', '.join(snap['stage_load_profile'])}")
if snap.get("stage_success_criteria"):
parts.append(
"Erfolgskriterien dieser Stufe: "
+ "; ".join(str(x) for x in snap["stage_success_criteria"][:4])
)
if snap.get("stage_anti_patterns"):
parts.append(
"Vermeiden: " + "; ".join(str(x) for x in snap["stage_anti_patterns"][:3])
)
if snap.get("skill_hints"):
parts.append(
"Fähigkeiten-/Fokus-Hinweise: "
+ "; ".join(str(x) for x in snap["skill_hints"][:4])
)
expected = snap.get("expected_skills") or []
if expected:
names = [
str(s.get("skill_name") or "").strip()
for s in expected[:5]
if str(s.get("skill_name") or "").strip()
]
if names:
parts.append(
"Erwartete Fähigkeiten (Scoring): " + ", ".join(names)
)
if spec.get("rationale"):
parts.append(f"Qualitätsprüfung: {spec['rationale']}")
if spec.get("sketch"):
parts.append(f"Skizze: {spec['sketch']}")
parts.append(
"Die Übung muss die Stufe didaktisch erfüllen: klare Voraussetzungen, messbares Stufenziel, "
"Bezug zum Gesamtpfad — keine generische Kraftübung ohne Technikbezug. "
"Konkrete Durchführung, Ziel und Trainerhinweise ausformulieren."
)
return "\n\n".join(parts)[:8000]
def build_gap_fill_offer(
*,
spec: Mapping[str, Any],
steps: Sequence[Mapping[str, Any]],
goal_query: str = "",
brief: Optional[PlanningSemanticBrief] = None,
proposal: Optional[Mapping[str, Any]] = None,
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
) -> Dict[str, Any]:
source = spec.get("source")
idx = int(spec.get("insert_after_index") or 0)
major_idx = spec.get("roadmap_major_step_index")
if source == "roadmap_unfilled" and major_idx is not None:
try:
mi = int(major_idx)
except (TypeError, ValueError):
mi = idx
step_a, step_b = _resolve_neighbor_steps_by_major_index(steps, mi)
idx = mi
else:
step_a = steps[idx] if idx < len(steps) else None
step_b = steps[idx + 1] if idx + 1 < len(steps) else None
offer_id = f"{spec.get('source')}-{idx}-{uuid.uuid4().hex[:8]}"
enriched_snapshot = dict(roadmap_snapshot) if roadmap_snapshot else {}
major_raw = spec.get("roadmap_major_step_index")
if major_raw is not None:
enriched_snapshot = enrich_gap_snapshot_with_entry_state(
enriched_snapshot,
steps=steps,
major_step_index=major_raw,
)
goal_for_ai = ""
if brief and goal_query:
goal_for_ai = build_gap_fill_goal_text(
goal_query=goal_query,
brief=brief,
spec=spec,
step_a=step_a,
step_b=step_b,
roadmap_snapshot=enriched_snapshot or None,
)
ctx_preview = enriched_snapshot or None
offer: Dict[str, Any] = {
"offer_id": offer_id,
"source": spec.get("source"),
"insert_after_index": idx,
"replace_step_index": spec.get("replace_step_index"),
"title_hint": spec.get("title_hint"),
"sketch": spec.get("sketch"),
"goal_for_ai": goal_for_ai or spec.get("sketch"),
"context_preview": ctx_preview,
"phase": spec.get("phase"),
"rationale": spec.get("rationale"),
"has_ai_payload": False,
"from_title": (step_a or {}).get("title"),
"to_title": (step_b or {}).get("title"),
"primary_topic": (brief.primary_topic if brief else None),
"roadmap_major_step_index": spec.get("roadmap_major_step_index"),
}
if proposal:
offer["has_ai_payload"] = True
offer["proposal_key"] = proposal.get("proposal_key")
offer["ai_suggestion"] = proposal.get("ai_suggestion")
offer["proposal_title"] = proposal.get("title")
offer["proposal_summary"] = proposal.get("summary")
return offer
def apply_gap_fill_after_qa(
cur,
steps: List[Dict[str, Any]],
specs: Sequence[Mapping[str, Any]],
*,
goal_query: str,
brief: PlanningSemanticBrief,
include_ai_calls: bool = True,
max_ai_proposals: int = 3,
auto_insert_proposals: bool = False,
roadmap_snapshot: Optional[Mapping[str, Any]] = None,
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]:
"""
Erzeugt gap_fill_offers für die UI; optional KI-Vorschläge einfügen.
Returns: (steps, ai_proposals, gap_fill_offers)
"""
if not specs:
return steps, [], []
out = list(steps)
proposals: List[Dict[str, Any]] = []
offers: List[Dict[str, Any]] = []
for spec in specs:
source = spec.get("source")
if source == "roadmap_unfilled":
proposal: Optional[Dict[str, Any]] = None
if include_ai_calls and len(proposals) < max_ai_proposals:
proposal = try_suggest_ai_stage_step(
cur,
goal_query=goal_query,
brief=brief,
spec=spec,
steps=out,
)
offer = build_gap_fill_offer(
spec=spec,
steps=out,
goal_query=goal_query,
brief=brief,
proposal=proposal,
roadmap_snapshot=roadmap_snapshot,
)
offers.append(offer)
if proposal and auto_insert_proposals:
proposals.append(
{
"roadmap_major_step_index": spec.get("roadmap_major_step_index"),
"proposal_key": proposal.get("proposal_key"),
"proposal_title": proposal.get("title"),
"offer_id": offer.get("offer_id"),
}
)
continue
idx = int(spec.get("insert_after_index") or 0)
if idx < 0 or idx >= len(out) - 1:
continue
step_a = out[idx]
step_b = out[idx + 1]
if step_a.get("is_ai_proposal") or step_b.get("is_ai_proposal"):
offer = build_gap_fill_offer(
spec=spec,
steps=out,
goal_query=goal_query,
brief=brief,
proposal=None,
roadmap_snapshot=roadmap_snapshot,
)
offers.append(offer)
continue
gap = dict(spec.get("gap") or {})
if not gap.get("expected_phase"):
gap["expected_phase"] = spec.get("phase") or "vertiefung"
proposal = None
if include_ai_calls and len(proposals) < max_ai_proposals:
proposal = try_suggest_ai_bridge_step(
cur,
goal_query=goal_query,
brief=brief,
step_a=step_a,
step_b=step_b,
gap=gap,
title_hint=str(spec.get("title_hint") or ""),
sketch_hint=str(spec.get("sketch") or ""),
)
offer = build_gap_fill_offer(
spec=spec,
steps=out,
goal_query=goal_query,
brief=brief,
proposal=proposal,
roadmap_snapshot=roadmap_snapshot,
)
offers.append(offer)
if proposal and auto_insert_proposals:
out.insert(idx + 1, proposal)
proposals.append(
{
"inserted_after_index": idx,
"proposal_key": proposal.get("proposal_key"),
"proposal_title": proposal.get("title"),
"gap": gap,
"offer_id": offer.get("offer_id"),
}
)
return out, proposals, offers
def insert_ai_proposals_for_gaps(
cur,
steps: list,
unfilled_gaps: list,
*,
goal_query: str,
brief: PlanningSemanticBrief,
max_proposals: int = 2,
) -> tuple[list, list]:
"""Legacy: Fügt KI-Vorschläge für Lücken ein, wenn Bibliotheks-Brücke fehlte."""
specs = collect_gap_fill_specs(
steps=steps,
unfilled_gaps=unfilled_gaps,
off_topic_steps=[],
llm_specs=[],
brief=brief,
goal_query=goal_query,
)
out, proposals, _offers = apply_gap_fill_after_qa(
cur,
steps,
specs,
goal_query=goal_query,
brief=brief,
include_ai_calls=True,
max_ai_proposals=max_proposals,
auto_insert_proposals=True,
)
return out, proposals
__all__ = [
"apply_gap_fill_after_qa",
"build_gap_fill_goal_text",
"build_gap_fill_offer",
"collect_gap_fill_specs",
"insert_ai_proposals_for_gaps",
"try_suggest_ai_bridge_step",
"try_suggest_ai_stage_step",
]

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More