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

699 lines
23 KiB
Markdown

# 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
```dart
/// 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
```dart
/// 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**:
```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
- **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](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*