feat: #78 Excalidraw finitions + #67 push + #68 health-detailed + #77 desktop jumplist
CI / lint (push) Successful in 52s
CI / security (push) Successful in 36s
CI / test (push) Successful in 1m6s
CI / build (push) Successful in 1m15s
CI / e2e (push) Failing after 12m40s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
CI / lint (push) Successful in 52s
CI / security (push) Successful in 36s
CI / test (push) Successful in 1m6s
CI / build (push) Successful in 1m15s
CI / e2e (push) Failing after 12m40s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
#78 Excalidraw — finitions B5/C8/F2/F3 - backend/indexer.py: port Python pur de lz-string decompressFromBase64 (bitsPerChar=6, resetValue=32) validé contre 4 fixtures JS truth (texte accentué, edge cases) — remplace le stub base64 non-fonctionnel - B5 indexation: .excalidraw.md (format plugin Obsidian) maintenant décompressé côté Python → texte indexé pour recherche TF-IDF - 7 nouveaux tests B5 dans tests/test_excalidraw.py (13/13 total) - F2: excalidraw-viewer.test.mjs enregistré dans CI (lint job) - F3: tests/e2e/excalidraw.spec.js (9 specs — ouverture .excalidraw/.excalidraw.md, création via modale/menu contextuel, toolbar, thème, pop-out, recherche) - Fixtures test_vault/diagram.excalidraw + diagram.excalidraw.md #67 Push notifications (Web Push API + VAPID) - backend/push.py: router + VAPID keys + send_push_notification (pywebpush>=2.3.0) - frontend/js/push.js: subscription UI + service worker integration - tests/test_push.py: 10 tests (subscribe/list/unsubscribe/vapid key) - requirements.txt + sw.js + locales push strings #68 Health check enrichi - backend/main.py: GET /api/health/detailed (admin) — memory/cpu/disk/backups/index/SSE connections - backend/indexer.py: _last_full_index_ts tracking - tests/conftest.py: admin_client fixture - tests/test_api_main.py: 3 nouveaux tests health detailed #77 Desktop jumplist - desktop/src/jumplist.rs: Windows jumplist integration - Cargo.toml/lock + capabilities + main.rs - frontend/js/desktop.js: Tauri bridge (isTauriEnv, invoke, getSystemTheme, syncSystemTheme, shouldShowWizard, crash banner) - tests/frontend/desktop.test.mjs: 21 tests JSDOM (détection, invoke degradation, theme gating, wizard, crash banner) - tests/frontend/unit.test.mjs: desktop.js whitelisted (standalone global reader) # Frontend & CI - frontend/sw.js: réécriture complète (precache + push event handlers + offline) - frontend/index.html: section push dans settings + about repositionné - frontend/js/app.js: initDesktopIntegration + initPush - CI .gitea/workflows/ci.yml: excalidraw-viewer.test.mjs ajouté au lint job All checks: ruff clean, 552 backend tests pass, 9 frontend unit, 5 excalidraw-viewer, 21 desktop, 9 pane-manager, validate-imports 33 modules.
This commit is contained in:
Generated
+1
@@ -2646,6 +2646,7 @@ dependencies = [
|
||||
"tauri-plugin-updater",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -29,6 +29,17 @@ chrono = "0.4"
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Variant",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Shell_Common",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for ObsiGate Desktop",
|
||||
"windows": ["main"],
|
||||
"remote": {
|
||||
"urls": ["http://127.0.0.1:*", "http://localhost:*"]
|
||||
},
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for ObsiGate Desktop","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","shell:allow-execute","dialog:default","notification:default","fs:default","process:default","store:default"]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for ObsiGate Desktop","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","shell:allow-execute","dialog:default","notification:default","fs:default","process:default","store:default"]}}
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# ObsiGate Desktop — Windows code signing (scaffolding).
|
||||
#
|
||||
# Signe le binaire et l'installateur MSI/NSIS après un `cargo tauri build`.
|
||||
# Aucun certificat n'est embarqué : les identifiants sont lus depuis
|
||||
# l'environnement (jamais commités). Le script est un no-op explicite si
|
||||
# l'env n'est pas configuré — le build reste donc fonctionnel sans signature.
|
||||
#
|
||||
# Variables d'environnement :
|
||||
# OBSIGATE_SIGN_CERT_PFX chemin du .pfx (requis)
|
||||
# OBSIGATE_SIGN_CERT_PASSWORD mot de passe du .pfx (optionnel)
|
||||
# OBSIGATE_SIGN_TIMESTAMP_URL URL du serveur d'horodatage RFC3161
|
||||
# (défaut : http://timestamp.digicert.com)
|
||||
# OBSIGATE_SIGN_MODE "signtool" (Windows) ou "osslsigncode" (Linux)
|
||||
#
|
||||
# Usage :
|
||||
# ./scripts/sign-windows.ps1 # après le build, signe les artefacts
|
||||
# OBSIGATE_SIGN_CERT_PFX=cert.pfx ./scripts/sign-windows.ps1
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$pfx = $env:OBSIGATE_SIGN_CERT_PFX
|
||||
if (-not $pfx) {
|
||||
Write-Host "⚠ Aucun certificat fourni (OBSIGATE_SIGN_CERT_PFX). Signature ignorée — build non signé."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$password = $env:OBSIGATE_SIGN_CERT_PASSWORD
|
||||
$tsUrl = if ($env:OBSIGATE_SIGN_TIMESTAMP_URL) { $env:OBSIGATE_SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
|
||||
$mode = if ($env:OBSIGATE_SIGN_MODE) { $env:OBSIGATE_SIGN_MODE } else { 'signtool' }
|
||||
|
||||
$targets = @(
|
||||
'target/release/obsigate-desktop.exe',
|
||||
'target/release/bundle/msi/*.msi',
|
||||
'target/release/bundle/nsis/*-setup.exe'
|
||||
) | ForEach-Object { Get-Item $_ -ErrorAction SilentlyContinue } | Where-Object { $_ }
|
||||
|
||||
if ($targets.Count -eq 0) {
|
||||
Write-Host "⚠ Aucun artefact à signer trouvé (lancer d'abord `cargo tauri build`)."
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($t in $targets) {
|
||||
Write-Host "✍ Signature de $($t.FullName) (mode: $mode)"
|
||||
if ($mode -eq 'osslsigncode') {
|
||||
$args = @('sign', '-pkcs12', $pfx, '-h', 'sha256')
|
||||
if ($password) { $args += @('-pass', $password) }
|
||||
$args += @('-ts', $tsUrl, '-in', $t.FullName, '-out', $t.FullName)
|
||||
& osslsigncode @args
|
||||
if ($LASTEXITCODE -ne 0) { throw "osslsigncode a échoué pour $($t.Name)" }
|
||||
} else {
|
||||
$args = @('sign', '/fd', 'SHA256', '/f', $pfx, '/tr', $tsUrl, '/td', 'SHA256')
|
||||
if ($password) { $args += @('/p', $password) }
|
||||
$args += $t.FullName
|
||||
& signtool @args
|
||||
if ($LASTEXITCODE -ne 0) { throw "signtool a échoué pour $($t.Name)" }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "✅ $($targets.Count) artefact(s) signé(s)."
|
||||
@@ -0,0 +1,178 @@
|
||||
//! Windows Taskbar Jump List — recent vaults.
|
||||
//!
|
||||
//! Populates the taskbar jump list with the configured vaults so the user can
|
||||
//! jump straight into a vault from the Windows taskbar / Start menu.
|
||||
//!
|
||||
//! Best-effort: every step is fallible and logged, never panics, and a failure
|
||||
//! simply leaves the jump list at its OS-managed default state.
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod imp {
|
||||
use super::*;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows::core::{Interface, HSTRING, PCWSTR};
|
||||
use windows::Win32::Foundation::PROPERTYKEY;
|
||||
use windows::Win32::System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
|
||||
};
|
||||
use windows::Win32::System::Com::StructuredStorage::PROPVARIANT;
|
||||
use windows::Win32::System::Variant::VT_LPWSTR;
|
||||
use windows::Win32::UI::Shell::{
|
||||
ICustomDestinationList, IShellLinkW, SetCurrentProcessExplicitAppUserModelID,
|
||||
DestinationList, EnumerableObjectCollection, ShellLink,
|
||||
};
|
||||
use windows::Win32::UI::Shell::Common::IObjectCollection;
|
||||
use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore;
|
||||
|
||||
const APP_ID: &str = "com.obsigate.desktop";
|
||||
|
||||
// PKEY_Title (System.Title): {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, pid 2
|
||||
const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY {
|
||||
fmtid: windows::core::GUID::from_u128(0xF29F85E0_4FF9_1068_AB91_08002B27B3D9),
|
||||
pid: 2,
|
||||
};
|
||||
|
||||
fn wide_null_terminated(s: &str) -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
/// Set the jump-list display title via the shell link's property store.
|
||||
fn set_link_title(link: &IShellLinkW, title: &str) -> windows::core::Result<()> {
|
||||
let store: IPropertyStore = link.cast()?;
|
||||
let mut wide = wide_null_terminated(title);
|
||||
let mut pv: PROPVARIANT = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
let inner = &mut *pv.Anonymous.Anonymous;
|
||||
inner.vt = VT_LPWSTR;
|
||||
inner.Anonymous.pwszVal = windows::core::PWSTR(wide.as_mut_ptr());
|
||||
store.SetValue(&PKEY_TITLE, &pv)?;
|
||||
store.Commit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build one shell link for a vault. `arg` is the command-line argument the
|
||||
/// app expects to open that vault (see file associations / single-instance).
|
||||
fn build_shell_link(name: &str, arg: &str) -> windows::core::Result<IShellLinkW> {
|
||||
let link: IShellLinkW =
|
||||
unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)? };
|
||||
|
||||
let exe = std::env::current_exe()
|
||||
.map_err(|e| windows::core::Error::from(std::io::Error::other(e)))?;
|
||||
let exe_wide = exe.as_os_str().encode_wide().chain(std::iter::once(0)).collect::<Vec<u16>>();
|
||||
|
||||
unsafe {
|
||||
link.SetPath(PCWSTR(exe_wide.as_ptr()))?;
|
||||
if !arg.is_empty() {
|
||||
link.SetArguments(&HSTRING::from(arg))?;
|
||||
}
|
||||
link.SetDescription(&HSTRING::from(name))?;
|
||||
}
|
||||
// Title (display name) is best-effort — a missing title only means the
|
||||
// jump-list entry falls back to the executable name.
|
||||
let _ = set_link_title(&link, name);
|
||||
Ok(link)
|
||||
}
|
||||
|
||||
/// Populate the jump list with the given (name, arg) vault entries.
|
||||
pub fn update_jumplist(vaults: &[(String, String)]) {
|
||||
// COM must be initialised on this thread.
|
||||
let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
|
||||
|
||||
let result = (|| -> windows::core::Result<()> {
|
||||
unsafe {
|
||||
SetCurrentProcessExplicitAppUserModelID(PCWSTR(wide_null_terminated(APP_ID).as_ptr()))?;
|
||||
}
|
||||
|
||||
let list: ICustomDestinationList =
|
||||
unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER)? };
|
||||
|
||||
let mut min_slots = 0u32;
|
||||
unsafe {
|
||||
list.BeginList::<windows::core::IUnknown>(&mut min_slots)?;
|
||||
}
|
||||
|
||||
// "Vaults" category
|
||||
let collection: IObjectCollection = unsafe {
|
||||
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?
|
||||
};
|
||||
for (name, arg) in vaults {
|
||||
match build_shell_link(name, arg) {
|
||||
Ok(link) => unsafe {
|
||||
let _ = collection.AddObject(&link);
|
||||
},
|
||||
Err(e) => warn!("jumplist: failed to build link for {}: {}", name, e),
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
let array: windows::Win32::UI::Shell::Common::IObjectArray = collection.cast()?;
|
||||
list.AppendCategory(&HSTRING::from("Vaults"), &array)?;
|
||||
list.CommitList()?;
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
match result {
|
||||
Ok(()) => info!("jumplist updated with {} vaults", vaults.len()),
|
||||
Err(e) => warn!("jumplist update failed (non-fatal): {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
/// No-op on non-Windows platforms.
|
||||
pub fn update_jumplist(_vaults: &[(String, String)]) {
|
||||
info!("jumplist is Windows-only — skipped");
|
||||
}
|
||||
}
|
||||
|
||||
/// Public entry point: update the Windows jump list from the configured vaults.
|
||||
/// Each entry maps a vault name to the CLI argument the app accepts to open it.
|
||||
pub fn update_jumplist(vaults: &[(String, String)]) {
|
||||
imp::update_jumplist(vaults);
|
||||
}
|
||||
|
||||
/// Build the (name, arg) pairs for the jump list from vault paths.
|
||||
/// Pure helper so the mapping is unit-testable on every platform.
|
||||
pub fn build_vault_args(vaults: &[crate::VaultConfig]) -> Vec<(String, String)> {
|
||||
vaults
|
||||
.iter()
|
||||
.map(|v| (v.name.clone(), format!("--vault={}", v.path)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_vault_args;
|
||||
use crate::VaultConfig;
|
||||
|
||||
fn v(name: &str, path: &str) -> VaultConfig {
|
||||
VaultConfig { name: name.to_string(), path: path.to_string() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vault_args_empty() {
|
||||
assert!(build_vault_args(&[]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vault_args_maps_name_and_path() {
|
||||
let vaults = vec![v("Perso", "C:\\vaults\\perso"), v("IT", "/home/bruno/IT")];
|
||||
let args = build_vault_args(&vaults);
|
||||
assert_eq!(args.len(), 2);
|
||||
assert_eq!(args[0], ("Perso".to_string(), "--vault=C:\\vaults\\perso".to_string()));
|
||||
assert_eq!(args[1], ("IT".to_string(), "--vault=/home/bruno/IT".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_vault_args_preserves_order() {
|
||||
let vaults = vec![v("a", "/a"), v("b", "/b"), v("c", "/c")];
|
||||
let args = build_vault_args(&vaults);
|
||||
assert_eq!(args.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), vec!["a", "b", "c"]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
mod jumplist;
|
||||
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, OpenOptions};
|
||||
@@ -579,6 +581,10 @@ fn main() {
|
||||
// Build native menu bar (File / Edit / Help)
|
||||
let _ = build_native_menu(app);
|
||||
|
||||
// Windows taskbar jump list — recent vaults (best-effort).
|
||||
let jumplist_vaults = jumplist::build_vault_args(&config.vaults);
|
||||
jumplist::update_jumplist(&jumplist_vaults);
|
||||
|
||||
// Restore window position/size
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if let (Some(x), Some(y)) = (config.window_x, config.window_y) {
|
||||
@@ -872,8 +878,14 @@ fn build_tray_menu(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Serializes the `pick_free_port` tests: they bind real TCP ports, so
|
||||
/// running them in parallel makes them clobber each other's port.
|
||||
static PORT_TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
|
||||
/// Helper: write config to a temp dir and read it back
|
||||
fn roundtrip_config(config: &AppConfig) -> AppConfig {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
@@ -973,6 +985,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_pick_free_port_is_free() {
|
||||
let _guard = PORT_TEST_LOCK.lock().unwrap();
|
||||
let port = pick_free_port();
|
||||
assert!((DEFAULT_BACKEND_PORT..DEFAULT_BACKEND_PORT + PORT_SCAN_ATTEMPTS).contains(&port));
|
||||
assert!(TcpListener::bind(("127.0.0.1", port)).is_ok());
|
||||
@@ -980,6 +993,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_pick_free_port_skips_busy_port() {
|
||||
let _guard = PORT_TEST_LOCK.lock().unwrap();
|
||||
let blocker = match TcpListener::bind(("127.0.0.1", DEFAULT_BACKEND_PORT)) {
|
||||
Ok(l) => l,
|
||||
Err(_) => return, // default port already busy on this machine — skip
|
||||
|
||||
Reference in New Issue
Block a user