Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45de2591a7 | ||
|
|
2fe6067085 | ||
|
|
ce2dfcd13c |
@@ -22,14 +22,20 @@ This document contains instructions and documentation references for AI assistan
|
||||
* Related repos: , `core-api`, `tatlock`, `library-desk`, `scheduler`, `portainer-core`
|
||||
|
||||
### 🐳 Deployment & Infrastructure
|
||||
|
||||
**⚠️ IMPORTANT: Service Port Reference**
|
||||
| Service | LAN Port | External URL | Notes |
|
||||
|---------|----------|--------------|-------|
|
||||
| **Core API** | 8083 | `api.schweitz.net` | FastAPI backend for this UI |
|
||||
| **Tatlock API** | 8000 | `tatlock.schweitz.net` | Legacy Python API (Ollama proxy) |
|
||||
| **Tatlock UI** | 9999 | `home.schweitz.net` | This Flutter app |
|
||||
|
||||
* **Full stack documentation**: Available in the `portainer-core` repo
|
||||
* Access: `curl http://localhost:3002/jpmschweitzer/portainer-core/raw/branch/main/CONTAINERS.md`
|
||||
* Access: `curl http://192.168.86.149:3002/jpmschweitzer/portainer-core/raw/branch/main/CONTAINERS.md`
|
||||
* Contains: All service ports, URLs, Redis DB allocations, external domains
|
||||
* **Tatlock deployment**:
|
||||
* LAN: `http://192.168.86.149:8000`
|
||||
* External: `tatlock.schweitz.net` (behind Authentik SSO)
|
||||
* Redis DBs: 1 (memory), 6 (benchmarks)
|
||||
* **Health check**: `curl http://192.168.86.149:8000/health`
|
||||
* **Health checks**:
|
||||
* Core API: `curl http://192.168.86.149:8083/health`
|
||||
* Tatlock API: `curl http://192.168.86.149:8000/health`
|
||||
|
||||
### 🛡️ Git Discipline
|
||||
* **Commit Messages:** Use the [Conventional Commits](https://www.conventionalcommits.org/) format.
|
||||
|
||||
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.4] - 2026-01-03
|
||||
|
||||
### Added
|
||||
- `health.json` generated at build time with app version info
|
||||
- `health.html` now displays version, title, and status from health.json
|
||||
|
||||
## [1.0.3] - 2026-01-03
|
||||
|
||||
### Fixed
|
||||
- Fixed auth endpoint path: `/auth/me` → `/auth/users/me`
|
||||
|
||||
## [1.0.2] - 2026-01-03
|
||||
|
||||
### Fixed
|
||||
- Production Docker build now uses correct API URLs
|
||||
- Added `--dart-define` flags for `CORE_API_URL` and `TATLOCK_API_URL`
|
||||
- This enables `requiresAuth=true` so authentication is actually triggered
|
||||
- Updated AGENTS.md with clear service port reference table
|
||||
|
||||
## [1.0.1] - 2026-01-03
|
||||
|
||||
### Fixed
|
||||
|
||||
+8
-2
@@ -15,8 +15,14 @@ COPY . .
|
||||
# Generate code with build_runner
|
||||
RUN dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Build for web release
|
||||
RUN flutter build web --release
|
||||
# Generate health.json with version info
|
||||
RUN dart run tool/generate_health_json.dart
|
||||
|
||||
# Build for web release with production configuration
|
||||
# These URLs enable authentication (requiresAuth = true when URL contains schweitz.net)
|
||||
RUN flutter build web --release \
|
||||
--dart-define=CORE_API_URL=https://api.schweitz.net \
|
||||
--dart-define=TATLOCK_API_URL=https://tatlock.schweitz.net
|
||||
|
||||
# Stage 2: Serve with nginx
|
||||
FROM nginx:alpine
|
||||
|
||||
@@ -102,7 +102,7 @@ class AuthDatasource {
|
||||
/// Returns user profile if authenticated via the proxy.
|
||||
/// Throws 401 if not authenticated or accessing directly.
|
||||
Future<AuthSyncResponse> getCurrentUser() async {
|
||||
final response = await _dio.get<Map<String, dynamic>>('/auth/me');
|
||||
final response = await _dio.get<Map<String, dynamic>>('/auth/users/me');
|
||||
return AuthSyncResponse.fromJson(response.data!);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ part 'auth_provider.g.dart';
|
||||
/// - **Web**: NPM forward auth with Authentik (cookies handled by proxy)
|
||||
/// - **Mobile**: OIDC Authorization Code flow with flutter_appauth
|
||||
///
|
||||
/// On web, the app calls GET /auth/me to check if user is authenticated
|
||||
/// On web, the app calls GET /auth/users/me to check if user is authenticated
|
||||
/// via NPM forward auth headers. On mobile, uses OIDC flow then POST /auth/sync.
|
||||
@riverpod
|
||||
class AuthNotifier extends _$AuthNotifier {
|
||||
@@ -59,7 +59,7 @@ class AuthNotifier extends _$AuthNotifier {
|
||||
/// Returns AuthState if authenticated, null if not.
|
||||
Future<AuthState?> _tryWebAuth() async {
|
||||
try {
|
||||
developer.log('Attempting web auth via /auth/me', name: 'auth');
|
||||
developer.log('Attempting web auth via /auth/users/me', name: 'auth');
|
||||
final authDatasource = ref.read(authDatasourceProvider);
|
||||
final syncResponse = await authDatasource.getCurrentUser();
|
||||
|
||||
@@ -238,7 +238,7 @@ class AuthNotifier extends _$AuthNotifier {
|
||||
// User needs to access via the authenticated proxy URL
|
||||
if (kIsWeb) {
|
||||
developer.log('Web sign-in: user should access via authenticated proxy', name: 'auth');
|
||||
// Try to refresh auth state from /auth/me
|
||||
// Try to refresh auth state from /auth/users/me
|
||||
state = const AsyncLoading();
|
||||
final webAuth = await _tryWebAuth();
|
||||
if (webAuth != null) {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.1+1
|
||||
version: 1.0.4+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.4
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env dart
|
||||
// Generates web/health.json from pubspec.yaml
|
||||
// Run: dart run tool/generate_health_json.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void main() {
|
||||
final pubspecFile = File('pubspec.yaml');
|
||||
if (!pubspecFile.existsSync()) {
|
||||
stderr.writeln('Error: pubspec.yaml not found');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final pubspecContent = pubspecFile.readAsStringSync();
|
||||
final pubspec = loadYaml(pubspecContent) as YamlMap;
|
||||
|
||||
final name = pubspec['name'] as String;
|
||||
final description = pubspec['description'] as String? ?? '';
|
||||
final versionString = pubspec['version'] as String;
|
||||
|
||||
// Parse version: "1.0.3+1" -> version="1.0.3", buildNumber=1
|
||||
final versionParts = versionString.split('+');
|
||||
final version = versionParts[0];
|
||||
final buildNumber = versionParts.length > 1 ? int.parse(versionParts[1]) : 0;
|
||||
|
||||
final health = {
|
||||
'status': 'healthy',
|
||||
'name': name,
|
||||
'title': description,
|
||||
'version': version,
|
||||
'buildNumber': buildNumber,
|
||||
'fullVersion': '$version+$buildNumber',
|
||||
};
|
||||
|
||||
final webDir = Directory('web');
|
||||
if (!webDir.existsSync()) {
|
||||
webDir.createSync(recursive: true);
|
||||
}
|
||||
|
||||
final healthFile = File('web/health.json');
|
||||
healthFile.writeAsStringSync(
|
||||
const JsonEncoder.withIndent(' ').convert(health),
|
||||
);
|
||||
|
||||
print('Generated web/health.json with version $version+$buildNumber');
|
||||
}
|
||||
+25
-2
@@ -1,7 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OK</title>
|
||||
<title>Health Check</title>
|
||||
<style>
|
||||
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
|
||||
.healthy { color: #0f0; }
|
||||
.error { color: #f00; }
|
||||
pre { background: #222; padding: 15px; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>OK</body>
|
||||
<body>
|
||||
<h1 id="status">Loading...</h1>
|
||||
<pre id="data"></pre>
|
||||
<script>
|
||||
fetch('/health.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
document.getElementById('status').textContent = data.status?.toUpperCase() || 'OK';
|
||||
document.getElementById('status').className = data.status === 'healthy' ? 'healthy' : 'error';
|
||||
document.getElementById('data').textContent = JSON.stringify(data, null, 2);
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('status').textContent = 'ERROR';
|
||||
document.getElementById('status').className = 'error';
|
||||
document.getElementById('data').textContent = err.message;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"status": "healthy",
|
||||
"name": "tatlock_ui",
|
||||
"title": "Tatlock - a Home Lab AI",
|
||||
"version": "1.0.4",
|
||||
"buildNumber": 1,
|
||||
"fullVersion": "1.0.4+1"
|
||||
}
|
||||
Reference in New Issue
Block a user