// 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/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 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? _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 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 dispose() { return _disposeFuture ??= _dispose(); } /// Closes runtime resources and records shutdown diagnostics when enabled. Future _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(); } }