This commit is contained in:
2026-06-11 10:43:30 -04:00
parent 336e6a264c
commit 4f92942ccb
9 changed files with 175 additions and 24 deletions
+120 -4
View File
@@ -1,7 +1,123 @@
# Tauri + Vanilla # WVU Medicine Help Alert Web Client
This template should help get you started developing with Tauri in vanilla HTML, CSS and Javascript. A lightweight Tauri-based desktop wrapper for the WVU Medicine PinPoint HelpAlert duress alarm system. Built to replace the vendor's crashing desktop client with a stable, tray-resident application that brings alerts to the foreground automatically.
## Recommended IDE Setup ## Why This Exists
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) The vendor's native desktop client crashes hospital workstations. The web version works but has two problems: users get logged out due to inactivity, and browser notifications don't reliably surface when the tab is backgrounded. This app solves both by wrapping the web client in a persistent Tauri window with native alert detection.
## How It Works
1. The app loads PinPoint's web login page in a native webview
2. After login, the user clicks "Alerts" to navigate to the HelpAlert GWT page
3. Injected JavaScript monitors the DOM every 2 seconds for new alerts (identified by "Claim" buttons and "Elapsed Time" markers)
4. When a new alert is detected, the app calls into the Rust backend which unminimizes, focuses, and temporarily pins the window on top
5. Closing the window minimizes to the system tray instead of exiting — the app stays running and monitoring
## Architecture
- **Frontend:** Vendor's HelpAlert web app loaded directly in the Tauri webview, no custom UI
- **Injection:** `src/inject.js` handles `window.open()` interception (alerts page opens in same window instead of a new tab) and DOM-based alert detection
- **Backend:** Rust/Tauri handles system tray, window management, and the `alert_detected` command that brings the window to the foreground
- **IPC:** JavaScript detects alerts → invokes Tauri command → Rust brings window to front
## Prerequisites
- Rust toolchain (`rustup`)
- Tauri CLI: `cargo install tauri-cli --version "^2"`
- For Windows cross-compilation from Linux: `x86_64-pc-windows-gnu` target
- WiX Toolset (for MSI installer generation)
## WRY Fork Requirement
This project requires a forked version of [wry](https://github.com/tauri-apps/wry) (Tauri's WebView library). The vendor's HelpAlert page uses GWT (Google Web Toolkit), which declares a global `ipc` variable. WRY also declares `window.ipc` for its IPC bridge, causing a collision that crashes the GWT page.
The fork renames WRY's `ipc` to `__wry_ipc` in `src/webview2/mod.rs` (line 897), resolving the collision.
The fork is referenced in `src-tauri/Cargo.toml`:
```toml
[patch.crates-io]
wry = { path = "../../wry" }
```
Adjust the path to match your local wry fork location.
## Building
### Development
```bash
cargo tauri dev
```
### Release (Windows cross-compile from Linux)
```bash
cargo tauri build -- --target x86_64-pc-windows-gnu
```
The MSI installer will be in `src-tauri/target/x86_64-pc-windows-gnu/release/bundle/msi/`.
The installer bundles the WebView2 runtime offline, so target machines don't need internet access.
### Standalone EXE
The raw `.exe` is also available at `src-tauri/target/x86_64-pc-windows-gnu/release/` but requires WebView2 to already be installed on the target machine.
## Project Structure
```
duress/
├── src/
│ ├── index.html # Fallback loading page (shown briefly during startup)
│ └── inject.js # Injected JS: window.open intercept + alert detection
├── src-tauri/
│ ├── Cargo.toml # Rust dependencies (includes wry fork patch)
│ ├── tauri.conf.json # App config: name, version, bundling, security
│ ├── capabilities/
│ │ └── default.json # Tauri v2 permissions and remote domain IPC access
│ ├── permissions/
│ │ └── alert-detected.toml # Custom command permission
│ ├── src/
│ │ ├── main.rs # Windows entrypoint
│ │ └── lib.rs # App logic: tray, window management, alert_detected command
│ └── icons/ # App icons (generated via cargo tauri icon)
└── README.md
```
## Configuration
### Adding the vendor URL
The vendor URL is set in `lib.rs`:
```rust
let url = WebviewUrl::External("https://helpalert.wvumedicine.org".parse().unwrap());
```
### Alert detection tuning
Alert detection in `inject.js` looks for lines preceding "Geo:" in the page text. If the vendor changes their alert format, update the detection logic in the `setInterval` callback.
### Polling interval
The DOM is checked every 2000ms (2 seconds). Adjust the `setInterval` delay in `inject.js` if needed.
## Deployment Notes
- Target machines need Windows 10 or 11
- The MSI installer includes WebView2 runtime (offline) — no internet required
- The app installs per-machine by default
- Users should be instructed that closing the window minimizes to tray, and "Quit" in the tray menu is the actual exit
## Known Limitations
- Alert detection relies on DOM text parsing ("Geo:" pattern preceding alert names). If the vendor updates their GWT UI significantly, detection may need adjustment.
- GWT class names are obfuscated and change between builds, so selectors are text-based rather than class-based.
- Session keepalive has not been implemented yet — if the session times out, the user must re-authenticate manually. Testing suggests the session may persist indefinitely while the app is running.
- The wry fork must be kept in sync with upstream when updating Tauri.
## License
Internal use — WVU Medicine / Racoon City Technologies.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]] [[package]]
name = "WVUMedicine_HelpAlert_WebClient" name = "WVUMedicine_HelpAlert_WebClient"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
+3 -3
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "WVUMedicine_HelpAlert_WebClient" name = "WVUMedicine_HelpAlert_WebClient"
version = "0.1.0" version = "1.0.0"
description = "A Tauri App" description = "Help Alert Web Client"
authors = ["you"] authors = ["James Brotosky <james.brotosky@wvumedicine.org"]
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+13 -3
View File
@@ -2,10 +2,20 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": [
"main"
],
"remote": {
"urls": [
"https://helpalert.wvumedicine.org/*"
]
},
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default", "core:window:allow-set-focus",
"opener:allow-open-url" "core:window:allow-show",
"core:window:allow-set-always-on-top",
"core:window:allow-unminimize",
"allow-alert-detected"
] ]
} }
@@ -0,0 +1,4 @@
[[permission]]
identifier = "allow-alert-detected"
description = "Allow the alert_detected command"
commands.allow = ["alert_detected"]
+17
View File
@@ -66,6 +66,7 @@ pub fn run() {
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![alert_detected])
.on_window_event(|window: &tauri::Window, event: &WindowEvent| { .on_window_event(|window: &tauri::Window, event: &WindowEvent| {
if let WindowEvent::CloseRequested { api, .. } = event { if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close(); api.prevent_close();
@@ -75,3 +76,19 @@ pub fn run() {
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
#[tauri::command]
fn alert_detected(app: tauri::AppHandle, name: String) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
let _ = window.set_always_on_top(true);
let win = window.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(5));
let _ = win.set_always_on_top(false);
});
}
println!("[DuressGuard] Alert: {}", name);
}
+10 -3
View File
@@ -1,13 +1,13 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "WVUMedicine Help Alert Web Client", "productName": "WVUMedicine Help Alert Web Client",
"version": "0.1.0", "version": "1.0.0",
"identifier": "org.wvumedicine.duress", "identifier": "org.wvumedicine.duress",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
}, },
"app": { "app": {
"withGlobalTauri": false, "withGlobalTauri": true,
"windows": [], "windows": [],
"security": { "security": {
"csp": null "csp": null
@@ -15,7 +15,14 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": [
"nsis"
],
"windows": {
"webviewInstallMode": {
"type": "offlineInstaller"
}
},
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
+6 -9
View File
@@ -1,6 +1,6 @@
(function() { (function () {
var originalOpen = window.open; var originalOpen = window.open;
window.open = function(url) { window.open = function (url) {
if (url) { window.location.href = url; return null; } if (url) { window.location.href = url; return null; }
return originalOpen.apply(this, arguments); return originalOpen.apply(this, arguments);
}; };
@@ -9,7 +9,7 @@
Notification.requestPermission(); Notification.requestPermission();
} }
var seenAlerts = {}; var seenAlerts = {};
setInterval(function() { setInterval(function () {
if (!document.body) return; if (!document.body) return;
var text = document.body.innerText; var text = document.body.innerText;
@@ -27,11 +27,8 @@
for (var key in currentAlerts) { for (var key in currentAlerts) {
if (!seenAlerts[key]) { if (!seenAlerts[key]) {
console.log('[DuressGuard] NEW ALERT:', key); console.log('[DuressGuard] NEW ALERT:', key);
if (Notification.permission === 'granted') { if (window.__TAURI_INTERNALS__) {
new Notification('DURESS ALERT', { window.__TAURI_INTERNALS__.invoke('alert_detected', { name: key });
body: key,
requireInteraction: true
});
} }
try { try {
var ctx = new AudioContext(); var ctx = new AudioContext();
@@ -43,7 +40,7 @@
gain.gain.value = 0.3; gain.gain.value = 0.3;
osc.start(); osc.start();
osc.stop(ctx.currentTime + 0.5); osc.stop(ctx.currentTime + 0.5);
} catch(e) {} } catch (e) { }
} }
} }
seenAlerts = currentAlerts; seenAlerts = currentAlerts;