Skip to content

Commit c27bbbc

Browse files
wip
1 parent 1ade303 commit c27bbbc

37 files changed

Lines changed: 889 additions & 560 deletions

File tree

CLAUDE.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Repository Structure
6+
7+
This is a monorepo with four sub-projects:
8+
9+
- `school_data_hub_flutter/` — Flutter client (Android, Windows; WIP iOS/macOS/Linux)
10+
- `school_data_hub_server/` — Serverpod 2.9.1 Dart backend
11+
- `school_data_hub_client/` — Serverpod-generated shared client library (used by both Flutter and server)
12+
- `school_data_hub_website/` — Project website
13+
14+
## Common Commands
15+
16+
### Flutter Client (`school_data_hub_flutter/`)
17+
18+
```bash
19+
flutter run # Run on connected device/emulator
20+
flutter test # Run all tests
21+
flutter test test/path/to/test.dart # Run a single test file
22+
flutter analyze # Lint
23+
flutter pub get # Install dependencies
24+
```
25+
26+
### Server (`school_data_hub_server/`)
27+
28+
```bash
29+
# Run locally (apply pending migrations on start)
30+
dart run bin/main.dart --apply-migrations
31+
32+
# Shortcut via Makefile
33+
make run
34+
35+
# After modifying .yaml model schemas — regenerate models + UML
36+
make generate # = serverpod generate + uml
37+
make migration # = serverpod generate + create-migration + apply + uml
38+
39+
# Start Docker (Postgres + Redis) for local dev
40+
make docker # = docker compose up --build --detach
41+
```
42+
43+
### Release (Shorebird — from `school_data_hub_flutter/`)
44+
45+
```bash
46+
shorebird release android --artifact=apk # Full release
47+
shorebird patch android # OTA patch
48+
```
49+
50+
## Architecture
51+
52+
### Privacy-First Design
53+
54+
Personal pupil data (`PupilIdentity`) is **never stored on the server**. It is stored encrypted on each device (via `flutter_secure_storage`) and transferred between devices over an encrypted stream. The server only holds anonymous `PupilData` referenced by a numeric internal ID. API calls require both an authenticated session **and** the pupil's internal ID.
55+
56+
### Flutter Client Architecture
57+
58+
**Dependency Injection** uses `get_it` (accessed via the `di` global from `watch_it`/`flutter_it`). Managers are registered in three ordered scopes:
59+
60+
1. **Core scope** (always registered, `InitManager.registerCoreManagers()`): `EnvManager`, `NotificationService`, `ServerpodConnectivityMonitor`, `ShorebirdUpdateManager`
61+
2. **Active-env scope** (`InitScope.onActiveEnvScope`): registered in `InitOnActiveEnv` after an environment (school key) is set
62+
3. **Auth scope** (`InitScope.onAuthScope`): registered in `InitOnUserAuth` after login — contains all feature managers (`PupilProxyManager`, `AttendanceManager`, etc.)
63+
4. **Matrix scope** (`InitScope.onMatrixEnvScope`): lazily pushed when Matrix credentials are present
64+
65+
Scopes are dropped on logout/env-change and re-registered fresh. See `core/init/init_manager.dart` and `core/init/init_on_user_auth.dart` for the full dependency graph.
66+
67+
**State management** uses `watch_it` (`flutter_it` package). All managers extend `ChangeNotifier`. Widgets extend `WatchingWidget` (stateless) or `WatchingStatefulWidget` (stateful) and use:
68+
- `watchValue((ManagerType x) => x.someValueNotifier)` — watch a `ValueNotifier` field
69+
- `watch(someChangeNotifier)` — watch any `Listenable`
70+
71+
**The `PupilProxy` model** (`features/_pupil/domain/models/pupil_proxy.dart`) is the central reactive object. It combines:
72+
- `PupilData` — server model from Serverpod, fetched via `PupilDataApiService`
73+
- `PupilIdentity` — local personal data (name, class, birthday, etc.) managed by `PupilIdentityManager`
74+
75+
`PupilProxyManager` holds the master list of all proxies, keeps them updated via the `HubStreamService` server-sent event stream, and re-fetches on reconnect.
76+
77+
**Feature structure** follows `data / domain / presentation` layers:
78+
- `data/` — API service classes (one per feature, calling Serverpod endpoints via `school_data_hub_client`)
79+
- `domain/` — manager singletons, domain models, filter managers
80+
- `presentation/` — pages and widgets
81+
82+
**Real-time updates** flow through `HubStreamService`, which wraps a Serverpod streaming endpoint. Managers subscribe to this stream and handle `PupilData` (upsert) and `HubReconnected` (re-fetch) events.
83+
84+
**Filtering** is done through a hierarchy of filter managers (`PupilsFilter`, `PupilFilterManager`, feature-specific filter managers). `PupilsFilter` is the aggregate filter used to produce the filtered list shown in UIs. Migration to the `PupilsFilter` architecture is in progress — some older features still use ad-hoc filtering.
85+
86+
**Navigation** uses a bottom navigation bar with 5 tabs: Pupil Lists, School Lists, Learning Resources, Tools, Settings. Individual features navigate imperatively (no `go_router` routing in use for main flow; `go_router` is listed as a dependency but is a WIP).
87+
88+
### Server Architecture
89+
90+
Built on **Serverpod 2.9.1**. Model schemas live in `lib/src/_features/<feature>/schemas/` as `.yaml` files. Running `serverpod generate` produces Dart classes in `lib/src/generated/` and the client library in `school_data_hub_client/`.
91+
92+
Features mirror the Flutter client: `_attendance`, `_authorizations`, `_pupil`, `_school_lists`, `_schoolday_events`, `books`, `learning`, `learning_support`, `matrix`, `timetable`, `user`, `workbooks`, etc.
93+
94+
Encrypted files (documents, images) are stored in `storage/private/` subdirectories (`avatars`, `documents`, `events`, `auths`).
95+
96+
### Local Development Setup
97+
98+
Server URL in the school key:
99+
- **Windows**: `http://127.0.0.1:5000/api`
100+
- **Android Emulator**: `http://10.0.2.2:5000/api`
101+
102+
The server requires Docker running (Postgres + Redis). Start with `make docker` from `school_data_hub_server/`, then `make run` to apply migrations and start the Dart server.
103+
104+
## Key Conventions
105+
106+
- `di<SomeManager>()` — access any registered singleton anywhere in the Flutter app
107+
- Managers own their API service instances directly (instantiated in the manager, not DI-registered separately, unless shared)
108+
- Use `_log = Logger('FeatureName')` in every class; logs are collected by `LogService` and visible in the in-app log viewer
109+
- Server model changes: edit the `.yaml` schema → `make migration` → never hand-edit generated files in `lib/src/generated/`
110+
- All `.dart` files in `school_data_hub_client/` are generated — do not edit them directly

