- 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>
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.pngfor 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 configurationDataGridColumn<T>- Column definitions with buildersDataGridAction<T>- Per-row actionsDataGridBulkAction<T>- Multi-select actionsDataGridSource<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:
- Initialize Flutter project with all platforms
- Initialize git and push to Gitea
- Set up project structure (Clean Architecture folders)
- Configure dependencies (pubspec.yaml)
- Implement core infrastructure:
lib/core/api/api_client.dart- Dio HTTP clientlib/core/api/api_interceptors.dart- Auth, logging, error handlinglib/core/auth/auth_service.dart- Authentik OIDClib/core/auth/auth_provider.dart- Riverpod auth statelib/core/config/app_config.dart- Environment configurationlib/core/config/service_locator.dart- Dependency setuplib/core/theme/app_theme.dart- Material3 themelib/core/theme/theme_provider.dart- Theme state (system + override)
- Create app shell:
lib/app.dart- MaterialApp.router setuplib/routing/app_router.dart- go_router configurationlib/shared/layouts/app_scaffold.dart- Main layout with sidebarlib/shared/layouts/adaptive_navigation.dart- Responsive nav
- Set up CI/CD:
.gitea/workflows/test.yml.gitea/workflows/build-web.ymlDockerfile
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 metricsGET /infrastructure/widget-data- Services + groupsGET /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/containersGET /infrastructure/containers/{id}GET /infrastructure/containers/{id}/logsPOST /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(withstream: 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_contentfield 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/devicesGET /housekeeping/areasGET /housekeeping/scenesPOST /housekeeping/devices/{entity_id}/controlPOST /housekeeping/scenes/{scene_id}/activate
Phase 7: Polish & Deployment
Goal: Production-ready release.
Tasks:
- Flutter web optimization (deferred loading, tree shaking)
- Desktop platform testing and fixes
- Offline resilience patterns
- Error handling and user feedback
- Performance profiling
- Docker build and Portainer stack deployment
- 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/callbacknet.schweitz.tatlock://callbackhttp://localhost:*/callback
Next Steps After Session Resume
- Verify folder rename: Confirm
/mnt/media/Projects/tatlock-uiexists - Start Phase 0: Create documentation files
- Initialize Flutter:
flutter create . --project-name tatlock_ui --org net.schweitz --platforms=web,android,ios,macos,linux,windows - Initialize Git:
git init && git remote add origin ssh://git@git.schweitz.net:2222/jpmschweitzer/tatlock-ui.git - Continue with Phase 1: Core infrastructure setup
Last updated: 2024-12-30 Plan version: 2.0