Files
tatlock-ui/PLAN.md
T
jpmschweitzerandClaude Opus 4.5 a4aaea33b8 refactor: rename features to room-based directory structure
- Rename features/dashboard → features/front_hall
- Update routes: /, /control-room, /parlor, /settings
- Update navigation to 4 rooms (removed chat tab - will be omnipresent dock)
- Add docs/UI_LAYOUT.md with wireframes and responsive specs
- Update ARCHITECTURE.md and PLAN.md to reflect room structure

Directory structure now mirrors "Rooms of the Estate" UI navigation:
- front_hall/ (dashboard)
- control_room/ (infrastructure - containers, stacks, etc.)
- parlor/ (home automation)
- library/ (future)
- study/ (future, hidden)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 23:14:40 +01:00

23 KiB

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<T> - Declarative configuration
  • DataGridColumn<T> - Column definitions with builders
  • DataGridAction<T> - Per-row actions
  • DataGridBulkAction<T> - Multi-select actions
  • DataGridSource<T> - Abstract data source (Core API adapter)
  • DataGridNotifier<T> - 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
│   └── UI_LAYOUT.md             # Wireframes, responsive breakpoints, room navigation

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: Front Hall (Organizr Replacement)

Goal: Replace Organizr homepage with Tatlock UI dashboard.

Features to match (from portainer-ui.png):

  • System metrics gauges (CPU, Memory, Disk, Containers)
  • Service quick links grid
  • Recent activity timeline
  • Chat dock expanded by default

Files to create:

lib/features/front_hall/
├── presentation/
│   ├── pages/front_hall_page.dart
│   └── widgets/
│       ├── stat_card.dart
│       ├── service_card.dart
│       ├── service_grid.dart
│       └── activity_timeline.dart
├── domain/
│   ├── entities/system_metrics.dart
│   └── repositories/front_hall_repository.dart
└── data/
    ├── datasources/core_api_front_hall_source.dart
    └── repositories/front_hall_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: Control Room - Container Management

Goal: Full container CRUD and monitoring.

Files to create:

lib/features/control_room/
├── shared/                           # Shared within Control Room
│   └── widgets/
│       └── resource_chart.dart
├── containers/
│   ├── presentation/
│   │   ├── pages/
│   │   │   ├── containers_list_page.dart
│   │   │   └── container_detail_page.dart
│   │   ├── widgets/
│   │   │   ├── container_status_badge.dart
│   │   │   └── container_logs_viewer.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
├── stacks/                           # Future
├── networks/                         # Future
└── volumes/                          # Future

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: Chat Dock (Omnipresent)

Goal: Custom chat UI for Tatlock agents with SSE streaming. Injected at layout level, not a room.

Future requirement: Expand-to-fullscreen option as secondary UI overlay.

Files to create:

lib/core/api/
└── sse_client.dart                   # Platform-aware SSE streaming

lib/chat/                             # Top-level, not under features/
├── presentation/
│   ├── chat_dock.dart                # Collapsible dock widget
│   ├── chat_fullscreen.dart          # Fullscreen overlay (future)
│   └── 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: Parlor (Home Automation)

Goal: Control Home Assistant via Core API.

Files to create:

lib/features/parlor/
├── presentation/
│   ├── pages/
│   │   ├── parlor_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/parlor_repository.dart
└── data/
    ├── models/
    │   ├── device_model.dart
    │   └── device_control_request.dart
    ├── datasources/parlor_datasource.dart
    └── repositories/parlor_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

/// Main configuration for a data grid
class DataGridConfig<T> {
  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<DataGridColumn<T>> columns;
  final List<DataGridAction<T>> actions;
  final List<DataGridBulkAction<T>> bulkActions;
  final bool rowsSelectable;
  final bool showHeader;
  final bool showFooter;
  final bool enableSearch;
  final List<int> 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<T> {
  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<T> {
  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<void> 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<T> {
  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<void> Function(List<T> 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

/// Abstract data source
abstract class DataGridSource<T> {
  Future<DataGridResult<T>> fetch({
    String? searchQuery,
    String? sortField,
    bool sortDescending = false,
    int? offset,
    int? limit,
  });

  Future<int> count({String? searchQuery});
}

/// Result wrapper
class DataGridResult<T> {
  const DataGridResult({
    required this.items,
    required this.totalCount,
    this.hasMore = false,
  });

  final List<T> items;
  final int totalCount;
  final bool hasMore;
}

/// Core API adapter
class CoreApiDataSource<T> extends DataGridSource<T> {
  CoreApiDataSource({
    required this.endpoint,
    required this.fromJson,
    this.searchParam = 'search',
    this.sortParam = 'sort',
    this.orderParam = 'order',
  });

  final String endpoint;
  final T Function(Map<String, dynamic>) 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:

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):

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)

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

  • UI Layout Spec: docs/UI_LAYOUT.md - Wireframes, breakpoints, responsive behavior
  • 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 ("Rooms of the Estate")

See docs/UI_LAYOUT.md for detailed wireframes and responsive specifications.

The UI uses tabbed room navigation in the header rather than a traditional sidebar. Each room is a focused context with its own layout. The Tatlock chat assistant is omnipresent as a collapsible right dock.

Header Tabs:
┌─────────────────────────────────────────────────────────────────┐
│ Front Hall │ Control Room │ Parlor │ Library │                  │
└─────────────────────────────────────────────────────────────────┘

Room Contents:
├── Front Hall (Dashboard)
│   ├── Stat cards (CPU, Memory, Disk, Containers)
│   ├── Quick access service grid (external links)
│   ├── Recent activity timeline
│   └── Chat dock EXPANDED by default
│
├── Control Room (Infrastructure)
│   ├── Context sidebar: Containers, Stacks, Networks, Volumes, Images
│   ├── External links: Netdata, Portainer, NPM
│   ├── DataGrid views for each section
│   └── Chat dock collapsed
│
├── Parlor (Housekeeping)
│   ├── Context sidebar: Areas, Scenes
│   ├── Device control tiles per area
│   ├── Scene activation buttons
│   └── Chat dock collapsed
│
├── Library (Future)
│   ├── Knowledge management
│   ├── Bookmarks, notes, docs
│   └── Chat dock collapsed
│
└── Study (Hidden - Future)
    └── Secretarial: email, calendar

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