school_data_hub_flutter/lib/app_utils/shorebird_code_push_page.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,14 @@ class _ShorebirdCodePushPageState extends State<ShorebirdCodePushPage> {
127127

128128
void _showRestartBanner() {
129129
di<NotificationService>().showInformationDialog(
130+
NotificationType.info,
130131
'Ein neuer Patch ist verfügbar! Bitte starte die App neu.',
131132
);
132133
}
133134

134135
void _showErrorBanner(Object error) {
135136
di<NotificationService>().showInformationDialog(
137+
NotificationType.error,
136138
'Fehler beim Herunterladen des Updates: $error.',
137139
);
138140
ScaffoldMessenger.of(context)
@@ -203,15 +205,17 @@ class _ShorebirdCodePushPageState extends State<ShorebirdCodePushPage> {
203205
onPressed: () async {
204206
if (defaultTargetPlatform == TargetPlatform.android ||
205207
defaultTargetPlatform == TargetPlatform.iOS) {
206-
await TerminateRestart.instance.restartAppWithConfirmation(
207-
context,
208-
title: 'Restart App',
209-
message: 'Do you want to restart the app?',
210-
terminate: true,
211-
);
208+
await TerminateRestart.instance
209+
.restartAppWithConfirmation(
210+
context,
211+
title: 'Restart App',
212+
message: 'Do you want to restart the app?',
213+
terminate: true,
214+
);
212215
} else {
213216
if (!context.mounted) return;
214217
di<NotificationService>().showInformationDialog(
218+
NotificationType.info,
215219
'Bitte schließen Sie die App manuell und starten Sie sie erneut.',
216220
);
217221
}

school_data_hub_flutter/lib/common/services/notification_service.dart

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:logging/logging.dart';
35
import 'package:school_data_hub_flutter/common/models/enums.dart';
46

57
export 'package:school_data_hub_flutter/common/models/enums.dart';
68

9+
enum NotificationTarget { snackBar, informationDialog, overlay, idle }
10+
711
class NotificationData {
12+
final NotificationTarget target;
813
final NotificationType type;
914
final String message;
1015

11-
NotificationData(this.type, this.message);
16+
const NotificationData({
17+
required this.target,
18+
required this.type,
19+
required this.message,
20+
});
1221
}
1322

1423
final _log = Logger('NotificationService');
1524

1625
class NotificationService {
17-
final _snackBar = ValueNotifier<NotificationData>(
18-
NotificationData(NotificationType.success, ''),
26+
final _notification = ValueNotifier<NotificationData>(
27+
const NotificationData(
28+
target: NotificationTarget.idle,
29+
type: NotificationType.success,
30+
message: '',
31+
),
1932
);
20-
ValueListenable<NotificationData> get notification => _snackBar;
33+
ValueListenable<NotificationData> get notification => _notification;
2134

2235
final _apiRunning = ValueNotifier<bool>(false);
2336
ValueListenable<bool> get isRunning => _apiRunning;
@@ -26,6 +39,7 @@ class NotificationService {
2639

2740
final _heavyLoading = ValueNotifier<bool>(false);
2841
ValueListenable<bool> get heavyLoading => _heavyLoading;
42+
int _heavyLoadingCounter = 0;
2943

3044
NotificationService();
3145

@@ -47,21 +61,32 @@ class NotificationService {
4761
_log.warning('''SNACK BAR WARNING:
4862
$message''');
4963
case NotificationType.dialog:
50-
_log.info('''SNACK BAR DIALOG:
51-
$message''');
5264
}
5365

54-
//- TODO: Investigate when we really want to show one
55-
//- before uncommenting this
56-
// _snackBar.value = NotificationData(type, message);
66+
_notification.value = NotificationData(
67+
target: NotificationTarget.snackBar,
68+
type: type,
69+
message: message,
70+
);
71+
}
72+
73+
void showInformationDialog(NotificationType type, String message) {
74+
_notification.value = NotificationData(
75+
target: NotificationTarget.informationDialog,
76+
type: type,
77+
message: message,
78+
);
5779
}
5880

59-
void showInformationDialog(String message) {
60-
_snackBar.value = NotificationData(NotificationType.dialog, message);
81+
void showInformationDialogMessage(
82+
String message, {
83+
NotificationType type = NotificationType.info,
84+
}) {
85+
showInformationDialog(type, message);
86+
}
6187

62-
_log.fine('''INFORMATION DIALOG:
63-
$message
64-
''');
88+
void showErrorDialog(String message) {
89+
showInformationDialogMessage(message, type: NotificationType.error);
6590
}
6691

6792
void apiRunning(bool value) {
@@ -73,6 +98,36 @@ class NotificationService {
7398
}
7499

75100
void setHeavyLoadingValue(bool value) {
76-
_heavyLoading.value = value;
101+
if (value) {
102+
beginHeavyLoading();
103+
return;
104+
}
105+
endHeavyLoading();
106+
}
107+
108+
void beginHeavyLoading() {
109+
_heavyLoadingCounter += 1;
110+
if (_heavyLoadingCounter == 1) {
111+
_heavyLoading.value = true;
112+
}
113+
}
114+
115+
void endHeavyLoading() {
116+
if (_heavyLoadingCounter == 0) {
117+
return;
118+
}
119+
_heavyLoadingCounter -= 1;
120+
if (_heavyLoadingCounter == 0) {
121+
_heavyLoading.value = false;
122+
}
123+
}
124+
125+
Future<T> runWithHeavyLoading<T>(Future<T> Function() action) async {
126+
beginHeavyLoading();
127+
try {
128+
return await action();
129+
} finally {
130+
endHeavyLoading();
131+
}
77132
}
78133
}

school_data_hub_flutter/lib/common/widgets/hub_document/hub_documents_section.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ class _DocumentItem extends StatelessWidget {
197197
},
198198
onLongPress: () async {
199199
if (!isAuthorizedToDelete) {
200-
di<NotificationService>().showSnackBar(
200+
di<NotificationService>().showInformationDialog(
201201
NotificationType.error,
202202
'Nur Admins können Dokumente löschen',
203203
);

0 commit comments

Comments
 (0)