# Tatlock UI - Comprehensive Implementation Plan > **Session Continuity Note**: This document is designed to be self-contained. If starting a new session, read this entire document to understand the project scope and decisions made. ## Project Summary **Tatlock UI** is a Flutter application that will serve as the unified homelab dashboard for the Tower of Joy infrastructure, replacing Organizr at `home.schweitz.net`. ### Key Facts - **Project folder**: `/mnt/media/Projects/tatlock-ui` (renamed from tower-ui) - **Git remote**: `ssh://git@git.schweitz.net:2222/jpmschweitzer/tatlock-ui.git` (already created) - **Platforms**: web (primary), android, macos, linux, windows, ios - **Primary backend**: Core API (port 8083) + Tatlock API (port 8000) - **Authentication**: Authentik SSO via OIDC - **Web deployment**: Docker container on port 8092, proxied via NPM at `home.schweitz.net` ### What It Replaces - **Organizr** (current dashboard at home.schweitz.net) - See `/mnt/media/Projects/tower-ui/uploads/portainer-ui.png` for current UI - **Eventually Open WebUI** - Custom LLM chat interface for Tatlock agents --- ## Architecture Decisions (Final) ### 1. Dogmatic Clean Architecture **Decision**: Use strict Clean Architecture with documented requirements for consistency. ``` lib/features/{feature}/ ├── presentation/ # UI Layer │ ├── pages/ # Full screen widgets │ ├── widgets/ # Feature-specific widgets │ └── providers/ # Riverpod providers/controllers ├── domain/ # Business Logic Layer │ ├── entities/ # Business objects (immutable) │ ├── repositories/ # Abstract repository interfaces │ └── usecases/ # Single-purpose business operations └── data/ # Data Layer ├── models/ # JSON serializable DTOs ├── datasources/ # API clients, local storage └── repositories/ # Repository implementations ``` **Rules**: - Domain layer has NO dependencies on Flutter or external packages - Data layer implements domain interfaces - Presentation layer only depends on domain layer - All cross-layer communication via dependency injection (Riverpod) ### 2. DataGrid Component System **Decision**: Adapt the fframe ListGrid pattern for consistent table UIs across all features. Based on: `https://github.com/postmeridiem/fframe/blob/main/fframe/lib/screens/listgrid_screen/` Key components: - `DataGridConfig` - Declarative configuration - `DataGridColumn` - Column definitions with builders - `DataGridAction` - Per-row actions - `DataGridBulkAction` - Multi-select actions - `DataGridSource` - Abstract data source (Core API adapter) - `DataGridNotifier` - Riverpod state management ### 3. Theming **Decision**: System preference + manual override, using Material3 colorScheme exclusively. - Follow system dark/light mode by default - Allow manual override that persists to SharedPreferences - Never use raw colors in widgets - always `Theme.of(context).colorScheme.*` - Use automatic theme generator (flex_color_scheme or similar) ### 4. State Management **Decision**: Riverpod 2.x with code generation. Packages: `flutter_riverpod`, `riverpod_annotation`, `riverpod_generator` --- ## Implementation Phases ### Phase 0: Documentation **Goal**: Establish project documentation before any code. **Files to create**: ``` tatlock-ui/ ├── README.md # Project overview, setup instructions ├── AGENTS.md # LLM agent instructions (copy pattern from Tatlock) ├── CHANGELOG.md # Version history (start with 0.0.1) ├── docs/ │ ├── ARCHITECTURE.md # Clean Architecture rules and patterns │ ├── API_INTEGRATION.md # Core API and Tatlock API endpoints │ ├── DEPLOYMENT.md # Docker, NPM, Portainer setup │ ├── DATAGRID.md # DataGrid component API specification │ └── THEMING.md # Theme system documentation ``` **Content for ARCHITECTURE.md** - Document: - Clean Architecture layer rules - File naming conventions - Dependency injection patterns - Testing requirements per layer **Content for API_INTEGRATION.md** - Document: - Core API endpoints (from `/mnt/media/Projects/portainer-core/CONTAINERS.md`) - Tatlock API endpoints (chat completions, responses API) - Authentication flow with Authentik - Error handling patterns **Content for DATAGRID.md** - Document: - Full DataGrid API spec (see below) - Usage examples for each feature - Customization patterns ### Phase 1: Foundation **Goal**: Project setup, core infrastructure, basic shell. **Tasks**: 1. Initialize Flutter project with all platforms 2. Initialize git and push to Gitea 3. Set up project structure (Clean Architecture folders) 4. Configure dependencies (pubspec.yaml) 5. Implement core infrastructure: - `lib/core/api/api_client.dart` - Dio HTTP client - `lib/core/api/api_interceptors.dart` - Auth, logging, error handling - `lib/core/auth/auth_service.dart` - Authentik OIDC - `lib/core/auth/auth_provider.dart` - Riverpod auth state - `lib/core/config/app_config.dart` - Environment configuration - `lib/core/config/service_locator.dart` - Dependency setup - `lib/core/theme/app_theme.dart` - Material3 theme - `lib/core/theme/theme_provider.dart` - Theme state (system + override) 6. Create app shell: - `lib/app.dart` - MaterialApp.router setup - `lib/routing/app_router.dart` - go_router configuration - `lib/shared/layouts/app_scaffold.dart` - Main layout with sidebar - `lib/shared/layouts/adaptive_navigation.dart` - Responsive nav 7. Set up CI/CD: - `.gitea/workflows/test.yml` - `.gitea/workflows/build-web.yml` - `Dockerfile` ### Phase 2: DataGrid Component **Goal**: Build the reusable DataGrid system before features. **Files to create**: ``` lib/shared/components/data_grid/ ├── data_grid.dart # Main widget ├── data_grid_config.dart # Configuration classes ├── data_grid_column.dart # Column definition ├── data_grid_action.dart # Action definitions ├── data_grid_source.dart # Abstract data source ├── data_grid_provider.dart # Riverpod state ├── data_grid_state.dart # Freezed state class ├── widgets/ │ ├── data_grid_header.dart # Header row with sort indicators │ ├── data_grid_row.dart # Data row │ ├── data_grid_cell.dart # Cell wrapper │ ├── data_grid_checkbox.dart # Selection checkbox │ ├── data_grid_actions_menu.dart # Row actions popup │ ├── data_grid_bulk_actions.dart # Bulk action bar │ ├── data_grid_search_bar.dart # Search input │ ├── data_grid_footer.dart # Footer with count/pagination │ └── data_grid_empty_state.dart # Empty state display └── adapters/ └── core_api_source.dart # Core API data source adapter ``` ### Phase 3: Dashboard (Organizr Replacement) **Goal**: Replace Organizr homepage with Tatlock UI dashboard. **Features to match** (from portainer-ui.png): - Sidebar navigation with grouped items - System metrics gauges (CPU, Memory, Disk) - Weather widget (optional - requires API) - Air quality widget (optional) - Service quick links grid **Files to create**: ``` lib/features/dashboard/ ├── presentation/ │ ├── pages/dashboard_page.dart │ └── widgets/ │ ├── metrics_gauge.dart │ ├── service_card.dart │ ├── service_grid.dart │ └── quick_actions.dart ├── domain/ │ ├── entities/system_metrics.dart │ └── repositories/dashboard_repository.dart └── data/ ├── datasources/core_api_dashboard_source.dart └── repositories/dashboard_repository_impl.dart ``` **Core API endpoints used**: - `GET /infrastructure/resources/system` - System metrics - `GET /infrastructure/widget-data` - Services + groups - `GET /health` - Overall health status ### Phase 4: Container Management **Goal**: Full container CRUD and monitoring. **Files to create**: ``` lib/features/containers/ ├── presentation/ │ ├── pages/ │ │ ├── containers_list_page.dart │ │ └── container_detail_page.dart │ ├── widgets/ │ │ ├── container_status_badge.dart │ │ ├── container_logs_viewer.dart │ │ └── container_resource_chart.dart │ └── providers/ │ └── containers_provider.dart ├── domain/ │ ├── entities/container.dart │ ├── repositories/container_repository.dart │ └── usecases/ │ ├── get_containers.dart │ ├── start_container.dart │ ├── stop_container.dart │ ├── restart_container.dart │ └── get_container_logs.dart └── data/ ├── models/container_model.dart ├── datasources/containers_datasource.dart └── repositories/container_repository_impl.dart ``` **Core API endpoints used**: - `GET /infrastructure/containers` - `GET /infrastructure/containers/{id}` - `GET /infrastructure/containers/{id}/logs` - `POST /infrastructure/containers/{id}/{action}` - `GET /infrastructure/resources/containers` ### Phase 5: LLM Chat Interface **Goal**: Custom chat UI for Tatlock agents with SSE streaming. **Files to create**: ``` lib/core/api/ └── sse_client.dart # Platform-aware SSE streaming lib/features/chat/ ├── presentation/ │ ├── pages/chat_page.dart │ └── widgets/ │ ├── message_list.dart │ ├── message_bubble.dart │ ├── reasoning_block.dart # Collapsible think blocks │ ├── streaming_text.dart │ ├── chat_input.dart │ ├── model_selector.dart │ └── agent_indicator.dart # Shows which agent is responding ├── domain/ │ ├── entities/ │ │ ├── message.dart │ │ ├── conversation.dart │ │ └── chat_completion.dart │ └── repositories/chat_repository.dart └── data/ ├── models/ │ ├── chat_completion_request.dart │ ├── chat_completion_response.dart │ └── chat_completion_chunk.dart ├── datasources/tatlock_api_datasource.dart └── repositories/chat_repository_impl.dart ``` **Tatlock API endpoints used**: - `POST /v1/chat/completions` (with `stream: true`) - `GET /v1/models` **SSE Implementation Notes**: - Use platform-conditional imports - Web: XMLHttpRequest or fetch API - Mobile/Desktop: eventsource or http package with stream parsing - Handle `reasoning_content` field for think blocks (DeepSeek R1 format) ### Phase 6: Home Automation **Goal**: Control Home Assistant via Core API. **Files to create**: ``` lib/features/housekeeping/ ├── presentation/ │ ├── pages/ │ │ ├── housekeeping_page.dart │ │ └── area_detail_page.dart │ └── widgets/ │ ├── device_tile.dart │ ├── device_control_sheet.dart │ ├── scene_card.dart │ └── area_grid.dart ├── domain/ │ ├── entities/ │ │ ├── device.dart │ │ ├── area.dart │ │ └── scene.dart │ └── repositories/housekeeping_repository.dart └── data/ ├── models/ │ ├── device_model.dart │ └── device_control_request.dart ├── datasources/housekeeping_datasource.dart └── repositories/housekeeping_repository_impl.dart ``` **Core API endpoints used**: - `GET /housekeeping/devices` - `GET /housekeeping/areas` - `GET /housekeeping/scenes` - `POST /housekeeping/devices/{entity_id}/control` - `POST /housekeeping/scenes/{scene_id}/activate` ### Phase 7: Polish & Deployment **Goal**: Production-ready release. **Tasks**: 1. Flutter web optimization (deferred loading, tree shaking) 2. Desktop platform testing and fixes 3. Offline resilience patterns 4. Error handling and user feedback 5. Performance profiling 6. Docker build and Portainer stack deployment 7. NPM proxy configuration update (home.schweitz.net → tatlock-ui) --- ## DataGrid API Specification ### Configuration Classes ```dart /// Main configuration for a data grid class DataGridConfig { const DataGridConfig({ required this.columns, this.actions = const [], this.bulkActions = const [], this.rowsSelectable = false, this.showHeader = true, this.showFooter = true, this.enableSearch = false, this.searchableColumns = const [], this.defaultSortColumn, this.defaultSortDescending = false, this.emptyStateBuilder, this.loadingBuilder, this.errorBuilder, this.onRowTap, this.cellPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 8), this.headerHeight = 48.0, this.rowHeight, this.dataMode = const DataGridDataMode.all(), }); final List> columns; final List> actions; final List> bulkActions; final bool rowsSelectable; final bool showHeader; final bool showFooter; final bool enableSearch; final List searchableColumns; final int? defaultSortColumn; final bool defaultSortDescending; final Widget Function(BuildContext)? emptyStateBuilder; final Widget Function(BuildContext)? loadingBuilder; final Widget Function(BuildContext, Object error)? errorBuilder; final void Function(T item)? onRowTap; final EdgeInsetsGeometry cellPadding; final double headerHeight; final double? rowHeight; final DataGridDataMode dataMode; } /// Column definition class DataGridColumn { const DataGridColumn({ required this.header, required this.valueBuilder, this.cellBuilder, this.cellControlsBuilder, this.width = const DataGridColumnWidth.flex(1), this.alignment = DataGridColumnAlignment.start, this.sortable = false, this.sortField, this.searchable = false, this.visible = true, this.tooltip, }); final String header; final String Function(T item) valueBuilder; final Widget Function(BuildContext, T item)? cellBuilder; final Widget Function(BuildContext, T item)? cellControlsBuilder; final DataGridColumnWidth width; final DataGridColumnAlignment alignment; final bool sortable; final String? sortField; final bool searchable; final bool visible; final String Function(T item)? tooltip; } /// Column width - sealed class with variants sealed class DataGridColumnWidth { const DataGridColumnWidth._(); const factory DataGridColumnWidth.fixed(double width) = _FixedWidth; const factory DataGridColumnWidth.flex(int flex) = _FlexWidth; const factory DataGridColumnWidth.fraction(double fraction) = _FractionWidth; } /// Per-row action class DataGridAction { const DataGridAction({ required this.icon, required this.label, required this.onTap, this.showWhen, this.destructive = false, this.requiresConfirmation = false, this.confirmationMessage, }); final IconData icon; final String label; final Future Function(T item) onTap; final bool Function(T item)? showWhen; final bool destructive; final bool requiresConfirmation; final String? confirmationMessage; } /// Bulk action on selected rows class DataGridBulkAction { const DataGridBulkAction({ required this.icon, required this.label, required this.onTap, this.minSelected = 1, this.maxSelected, this.destructive = false, this.requiresConfirmation = false, }); final IconData icon; final String label; final Future Function(List items) onTap; final int minSelected; final int? maxSelected; final bool destructive; final bool requiresConfirmation; } /// Data loading mode sealed class DataGridDataMode { const DataGridDataMode._(); const factory DataGridDataMode.all() = _AllDataMode; const factory DataGridDataMode.paginated({int pageSize}) = _PaginatedDataMode; const factory DataGridDataMode.infinite({int initialLoad, int loadMoreThreshold}) = _InfiniteDataMode; } ``` ### Data Source ```dart /// Abstract data source abstract class DataGridSource { Future> fetch({ String? searchQuery, String? sortField, bool sortDescending = false, int? offset, int? limit, }); Future count({String? searchQuery}); } /// Result wrapper class DataGridResult { const DataGridResult({ required this.items, required this.totalCount, this.hasMore = false, }); final List items; final int totalCount; final bool hasMore; } /// Core API adapter class CoreApiDataSource extends DataGridSource { CoreApiDataSource({ required this.endpoint, required this.fromJson, this.searchParam = 'search', this.sortParam = 'sort', this.orderParam = 'order', }); final String endpoint; final T Function(Map) fromJson; final String searchParam; final String sortParam; final String orderParam; } ``` --- ## Web Deployment Architecture ``` Internet → NPM (home.schweitz.net:443) → tatlock-ui container (port 8092) → Flutter web ``` **Dockerfile**: ```dockerfile FROM ghcr.io/cirruslabs/flutter:3.27.0 AS build WORKDIR /app COPY pubspec.* ./ RUN flutter pub get COPY . . RUN flutter build web --release FROM nginx:alpine COPY --from=build /app/build/web /usr/share/nginx/html EXPOSE 80 ``` **Portainer Stack (docker-compose.yml)**: ```yaml version: '3.8' services: tatlock-ui: image: git.schweitz.net/jpmschweitzer/tatlock-ui:latest container_name: tatlock-ui restart: unless-stopped ports: - "8092:80" networks: - docker-dataplane networks: docker-dataplane: external: true ``` **NPM Proxy Configuration**: - Domain: `home.schweitz.net` - Forward to: `localhost:8092` - SSL: Let's Encrypt, Force SSL --- ## Key Dependencies (pubspec.yaml) ```yaml dependencies: flutter: sdk: flutter # State Management flutter_riverpod: ^2.5.0 riverpod_annotation: ^2.3.0 hooks_riverpod: ^2.5.0 flutter_hooks: ^0.20.0 # Code Generation freezed_annotation: ^2.4.0 json_annotation: ^4.8.0 # Networking dio: ^5.4.0 retrofit: ^4.1.0 # Authentication flutter_appauth: ^6.0.0 flutter_secure_storage: ^9.0.0 # Routing go_router: ^13.0.0 # UI flex_color_scheme: ^7.3.0 flutter_adaptive_scaffold: ^0.1.0 flutter_markdown: ^0.6.0 flutter_highlight: ^0.7.0 fl_chart: ^0.65.0 dev_dependencies: build_runner: ^2.4.0 freezed: ^2.4.0 json_serializable: ^6.7.0 riverpod_generator: ^2.4.0 retrofit_generator: ^8.1.0 mocktail: ^1.0.0 ``` --- ## Reference Documents - **Infrastructure**: `/mnt/media/Projects/portainer-core/CONTAINERS.md` - **Tatlock Philosophy**: `/mnt/media/Projects/tatlock/PHILOSOPHY.md` - **Core API**: `http://192.168.86.149:8083/docs` - **Tatlock API**: `http://192.168.86.149:8000/docs` - **fframe ListGrid**: `https://github.com/postmeridiem/fframe/tree/main/fframe/lib/screens/listgrid_screen` - **Current Organizr UI**: `/mnt/media/Projects/tower-ui/uploads/portainer-ui.png` - **Tatlock Logo**: `/mnt/media/Projects/tower-ui/uploads/logo-tatlock.png` --- ## Navigation Structure (matching Organizr) ``` ├── Homepage (Dashboard) ├── Tatlock (Chat) ├── Jellyfin (external link) ├── SearXNG (external link) ├── AMP │ └── AMP Home ├── Coding │ ├── Cloud IDE (external) │ └── Gitea (external) ├── Infrastructure │ ├── Containers (main feature) │ ├── Netdata (external link) │ ├── Portainer (external link) │ └── Proxy Manager (external link) ├── Housekeeping (Home Automation) ├── API Docs (external link to Core API) └── Settings ``` --- ## Authentik Configuration Create application in Authentik admin: - **Name**: Tatlock UI - **Slug**: `tatlock-ui` - **Provider**: OAuth2/OIDC - **Client Type**: Public - **Redirect URIs**: - `https://home.schweitz.net/callback` - `net.schweitz.tatlock://callback` - `http://localhost:*/callback` --- ## Next Steps After Session Resume 1. **Verify folder rename**: Confirm `/mnt/media/Projects/tatlock-ui` exists 2. **Start Phase 0**: Create documentation files 3. **Initialize Flutter**: `flutter create . --project-name tatlock_ui --org net.schweitz --platforms=web,android,ios,macos,linux,windows` 4. **Initialize Git**: `git init && git remote add origin ssh://git@git.schweitz.net:2222/jpmschweitzer/tatlock-ui.git` 5. **Continue with Phase 1**: Core infrastructure setup --- *Last updated: 2024-12-30* *Plan version: 2.0*