feat: localStorage injection for AbleSign WebView cells
Some checks are pending
release / meta (push) Waiting to run
release / build (push) Blocked by required conditions

Server bundle:
- BundleCell gains local_storage: Record<string,string> field
- AbleSign entities populate screenId + screenToken (decrypted)
  into local_storage for the cell

Kiosk:
- BundleCell.local_storage deserialized from bundle
- ensure_web() accepts local_storage param
- Injects UserScript at document-start that sets localStorage
  items before the page loads — AbleSign player reads these
  and skips the pairing screen

Also: getSetupState auto-creates row if missing (handles DELETE)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mitchell R 2026-05-27 04:02:30 +02:00
parent d58792524d
commit 8b988d7a7d
3 changed files with 44 additions and 2 deletions

View file

@ -138,6 +138,8 @@ pub struct BundleCell {
pub fit: String,
#[serde(default)]
pub smart_url: Option<SmartUrlConfig>,
#[serde(default)]
pub local_storage: Option<std::collections::HashMap<String, String>>,
}
fn default_fit() -> String { "cover".to_string() }

View file

@ -1193,7 +1193,7 @@ fn render_layout(display_id: &str, layout_id: &str) {
none_cell()
} else {
let key = html_key(html);
ensure_web(key, WebSource::Html(html), server_url, kiosk_key).upcast()
ensure_web(key, WebSource::Html(html), server_url, kiosk_key, None).upcast()
}
}
"web" => {
@ -1202,7 +1202,7 @@ fn render_layout(display_id: &str, layout_id: &str) {
none_cell()
} else {
let key = format!("web:{url}");
let wv = ensure_web(key, WebSource::Url(url), server_url, kiosk_key);
let wv = ensure_web(key, WebSource::Url(url), server_url, kiosk_key, cell.local_storage.as_ref());
// Smart URL: execute login/navigation steps after page loads.
if let Some(ref smart) = cell.smart_url {
let decrypt_key = server::load_encrypt_key()
@ -1924,6 +1924,7 @@ fn ensure_web(
source: WebSource<'_>,
server_url: &str,
kiosk_key: &str,
local_storage: Option<&std::collections::HashMap<String, String>>,
) -> webkit6::WebView {
let cached = WARM_WEBVIEWS.with(|m| m.borrow().get(&key).map(|e| e.webview.clone()));
if let Some(wv) = cached {
@ -1967,6 +1968,26 @@ fn ensure_web(
}
}
if let Some(ls) = local_storage {
if !ls.is_empty() {
let mut js = String::from("(function(){");
for (k, v) in ls {
js.push_str(&format!("localStorage.setItem({},{});", js_string_lit(k), js_string_lit(v)));
}
js.push_str("})();");
let script = webkit6::UserScript::new(
&js,
webkit6::UserContentInjectedFrames::TopFrame,
webkit6::UserScriptInjectionTime::Start,
&[],
&[],
);
if let Some(ucm) = webkit6::prelude::WebViewExt::user_content_manager(&wv) {
ucm.add_script(&script);
}
}
}
match source {
WebSource::Html(html) => {
webkit6::prelude::WebViewExt::load_html(&wv, html, None);

View file

@ -91,6 +91,8 @@ export interface BundleCell {
login_detect_url?: string;
session_check_interval_ms?: number;
};
/** Key→value pairs injected into WebView localStorage before page load. */
local_storage?: Record<string, string>;
}
export interface BundleLayout {
@ -220,6 +222,7 @@ export async function generateBundle(
// bundle still ships the legacy camera_id/web_url/html_content shape
// so the existing Rust kiosk consumes it unchanged.
let contentType = c.content_type;
let cellLocalStorage: Record<string, string> | undefined;
let cameraId = c.camera_id;
let webUrl = c.web_url;
let htmlContent = c.html_content;
@ -237,6 +240,21 @@ export async function generateBundle(
ent.type === "dashboard" && ent.dashboard_id ? `/dash/${ent.dashboard_id}` :
null;
htmlContent = ent.type === "html" ? ent.html_content : null;
// AbleSign: inject screenToken + screenId into localStorage
if (ent.type === "ablesign" && ent.ablesign_screen_id) {
const screen = await repo.getAbleSignScreen(ent.ablesign_screen_id);
if (screen) {
const ls: Record<string, string> = {
screenId: screen.ablesign_screen_id,
};
if (screen.ablesign_screen_token_encrypted) {
try {
ls["screenToken"] = secrets.decryptString(screen.ablesign_screen_token_encrypted, "ablesign-token");
} catch { /* token decrypt failed — player will show pairing */ }
}
cellLocalStorage = ls;
}
}
}
}
bundleCells.push({
@ -271,6 +289,7 @@ export async function generateBundle(
session_check_interval_ms: raw.session_check_interval_ms,
};
})() : undefined,
local_storage: cellLocalStorage,
});
}
result.push({