Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45de2591a7 | ||
|
|
2fe6067085 |
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.0.2] - 2026-01-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ COPY . .
|
|||||||
# Generate code with build_runner
|
# Generate code with build_runner
|
||||||
RUN dart run build_runner build --delete-conflicting-outputs
|
RUN dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# Generate health.json with version info
|
||||||
|
RUN dart run tool/generate_health_json.dart
|
||||||
|
|
||||||
# Build for web release with production configuration
|
# Build for web release with production configuration
|
||||||
# These URLs enable authentication (requiresAuth = true when URL contains schweitz.net)
|
# These URLs enable authentication (requiresAuth = true when URL contains schweitz.net)
|
||||||
RUN flutter build web --release \
|
RUN flutter build web --release \
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class AuthDatasource {
|
|||||||
/// Returns user profile if authenticated via the proxy.
|
/// Returns user profile if authenticated via the proxy.
|
||||||
/// Throws 401 if not authenticated or accessing directly.
|
/// Throws 401 if not authenticated or accessing directly.
|
||||||
Future<AuthSyncResponse> getCurrentUser() async {
|
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!);
|
return AuthSyncResponse.fromJson(response.data!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ part 'auth_provider.g.dart';
|
|||||||
/// - **Web**: NPM forward auth with Authentik (cookies handled by proxy)
|
/// - **Web**: NPM forward auth with Authentik (cookies handled by proxy)
|
||||||
/// - **Mobile**: OIDC Authorization Code flow with flutter_appauth
|
/// - **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.
|
/// via NPM forward auth headers. On mobile, uses OIDC flow then POST /auth/sync.
|
||||||
@riverpod
|
@riverpod
|
||||||
class AuthNotifier extends _$AuthNotifier {
|
class AuthNotifier extends _$AuthNotifier {
|
||||||
@@ -59,7 +59,7 @@ class AuthNotifier extends _$AuthNotifier {
|
|||||||
/// Returns AuthState if authenticated, null if not.
|
/// Returns AuthState if authenticated, null if not.
|
||||||
Future<AuthState?> _tryWebAuth() async {
|
Future<AuthState?> _tryWebAuth() async {
|
||||||
try {
|
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 authDatasource = ref.read(authDatasourceProvider);
|
||||||
final syncResponse = await authDatasource.getCurrentUser();
|
final syncResponse = await authDatasource.getCurrentUser();
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ class AuthNotifier extends _$AuthNotifier {
|
|||||||
// User needs to access via the authenticated proxy URL
|
// User needs to access via the authenticated proxy URL
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
developer.log('Web sign-in: user should access via authenticated proxy', name: 'auth');
|
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();
|
state = const AsyncLoading();
|
||||||
final webAuth = await _tryWebAuth();
|
final webAuth = await _tryWebAuth();
|
||||||
if (webAuth != null) {
|
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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.2+1
|
version: 1.0.4+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.4
|
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>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<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>
|
</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>
|
</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