Compare commits

..
4 Commits
Author SHA1 Message Date
Jeroen SchweitzerandClaude Opus 4.5 8b3bff7df0 chore: release v1.1.9
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m0s
Move health check to /health directory for NPM forward auth exclusion

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 18:54:17 +01:00
Jeroen SchweitzerandClaude Opus 4.5 2b2ddc1b1b docs: simplify release steps - CI auto-triggers on v* tag push
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 18:26:54 +01:00
Jeroen SchweitzerandClaude Opus 4.5 0d5986b81a chore: release v1.1.8
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m10s
Dark background on web/index.html to prevent white flash during auth redirects

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 18:19:10 +01:00
Jeroen SchweitzerandClaude Opus 4.5 31e3306997 chore: release v1.1.7
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 2m58s
Remove callback route - AuthController handles it before app starts:
- Removed /callback route from Flutter router
- Removed _OidcCallbackPage widget
- Auth is now invisible - no Flutter UI during auth flow

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:41:43 +01:00
8 changed files with 42 additions and 171 deletions
+2 -4
View File
@@ -59,10 +59,8 @@ This project uses version-tag-based CI/CD. Releases trigger automated Docker bui
3. Commit changes: `git commit -m "chore: release vX.X.X"`
4. Create git tag: `git tag vX.X.X`
5. Push with tags: `git push origin master --tags`
6. Create release in Gitea UI (git.schweitz.net → Releases → New Release)
* Select the tag
* Add release notes (can copy from CHANGELOG)
* **Publish** the release (this triggers CI/CD)
CI/CD auto-triggers when a tag starting with `v` is pushed.
**What happens on release:**
+17
View File
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.1.9] - 2026-01-04
### Changed
- Moved health check to `/health` directory - URL is now `/health` instead of `/health.html`
- Enables NPM forward auth path exclusion for health endpoint
## [1.1.8] - 2026-01-04
### Changed
- Dark background (`#1a1a2e`) on web/index.html to prevent white flash during auth redirects
## [1.1.7] - 2026-01-04
### Removed
- Removed `/callback` route from Flutter router - AuthController handles callback in main() before app starts
- Removed `_OidcCallbackPage` widget - no visible auth UI needed
## [1.1.6] - 2026-01-04
### Changed
-159
View File
@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:tatlock_ui/core/auth/auth_provider.dart';
import 'package:tatlock_ui/features/control_room/router.dart';
import 'package:tatlock_ui/features/front_hall/presentation/pages/front_hall_page.dart';
import 'package:tatlock_ui/features/security/router.dart';
@@ -15,7 +13,6 @@ abstract class AppRoutes {
static const frontHall = '/';
static const parlor = '/parlor';
static const settings = '/settings';
static const callback = '/callback';
}
/// Provides the GoRouter instance.
@@ -25,17 +22,6 @@ GoRouter appRouter(Ref ref) {
initialLocation: AppRoutes.frontHall,
debugLogDiagnostics: true,
routes: [
// OIDC callback route (handles auth code exchange)
GoRoute(
path: AppRoutes.callback,
name: 'callback',
builder: (context, state) => _OidcCallbackPage(
code: state.uri.queryParameters['code'],
callbackState: state.uri.queryParameters['state'],
error: state.uri.queryParameters['error'],
errorDescription: state.uri.queryParameters['error_description'],
),
),
// Main app routes (inside shell with app scaffold)
ShellRoute(
builder: (context, state, child) => AppScaffold(child: child),
@@ -100,148 +86,3 @@ class _PlaceholderPage extends StatelessWidget {
}
}
/// OIDC callback page that handles the authorization code exchange.
class _OidcCallbackPage extends ConsumerStatefulWidget {
const _OidcCallbackPage({
this.code,
this.callbackState,
this.error,
this.errorDescription,
});
final String? code;
final String? callbackState;
final String? error;
final String? errorDescription;
@override
ConsumerState<_OidcCallbackPage> createState() => _OidcCallbackPageState();
}
class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> {
bool _isProcessing = true;
String? _error;
@override
void initState() {
super.initState();
// Defer callback processing to avoid Riverpod state modification during build
WidgetsBinding.instance.addPostFrameCallback((_) {
_processCallback();
});
}
Future<void> _processCallback() async {
// Check mounted before any async work
if (!mounted) return;
// Check for error from Authentik
if (widget.error != null) {
// Silent OIDC (prompt=none) failed - no existing session
// Fall back to regular OIDC flow to show login UI
if (widget.error == 'login_required') {
ref.read(authProvider.notifier).signIn();
return;
}
setState(() {
_isProcessing = false;
_error = widget.errorDescription ?? widget.error;
});
return;
}
// Check for required parameters
if (widget.code == null || widget.callbackState == null) {
setState(() {
_isProcessing = false;
_error = 'Invalid callback - missing code or state parameter';
});
return;
}
// Get notifier reference before async gap to avoid disposed ref errors
final authNotifier = ref.read(authProvider.notifier);
// Exchange code for tokens
try {
await authNotifier.handleOidcCallback(
widget.code!,
widget.callbackState!,
);
// Navigate to home on success
if (mounted) {
context.go(AppRoutes.frontHall);
}
} catch (e) {
if (mounted) {
setState(() {
_isProcessing = false;
_error = e.toString();
});
}
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_error != null ? Icons.error_outline : Icons.home_work_outlined,
size: 64,
color: _error != null ? colorScheme.error : colorScheme.primary,
),
const SizedBox(height: 24),
Text(
_error != null ? 'Authentication Failed' : 'Signing in...',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
if (_isProcessing)
const CircularProgressIndicator()
else if (_error != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_error!,
style: TextStyle(color: colorScheme.onErrorContainer),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () => context.go(AppRoutes.frontHall),
icon: const Icon(Icons.refresh),
label: const Text('Try again'),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
),
],
],
),
),
),
),
),
);
}
}
+1 -1
View File
@@ -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.1.6+1
version: 1.1.9+1
environment:
sdk: ^3.10.4
+6 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env dart
// Generates web/health.json from pubspec.yaml
// Generates web/health/health.json from pubspec.yaml
// Run: dart run tool/generate_health_json.dart
// ignore_for_file: avoid_print
@@ -37,15 +37,15 @@ void main() {
'fullVersion': '$version+$buildNumber',
};
final webDir = Directory('web');
if (!webDir.existsSync()) {
webDir.createSync(recursive: true);
final healthDir = Directory('web/health');
if (!healthDir.existsSync()) {
healthDir.createSync(recursive: true);
}
final healthFile = File('web/health.json');
final healthFile = File('web/health/health.json');
healthFile.writeAsStringSync(
const JsonEncoder.withIndent(' ').convert(health),
);
print('Generated web/health.json with version $version+$buildNumber');
print('Generated web/health/health.json with version $version+$buildNumber');
}
+8
View File
@@ -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"
}
+1 -1
View File
@@ -13,7 +13,7 @@
<h1 id="status">Loading...</h1>
<pre id="data"></pre>
<script>
fetch('/health.json')
fetch('./health.json')
.then(r => r.json())
.then(data => {
document.getElementById('status').textContent = data.status?.toUpperCase() || 'OK';
+7
View File
@@ -32,6 +32,13 @@
<title>Tatlock</title>
<link rel="manifest" href="manifest.json">
<style>
body {
margin: 0;
padding: 0;
background-color: #1a1a2e;
}
</style>
</head>
<body>
<script src="flutter_bootstrap.js" async></script>