11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 05-live-show-execution | 03 | execute | 3 |
|
|
true |
|
|
Purpose: Implements UI-03 (all primary operations accessible without mouse). Standard DAW keyboard conventions make the app usable during live show execution without reaching for the mouse.
Output: Modified lightsync/frontend/app.js with a wireKeyboard() function called at init time.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-live-show-execution/05-CONTEXT.md @.planning/phases/05-live-show-execution/05-UI-SPEC.md @.planning/phases/05-live-show-execution/05-01-SUMMARY.md @.planning/phases/05-live-show-execution/05-02-SUMMARY.md ```javascript const client = new LightSyncClient(); // WebSocket client let timeline = null; // TimelineCanvas instance, set in initTimeline()// Audio element: document.getElementById('player') // audio.play(), audio.pause(), audio.paused, audio.currentTime, audio.duration
// Timeline history: timeline.history.undo(), timeline.history.redo() // Show ID: timeline.showId (null if no show loaded)
<!-- Keyboard bindings from CONTEXT.md + UI-SPEC.md -->
| Key | Action |
|-----|--------|
| Space | Play/Pause toggle |
| ArrowLeft | Seek -5s |
| ArrowRight | Seek +5s |
| Ctrl+Z | Undo |
| Ctrl+Shift+Z | Redo |
| Ctrl+Y | Redo (alt) |
| Ctrl+S | Explicit save |
<!-- Save endpoint (existing from shows.py) -->
GET /api/shows/{id} returns full ShowModel
POST /api/shows (body: ShowModel) saves/creates
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add wireKeyboard() function and call it at init</name>
<files>lightsync/frontend/app.js</files>
<read_first>
- lightsync/frontend/app.js (current state after Plan 02)
- .planning/phases/05-live-show-execution/05-UI-SPEC.md (Keyboard Shortcut Bindings section)
</read_first>
<action>
Add the `wireKeyboard` function to `lightsync/frontend/app.js`. Place it after the `wireAudio()` function definition (around line 148) and before the file list section.
```javascript
// ── Keyboard shortcuts ──────────────────────────────────────────────────────
function wireKeyboard() {
const audio = document.getElementById('player');
document.addEventListener('keydown', (e) => {
// Skip shortcuts when typing in form elements
const tag = document.activeElement?.tagName?.toLowerCase();
if (['input', 'select', 'textarea'].includes(tag)) return;
// Also skip Space on focused buttons to prevent double-toggle (Pitfall 6)
if (e.code === 'Space' && tag === 'button') return;
if (e.code === 'Space') {
e.preventDefault();
if (!audio) return;
if (audio.paused) {
audio.play();
client.send({ type: 'play', position: audio.currentTime });
} else {
audio.pause();
client.send({ type: 'pause', position: audio.currentTime });
}
} else if (e.code === 'ArrowLeft') {
e.preventDefault();
if (!audio) return;
audio.currentTime = Math.max(0, audio.currentTime - 5);
client.send({ type: 'seek', position: audio.currentTime });
// Clear preview strips on seek
document.querySelectorAll('.device-strip').forEach(s => {
s.classList.remove('active');
const colorEl = s.querySelector('.strip-color');
if (colorEl) colorEl.style.backgroundColor = '';
const animEl = s.querySelector('.strip-anim');
if (animEl) animEl.textContent = '\u2014';
});
} else if (e.code === 'ArrowRight') {
e.preventDefault();
if (!audio) return;
audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 5);
client.send({ type: 'seek', position: audio.currentTime });
// Clear preview strips on seek
document.querySelectorAll('.device-strip').forEach(s => {
s.classList.remove('active');
const colorEl = s.querySelector('.strip-color');
if (colorEl) colorEl.style.backgroundColor = '';
const animEl = s.querySelector('.strip-anim');
if (animEl) animEl.textContent = '\u2014';
});
} else if (e.ctrlKey && !e.shiftKey && e.code === 'KeyZ') {
e.preventDefault();
timeline?.history.undo();
} else if ((e.ctrlKey && e.shiftKey && e.code === 'KeyZ') || (e.ctrlKey && e.code === 'KeyY')) {
e.preventDefault();
timeline?.history.redo();
} else if (e.ctrlKey && e.code === 'KeyS') {
e.preventDefault(); // Prevent browser Save dialog (Pitfall 7)
if (timeline?.showId) {
fetch(`/api/shows/${timeline.showId}`)
.then(r => r.json())
.then(show => fetch('/api/shows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(show),
}))
.then(() => console.log('[save] show saved via Ctrl+S'))
.catch(e => console.warn('[save]', e));
}
}
});
}
Call wireKeyboard() at the bottom of the Init section, after wireAudio() and before initTimeline():
wireAudio();
wireKeyboard(); // NEW — keyboard shortcuts (Phase 5)
Key implementation details:
e.preventDefault()is called FIRST in every branch to suppress browser defaults (Space scroll, Ctrl+S save dialog, Arrow key scroll)- Space on
<button>elements is explicitly skipped to prevent the double-toggle issue (Pitfall 6) - Arrow seek sends
{ type: 'seek' }to the server so the cue scheduler rebuildsfired_ids - Arrow seek also clears preview strips (same pattern as seek bar mouseup from Plan 02)
- Space play/pause sends
{ type: 'play' }or{ type: 'pause' }to the server so the scheduler'sis_playingflag updates - Ctrl+S does a GET then POST round-trip (GET current show state → POST to save) — this is a manual trigger, the primary persistence is auto-save on block mutations
timeline?.history.undo()uses optional chaining since timeline might not be initialized yet cd /home/claude/led2 && python -c " with open('lightsync/frontend/app.js') as f: js = f.read() checks = { 'wireKeyboard function': 'function wireKeyboard' in js, 'wireKeyboard called': 'wireKeyboard()' in js, 'Space handler': "e.code === 'Space'" in js, 'ArrowLeft handler': "e.code === 'ArrowLeft'" in js, 'ArrowRight handler': "e.code === 'ArrowRight'" in js, 'Ctrl+Z handler': "e.code === 'KeyZ'" in js, 'Ctrl+Y handler': "e.code === 'KeyY'" in js, 'Ctrl+S handler': "e.code === 'KeyS'" in js, 'preventDefault': js.count('e.preventDefault()') >= 6, 'input guard': "'input', 'select', 'textarea'" in js or "input.*select.*textarea" in js, 'history.undo': 'history.undo()' in js, 'history.redo': 'history.redo()' in js, 'play message': "type: 'play'" in js or 'type: "play"' in js, 'pause message': "type: 'pause'" in js or 'type: "pause"' in js, } for name, ok in checks.items(): print(f'{name}: {"PASS" if ok else "FAIL"}') import sys; sys.exit(0 if all(checks.values()) else 1) " <acceptance_criteria>- app.js contains
function wireKeyboard()definition - app.js calls
wireKeyboard()in the init section (before or after wireAudio) - app.js Space handler calls
e.preventDefault()and togglesaudio.play()/pause() - app.js Space handler skips when
tag === 'button' - app.js ArrowLeft handler sets
audio.currentTime = Math.max(0, audio.currentTime - 5) - app.js ArrowRight handler sets
audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 5) - app.js Arrow handlers send
{ type: 'seek', position: audio.currentTime }via client.send - app.js Ctrl+Z handler calls
timeline?.history.undo() - app.js Ctrl+Shift+Z and Ctrl+Y both call
timeline?.history.redo() - app.js Ctrl+S handler calls
e.preventDefault()and fetches/api/shows/${timeline.showId}then POSTs - app.js input element guard checks for
input,select,textareatag names e.preventDefault()appears at least 6 times in the keydown handler (one per key combo) </acceptance_criteria> All 7 keyboard shortcuts are wired: Space (play/pause), ArrowLeft (seek -5s), ArrowRight (seek +5s), Ctrl+Z (undo), Ctrl+Shift+Z (redo), Ctrl+Y (redo alt), Ctrl+S (save). All shortcuts are suppressed in form elements. Browser defaults are prevented.
- app.js contains
<success_criteria>
- Space toggles play/pause and sends play/pause message to server
- Arrow keys seek +/- 5 seconds and send seek message to server
- Ctrl+Z undoes last timeline mutation via history.undo()
- Ctrl+Shift+Z and Ctrl+Y both redo via history.redo()
- Ctrl+S saves current show (no-op if no show loaded, prevents browser dialog)
- All shortcuts are disabled when typing in input/select/textarea fields
- Space does not trigger button clicks when a button is focused </success_criteria>