focus-flow/apps/focus_flow_flutter/lib/app/persistent_scheduler_composition.dart

295 lines
9.6 KiB
Dart

// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only
/// Composes the FocusFlow app around persistent SQLite scheduler state.
library;
import 'dart:io';
import 'package:scheduler_core/scheduler_core.dart' hide logger;
import 'package:scheduler_core/scheduler_core.dart'
as scheduler_core
show logger;
import 'package:scheduler_persistence_sqlite/sqlite.dart';
import '../controllers/backlog_board_controller.dart';
import '../controllers/scheduler_command_controller.dart';
import '../controllers/selected_date_controller.dart';
import '../controllers/today_screen_controller.dart';
import 'runtime/focus_flow_file_logger.dart';
import 'runtime/focus_flow_runtime_config.dart';
import 'demo_scheduler_composition.dart' show formatSchedulerDateLabel;
import 'scheduler_app_composition.dart';
/// Persistent SQLite-backed scheduler composition for normal app runtime.
final class PersistentSchedulerComposition implements SchedulerAppComposition {
/// Creates a persistent composition from already-open collaborators.
PersistentSchedulerComposition._({
required this.runtime,
required this.todayQuery,
required this.managementUseCases,
required this.commandUseCases,
required this.date,
required this.readAt,
required this.logger,
required this.uiTimeZone,
required this.clock,
required this.timeZoneResolver,
});
/// Opens the configured SQLite runtime and bootstraps required owner state.
static Future<PersistentSchedulerComposition> open({
String? sqlitePath,
Clock? clock,
CivilDate? selectedDate,
FocusFlowFileLogger? logger,
FocusFlowUiTimeZone? uiTimeZone,
}) async {
final resolvedLogger = logger ?? FocusFlowFileLogger.disabled();
final resolvedTimeZone = uiTimeZone ?? FocusFlowUiTimeZone.utc;
final resolvedSqlitePath = sqlitePath ?? sqlitePathFromEnvironment();
scheduler_core.logger.info(
() =>
'Opening SQLite runtime. sqlitePath=$resolvedSqlitePath '
'uiTimeZone=${resolvedTimeZone.code}',
);
final runtime = await SqliteApplicationRuntime.open(
path: resolvedSqlitePath,
);
final bootstrap = await runtime.bootstrap();
if (bootstrap.isFailure) {
await runtime.close();
scheduler_core.logger.error(
() =>
'SQLite bootstrap failed. failureCode=${bootstrap.failure!.code.name}',
error: bootstrap.failure!.code.name,
);
throw StateError(
'SQLite bootstrap failed: ${bootstrap.failure!.code.name}',
);
}
scheduler_core.logger.debug(() => 'SQLite bootstrap completed.');
final resolvedClock = clock ?? const SystemClock();
final openedAt = resolvedClock.now();
final date = selectedDate ?? resolvedTimeZone.toLocalCivilDate(openedAt);
final readAt = openedAt.toUtc();
final resolver = FixedOffsetTimeZoneResolver(
offset: resolvedTimeZone.utcOffset,
);
scheduler_core.logger.info(
() =>
'Persistent scheduler composition opened. date=${date.toIsoString()} '
'readAt=${readAt.toIso8601String()} uiTimeZone=${resolvedTimeZone.code}',
);
return PersistentSchedulerComposition._(
runtime: runtime,
todayQuery: GetTodayStateQuery(
applicationStore: runtime.applicationStore,
timeZoneResolver: resolver,
),
managementUseCases: V1ApplicationManagementUseCases(
applicationStore: runtime.applicationStore,
),
commandUseCases: V1ApplicationCommandUseCases(
applicationStore: runtime.applicationStore,
timeZoneResolver: resolver,
),
date: date,
readAt: readAt,
logger: resolvedLogger,
uiTimeZone: resolvedTimeZone,
clock: resolvedClock,
timeZoneResolver: resolver,
);
}
/// Resolves the SQLite path from Dart defines or the local dev default.
static String sqlitePathFromEnvironment() {
const configured = String.fromEnvironment('SCHEDULER_SQLITE_PATH');
final trimmed = configured.trim();
if (trimmed.isNotEmpty) {
return trimmed;
}
final home =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (home == null || home.trim().isEmpty) {
return 'scheduler.sqlite';
}
return '$home/ADHD_Scheduler/scheduler.sqlite';
}
/// SQLite runtime owning the database handle.
final SqliteApplicationRuntime runtime;
/// Query object used to read the Today state.
final GetTodayStateQuery todayQuery;
/// Management use cases used by backlog-facing UI surfaces.
final V1ApplicationManagementUseCases managementUseCases;
/// Command use cases used by interactive UI controls.
final V1ApplicationCommandUseCases commandUseCases;
/// Local date represented by the app.
final CivilDate date;
/// Stable read clock instant for operation contexts.
final DateTime readAt;
/// Optional app runtime logger.
final FocusFlowFileLogger logger;
/// UI-local fixed-offset time-zone reference for wall-clock behavior.
final FocusFlowUiTimeZone uiTimeZone;
/// Runtime clock used to derive current UTC instants.
final Clock clock;
/// Resolver configured from [uiTimeZone].
final FixedOffsetTimeZoneResolver timeZoneResolver;
/// Pending close operation, if disposal has started.
Future<void>? _disposeFuture;
/// Default project id used by quick capture.
@override
String get defaultProjectId => SqliteApplicationRuntime.defaultV1ProjectId;
/// Human-readable label for [date].
@override
String get dateLabel => dateLabelFor(date);
/// Initial selected owner-local planning date.
@override
CivilDate get initialDate => date;
/// Formats [date] for compact UI date labels.
@override
String dateLabelFor(CivilDate date) {
return formatSchedulerDateLabel(date);
}
/// Creates the controller that owns the visible planning date.
@override
SelectedDateController createSelectedDateController() {
scheduler_core.logger.finer(
() =>
'Creating selected date controller. date=${initialDate.toIsoString()}',
);
return SelectedDateController(initialDate: initialDate);
}
/// Creates the controller used by the compact Today screen mockup.
@override
TodayScreenController createTodayScreenController({
required SelectedDateController selectedDates,
}) {
scheduler_core.logger.finer(
() => 'Creating today screen controller. date=${date.toIsoString()}',
);
return TodayScreenController(
selectedDates: selectedDates,
dateLabelFor: dateLabelFor,
now: _utcNow,
timeZoneOffset: uiTimeZone.utcOffset,
read: (date) {
return todayQuery.execute(
GetTodayStateRequest(
context: context('ui-plan-1-read-today'),
date: date,
includeNextFlexibleItem: true,
),
);
},
);
}
/// Creates the controller used by the Backlog board screen.
@override
BacklogBoardController createBacklogBoardController({
required SelectedDateController selectedDates,
}) {
scheduler_core.logger.finer(
() => 'Creating backlog board controller. date=${date.toIsoString()}',
);
return BacklogBoardController(
selectedDates: selectedDates,
dateLabelFor: dateLabelFor,
readBoard: (request) {
return managementUseCases.getBacklogBoard(
GetBacklogBoardRequest(
context: context('ui-read-backlog-board'),
localDate: request.localDate,
searchText: request.searchText,
filters: request.filters,
sortKey: request.sortKey,
groupMode: request.groupMode,
),
);
},
readTaskDetail: (taskId, localDate) {
return managementUseCases.getBacklogTaskDetail(
GetBacklogTaskDetailRequest(
context: context('ui-read-backlog-detail'),
localDate: localDate,
taskId: taskId,
),
);
},
);
}
/// Creates the command controller used by interactive scheduler controls.
@override
SchedulerCommandController createCommandController({
required ReadRefresh refreshReads,
required SelectedDateReader selectedDate,
}) {
scheduler_core.logger.finer(
() =>
'Creating scheduler command controller. operationIdPrefix=ui-command-${readAt.microsecondsSinceEpoch}',
);
return SchedulerCommandController(
commands: commandUseCases,
contextFor: context,
selectedDate: selectedDate,
refreshReads: refreshReads,
quickCaptureProjectId: defaultProjectId,
operationIdPrefix: 'ui-command-${readAt.microsecondsSinceEpoch}',
currentDateFor: uiTimeZone.toLocalCivilDate,
now: _utcNow,
);
}
/// Creates an application operation context for [operationId].
ApplicationOperationContext context(String operationId, {DateTime? now}) {
return ApplicationOperationContext.start(
operationId: operationId,
ownerTimeZone: OwnerTimeZoneContext(
ownerId: SqliteApplicationRuntime.defaultV1OwnerId,
timeZoneId: uiTimeZone.code,
),
clock: FixedClock((now ?? _utcNow()).toUtc()),
idGenerator: SequentialIdGenerator(prefix: operationId),
);
}
/// Closes the persistent runtime.
@override
Future<void> dispose() {
return _disposeFuture ??= _dispose();
}
/// Closes runtime resources and records shutdown diagnostics when enabled.
Future<void> _dispose() async {
scheduler_core.logger.debug(() => 'Closing SQLite runtime.');
await runtime.close();
scheduler_core.logger.info(() => 'SQLite runtime closed.');
}
/// Returns the current runtime instant normalized to UTC.
DateTime _utcNow() {
return clock.now().toUtc();
}
}