Release!
This commit is contained in:
@@ -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.
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "WVUMedicine_HelpAlert_WebClient"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "WVUMedicine_HelpAlert_WebClient"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
version = "1.0.0"
|
||||
description = "Help Alert Web Client"
|
||||
authors = ["James Brotosky <james.brotosky@wvumedicine.org"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -2,10 +2,20 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"remote": {
|
||||
"urls": [
|
||||
"https://helpalert.wvumedicine.org/*"
|
||||
]
|
||||
},
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"opener:allow-open-url"
|
||||
"core:window:allow-set-focus",
|
||||
"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"]
|
||||
@@ -66,6 +66,7 @@ pub fn run() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![alert_detected])
|
||||
.on_window_event(|window: &tauri::Window, event: &WindowEvent| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
@@ -75,3 +76,19 @@ pub fn run() {
|
||||
.run(tauri::generate_context!())
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "WVUMedicine Help Alert Web Client",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"identifier": "org.wvumedicine.duress",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"withGlobalTauri": true,
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": null
|
||||
@@ -15,7 +15,14 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": [
|
||||
"nsis"
|
||||
],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "offlineInstaller"
|
||||
}
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
+2
-5
@@ -27,11 +27,8 @@
|
||||
for (var key in currentAlerts) {
|
||||
if (!seenAlerts[key]) {
|
||||
console.log('[DuressGuard] NEW ALERT:', key);
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification('DURESS ALERT', {
|
||||
body: key,
|
||||
requireInteraction: true
|
||||
});
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
window.__TAURI_INTERNALS__.invoke('alert_detected', { name: key });
|
||||
}
|
||||
try {
|
||||
var ctx = new AudioContext();
|
||||
|
||||
Reference in New Issue
Block a user