// 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'; import 'package:scheduler_persistence_sqlite/sqlite.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 '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, }); /// Opens the configured SQLite runtime and bootstraps required owner state. static Future open({ String? sqlitePath, Clock? clock, CivilDate? selectedDate, FocusFlowFileLogger? logger, }) async { final resolvedLogger = logger ?? FocusFlowFileLogger.disabled(); final resolvedSqlitePath = sqlitePath ?? sqlitePathFromEnvironment(); await resolvedLogger.info('Opening SQLite runtime at $resolvedSqlitePath.'); final runtime = await SqliteApplicationRuntime.open( path: resolvedSqlitePath, ); final bootstrap = await runtime.bootstrap(); if (bootstrap.isFailure) { await runtime.close(); await resolvedLogger.error( 'SQLite bootstrap failed.', error: bootstrap.failure!.code.name, ); throw StateError( 'SQLite bootstrap failed: ${bootstrap.failure!.code.name}', ); } await resolvedLogger.debug('SQLite bootstrap completed.'); final resolvedClock = clock ?? const SystemClock(); final date = selectedDate ?? CivilDate.fromDateTime(resolvedClock.now()); final readAt = resolvedClock.now().toUtc(); await resolvedLogger.info( 'Persistent scheduler composition opened for ${date.toIsoString()}.', ); return PersistentSchedulerComposition._( runtime: runtime, todayQuery: GetTodayStateQuery( applicationStore: runtime.applicationStore, timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), ), managementUseCases: V1ApplicationManagementUseCases( applicationStore: runtime.applicationStore, ), commandUseCases: V1ApplicationCommandUseCases( applicationStore: runtime.applicationStore, timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(), ), date: date, readAt: readAt, logger: resolvedLogger, ); } /// 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; /// Pending close operation, if disposal has started. Future? _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() { return SelectedDateController(initialDate: initialDate); } /// Creates the controller used by the compact Today screen mockup. @override TodayScreenController createTodayScreenController({ required SelectedDateController selectedDates, }) { return TodayScreenController( selectedDates: selectedDates, dateLabelFor: dateLabelFor, read: (date) { return todayQuery.execute( GetTodayStateRequest( context: context('ui-plan-1-read-today'), date: date, includeNextFlexibleItem: true, ), ); }, ); } /// Creates the command controller used by interactive scheduler controls. @override SchedulerCommandController createCommandController({ required ReadRefresh refreshReads, required SelectedDateReader selectedDate, }) { return SchedulerCommandController( commands: commandUseCases, contextFor: context, selectedDate: selectedDate, refreshReads: refreshReads, quickCaptureProjectId: defaultProjectId, operationIdPrefix: 'ui-command-${readAt.microsecondsSinceEpoch}', ); } /// Creates an application operation context for [operationId]. ApplicationOperationContext context(String operationId, {DateTime? now}) { return ApplicationOperationContext.start( operationId: operationId, ownerTimeZone: OwnerTimeZoneContext( ownerId: SqliteApplicationRuntime.defaultV1OwnerId, timeZoneId: SqliteApplicationRuntime.defaultV1TimeZoneId, ), clock: FixedClock(now ?? readAt), idGenerator: SequentialIdGenerator(prefix: operationId), ); } /// Closes the persistent runtime. @override Future dispose() { return _disposeFuture ??= _dispose(); } /// Closes runtime resources and records shutdown diagnostics when enabled. Future _dispose() async { await logger.debug('Closing SQLite runtime.'); await runtime.close(); await logger.info('SQLite runtime closed.'); } }