-
@@ -276,7 +300,7 @@ let wsDisconnect = null
async function loadDashboard() {
if (!activeChild.value) return
- await scheduleStore.fetchDashboard(activeChild.value.id)
+ await scheduleStore.fetchDashboard(activeChild.value.tv_token)
// Load templates for start dialog
const res = await api.get('/api/schedules')
@@ -284,7 +308,7 @@ async function loadDashboard() {
// WS subscription
if (wsDisconnect) wsDisconnect()
- const { disconnect } = useWebSocket(activeChild.value.id, (msg) => {
+ const { disconnect } = useWebSocket(activeChild.value.tv_token, (msg) => {
scheduleStore.applyWsEvent(msg)
})
wsDisconnect = disconnect
@@ -346,6 +370,14 @@ const estimatedFinishTime = computed(() => {
return `${hour}:${String(finish.getMinutes()).padStart(2, '0')} ${period}`
})
+async function toggleRulesOverlay() {
+ if (!activeChild.value) return
+ const endpoint = scheduleStore.showRulesOverlay
+ ? '/api/overlays/rules/hide'
+ : '/api/overlays/rules/show'
+ await api.post(endpoint, { child_id: activeChild.value.id })
+}
+
function selectBlock(block) {
if (!scheduleStore.session) return
// Clicking the current block does nothing — use Start/Pause/Resume buttons
@@ -368,11 +400,31 @@ watch(activeChild, loadDashboard)
h1 { font-size: 1.75rem; font-weight: 700; }
.dashboard-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
+ display: flex;
+ flex-direction: column;
gap: 1.5rem;
}
+.top-row {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1.5rem;
+}
+
+.bottom-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ align-items: start;
+}
+
+@media (max-width: 700px) {
+ .top-row,
+ .bottom-row {
+ grid-template-columns: 1fr;
+ }
+}
+
.card {
background: #1e293b;
border-radius: 1rem;
@@ -477,8 +529,6 @@ h1 { font-size: 1.75rem; font-weight: 700; }
.block-list { display: flex; flex-direction: column; gap: 0.5rem; }
-.strikes-card { grid-column: span 1; }
-
.strikes-list { display: flex; flex-direction: column; gap: 0.6rem; }
.strikes-row {
@@ -516,8 +566,46 @@ h1 { font-size: 1.75rem; font-weight: 700; }
color: #fca5a5;
}
-.tv-card { grid-column: span 1; }
.tv-desc { color: #64748b; margin-bottom: 1rem; font-size: 0.9rem; }
+.overlays-grid { display: flex; flex-wrap: wrap; gap: 0.75rem; }
+
+.overlay-btn {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.75rem 1.25rem;
+ background: #1e293b;
+ border: 1px solid #334155;
+ border-radius: 0.75rem;
+ color: #94a3b8;
+ font-size: 0.9rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.overlay-btn:hover:not(:disabled) { background: #334155; border-color: #475569; color: #f1f5f9; }
+.overlay-btn:disabled { opacity: 0.4; cursor: not-allowed; }
+.overlay-btn-active {
+ background: #1e1b4b;
+ border-color: #6366f1;
+ color: #a5b4fc;
+}
+.overlay-btn-active:hover:not(:disabled) { background: #312e81; }
+.overlay-btn-icon { font-size: 1.2rem; }
+.overlay-live-badge {
+ font-size: 0.65rem;
+ font-weight: 700;
+ background: #6366f1;
+ color: #fff;
+ padding: 0.1rem 0.4rem;
+ border-radius: 999px;
+ letter-spacing: 0.06em;
+ animation: pulse-badge 1.5s ease-in-out infinite;
+}
+@keyframes pulse-badge {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
.btn-primary {
display: inline-block;
diff --git a/frontend/src/views/TVView.vue b/frontend/src/views/TVView.vue
index 40ec065..346c05a 100644
--- a/frontend/src/views/TVView.vue
+++ b/frontend/src/views/TVView.vue
@@ -163,6 +163,24 @@