feat(06-02): add native desktop features — tray, window-state, autostart, version check
- Add GET /health/version public endpoint to API - Extend Tauri with 4 plugins: notification, autostart, window-state, store - Implement close-to-tray with prevent_close + prevent_exit (Pitfall 2) - Tray menu with Oeffnen/Beenden (German labels) - Async startup version check against server /health/version - Generate Tessera-branded icons (yellow T on dark bg, OKLCH palette) - Update capabilities for notification, autostart, window-state permissions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,8 +49,9 @@ completed: 2026-06-25
|
|||||||
- **Duration:** 3 min
|
- **Duration:** 3 min
|
||||||
- **Started:** 2026-06-25T08:56:13Z
|
- **Started:** 2026-06-25T08:56:13Z
|
||||||
- **Completed:** 2026-06-25T08:59:12Z
|
- **Completed:** 2026-06-25T08:59:12Z
|
||||||
- **Tasks:** 2 completed, 1 awaiting human verification (Task 4)
|
- **Tasks:** 4 completed (all)
|
||||||
- **Files created:** 3
|
- **Files created:** 3
|
||||||
|
- **CI Fixes:** 3 (corepack, prisma conditional postinstall, .gitkeep)
|
||||||
|
|
||||||
## Accomplishments
|
## Accomplishments
|
||||||
|
|
||||||
@@ -65,7 +66,10 @@ Each task was committed atomically:
|
|||||||
1. **Task 1: Set up Gitea remote and register act_runner** -- completed prior to this execution (checkpoint:human-action)
|
1. **Task 1: Set up Gitea remote and register act_runner** -- completed prior to this execution (checkpoint:human-action)
|
||||||
2. **Task 2: Define act_runner service and CI/CD setup runbook** -- `c0e3293` (feat)
|
2. **Task 2: Define act_runner service and CI/CD setup runbook** -- `c0e3293` (feat)
|
||||||
3. **Task 3: Create .gitea/workflows/ci.yml multi-stage pipeline** -- `756925b` (feat)
|
3. **Task 3: Create .gitea/workflows/ci.yml multi-stage pipeline** -- `756925b` (feat)
|
||||||
4. **Task 4: Trigger the pipeline with a real push** -- checkpoint:human-verify (awaiting)
|
4. **Task 4: Trigger the pipeline with a real push** -- Runs #85-89, green on Run #89 (`4d94a25`)
|
||||||
|
- Fix: corepack statt pnpm/action-setup (`e5d45e1`)
|
||||||
|
- Fix: prisma postinstall conditional (`c48e61f`)
|
||||||
|
- Fix: .gitkeep in apps/web/public (`4d94a25`)
|
||||||
|
|
||||||
## Files Created
|
## Files Created
|
||||||
|
|
||||||
@@ -96,9 +100,8 @@ No new threat surfaces introduced beyond those documented in the plan's threat m
|
|||||||
|
|
||||||
## Next Phase Readiness
|
## Next Phase Readiness
|
||||||
|
|
||||||
- Pipeline ready for first real push (Task 4 checkpoint pending human verification)
|
- Pipeline verified green (Run #89) — INFRA-04 fully validated
|
||||||
- After push verification, INFRA-04 will be fully validated
|
- All future pushes to main automatically lint, type-check, test, and redeploy
|
||||||
- All future pushes to main will automatically run lint, type-check, tests, and redeploy
|
|
||||||
|
|
||||||
## Self-Check: PASSED
|
## Self-Check: PASSED
|
||||||
|
|
||||||
@@ -107,4 +110,4 @@ No new threat surfaces introduced beyond those documented in the plan's threat m
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Phase: 06-desktop-client-ci-cd*
|
*Phase: 06-desktop-client-ci-cd*
|
||||||
*Completed: 2026-06-25 (Tasks 2-3; Task 4 pending)*
|
*Completed: 2026-06-25 (all tasks done, pipeline green)*
|
||||||
|
|||||||
@@ -12,4 +12,13 @@ export class HealthController {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('version')
|
||||||
|
getVersion() {
|
||||||
|
return {
|
||||||
|
version: process.env.npm_package_version ?? '0.0.1',
|
||||||
|
name: 'tessera',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1019
-14
File diff suppressed because it is too large
Load Diff
@@ -13,5 +13,9 @@ tauri-build = { version = "2", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["tray-icon"] }
|
tauri = { version = "2", features = ["tray-icon"] }
|
||||||
tauri-plugin-store = "2"
|
tauri-plugin-store = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
tauri-plugin-autostart = "2"
|
||||||
|
tauri-plugin-window-state = "2"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"store:default"
|
"store:default",
|
||||||
|
"notification:default",
|
||||||
|
"notification:allow-is-permission-granted",
|
||||||
|
"notification:allow-request-permission",
|
||||||
|
"notification:allow-notify",
|
||||||
|
"autostart:allow-enable",
|
||||||
|
"autostart:allow-disable",
|
||||||
|
"autostart:allow-is-enabled",
|
||||||
|
"window-state:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
use tauri::Manager;
|
use tauri::{
|
||||||
|
Manager,
|
||||||
|
menu::{MenuBuilder, MenuItemBuilder},
|
||||||
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
|
RunEvent, WindowEvent,
|
||||||
|
};
|
||||||
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
use tauri_plugin_notification::NotificationExt;
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
/// Main entry point for the Tessera desktop application.
|
#[derive(serde::Deserialize)]
|
||||||
///
|
struct VersionResponse {
|
||||||
/// Registers plugins, checks for a stored server URL on startup,
|
version: String,
|
||||||
/// and either navigates to the configured server or shows the
|
}
|
||||||
/// bundled setup page for first-run configuration.
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
let app = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||||
|
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Attempt to read a previously stored server URL
|
|
||||||
let store = app.store("config.json")?;
|
let store = app.store("config.json")?;
|
||||||
let server_url = store.get("server_url");
|
let server_url = store.get("server_url");
|
||||||
|
|
||||||
|
let mut url_for_check: Option<String> = None;
|
||||||
|
|
||||||
if let Some(url_value) = server_url {
|
if let Some(url_value) = server_url {
|
||||||
if let Some(url_str) = url_value.as_str() {
|
if let Some(url_str) = url_value.as_str() {
|
||||||
if !url_str.is_empty() {
|
if !url_str.is_empty() {
|
||||||
// Navigate the main window to the configured server URL
|
url_for_check = Some(url_str.to_string());
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
if let Ok(parsed) = url_str.parse::<tauri::Url>() {
|
if let Ok(parsed) = url_str.parse::<tauri::Url>() {
|
||||||
let _ = window.navigate(parsed);
|
let _ = window.navigate(parsed);
|
||||||
@@ -26,11 +37,83 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If no server_url is stored, the WebView remains on the
|
|
||||||
// bundled setup page (frontendDist = ../src/setup.html)
|
// Tray menu
|
||||||
|
let open = MenuItemBuilder::with_id("open", "Oeffnen").build(app)?;
|
||||||
|
let quit = MenuItemBuilder::with_id("quit", "Beenden").build(app)?;
|
||||||
|
let menu = MenuBuilder::new(app)
|
||||||
|
.item(&open)
|
||||||
|
.separator()
|
||||||
|
.item(&quit)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let _tray = TrayIconBuilder::new()
|
||||||
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
|
.menu(&menu)
|
||||||
|
.tooltip("Tessera")
|
||||||
|
.show_menu_on_left_click(false)
|
||||||
|
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||||
|
"open" => {
|
||||||
|
if let Some(w) = app.get_webview_window("main") {
|
||||||
|
let _ = w.show();
|
||||||
|
let _ = w.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"quit" => {
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if let TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
if let Some(w) = tray.app_handle().get_webview_window("main") {
|
||||||
|
let _ = w.show();
|
||||||
|
let _ = w.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(app)?;
|
||||||
|
|
||||||
|
// Version check (async, non-blocking)
|
||||||
|
if let Some(server_url) = url_for_check {
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
let app_version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let url = format!("{}/health/version", server_url.trim_end_matches('/'));
|
||||||
|
if let Ok(resp) = reqwest::get(&url).await {
|
||||||
|
if let Ok(info) = resp.json::<VersionResponse>().await {
|
||||||
|
if info.version != app_version {
|
||||||
|
let _ = app_handle
|
||||||
|
.notification()
|
||||||
|
.builder()
|
||||||
|
.title("Tessera Update")
|
||||||
|
.body("Eine neue Version ist verfuegbar.")
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.run(tauri::generate_context!())
|
.on_window_event(|window, event| {
|
||||||
.expect("error while running Tessera desktop application");
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
let _ = window.hide();
|
||||||
|
api.prevent_close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("error while building Tessera desktop application");
|
||||||
|
|
||||||
|
app.run(|_app, event| {
|
||||||
|
if let RunEvent::ExitRequested { api, .. } = event {
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user