security+feat(web): kill DOM XSS, add CSP, in-browser macro editor
Security:
- Eliminate DOM-based XSS: renderMacros/renderTabs rebuilt with
createElement/textContent/setAttribute + addEventListener. No macro name,
id, image_path, or category ever reaches innerHTML.
- Remove all inline on*= handlers; add a strict Content-Security-Policy meta
(script-src 'self', object-src 'none', base-uri 'none', frame-ancestors
'none').
- Guard JSON.parse on WebSocket frames.
Robustness:
- Service worker: network-first for the app shell + JS/CSS so fixes actually
ship; synthetic 503 for offline API calls; drop no-op /ws intercept; bump
cache version.
- WebSocket reconnect: exponential backoff (1s→30s), tear down old socket
before reconnect, stop on pagehide/beforeunload.
- Tab-switch race fixed with a request-id guard; executing state cleared in
finally.
Accessibility:
- Macro cards and tabs are real buttons with aria-labels/roles; toast +
connection status are aria-live; removed user-scalable=no.
Feature — web-based macro creation/editing:
- Floating "+" button + modal command-sequence builder (text/key/hotkey/
wait/app) with reorder/remove; create via POST, edit via PUT (prefilled
from GET /api/macro/{id}), delete with a named confirm. Works in both local
and relay auth modes. Empty state now offers "Create your first macro".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+46
-9
@@ -2,7 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'">
|
||||
<meta name="theme-color" content="#007acc">
|
||||
<meta name="description" content="Remote macro control for your desktop">
|
||||
|
||||
@@ -29,20 +30,20 @@
|
||||
<header class="header">
|
||||
<h1>MacroPad</h1>
|
||||
<div class="header-actions">
|
||||
<div class="connection-status">
|
||||
<div class="connection-status" id="connection-status" role="status" aria-live="polite">
|
||||
<div class="status-dot"></div>
|
||||
<span>Disconnected</span>
|
||||
</div>
|
||||
<div class="wake-lock-status" id="wake-lock-status" title="Screen wake lock">
|
||||
<span class="wake-icon">☀</span>
|
||||
</div>
|
||||
<button class="header-btn icon-btn" id="fullscreen-btn" onclick="app.toggleFullscreen()" title="Toggle fullscreen">⛶</button>
|
||||
<button class="header-btn secondary" onclick="app.refresh()">Refresh</button>
|
||||
<button type="button" class="wake-lock-status" id="wake-lock-status" title="Screen wake lock" aria-label="Toggle screen wake lock">
|
||||
<span class="wake-icon" aria-hidden="true">☀</span>
|
||||
</button>
|
||||
<button type="button" class="header-btn icon-btn" id="fullscreen-btn" title="Toggle fullscreen" aria-label="Toggle fullscreen">⛶</button>
|
||||
<button type="button" class="header-btn secondary" id="refresh-btn">Refresh</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tabs -->
|
||||
<nav class="tabs" id="tabs-container">
|
||||
<nav class="tabs" id="tabs-container" role="tablist" aria-label="Macro categories">
|
||||
<!-- Tabs rendered dynamically -->
|
||||
</nav>
|
||||
|
||||
@@ -53,8 +54,44 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Floating action button: create new macro -->
|
||||
<button type="button" class="fab" id="new-macro-btn" aria-label="New macro">+</button>
|
||||
|
||||
<!-- Macro create/edit modal -->
|
||||
<div class="modal" id="macro-modal" role="dialog" aria-modal="true" aria-labelledby="macro-modal-title" hidden>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="macro-modal-title">New Macro</h2>
|
||||
<button type="button" class="modal-close" id="macro-modal-close" aria-label="Close dialog">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="macro-name-input">Name</label>
|
||||
<input type="text" id="macro-name-input" autocomplete="off" placeholder="Macro name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="macro-category-input">Category</label>
|
||||
<input type="text" id="macro-category-input" autocomplete="off" list="macro-category-list" placeholder="Category (optional)">
|
||||
<datalist id="macro-category-list"></datalist>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Commands</label>
|
||||
<div class="command-list" id="command-list"></div>
|
||||
<div class="add-command-btns">
|
||||
<button type="button" class="add-command-btn" id="add-command-btn">+ Add step</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" id="delete-macro-btn" hidden>Delete</button>
|
||||
<button type="button" class="btn btn-secondary" id="cancel-macro-btn">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="save-macro-btn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
<div class="toast-container" id="toast-container" role="status" aria-live="polite"></div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user