Add per-block agenda overrides for daily sessions

- Add 📝 Agenda button to each block in Today's Schedule on the Dashboard
- Dialog allows setting a free-text activity/note for that block for the current day
- Agenda replaces subject options in the TV center panel while set; clears on session end
- Backend: new SessionBlockAgenda model, PUT /api/sessions/{id}/blocks/{block_id}/agenda
- Agendas included in dashboard snapshot and session_update WS broadcast
- New agenda_update WS event keeps TV in sync live when agenda is saved or cleared
- Update README with feature description, project structure, and WS event table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 07:58:30 -07:00
parent d724262e27
commit fdd85d3df5
9 changed files with 246 additions and 17 deletions

View File

@@ -12,6 +12,7 @@ A self-hosted web app for managing homeschool schedules, tracking daily learning
- **Break Activities** — A global list of break-time activities (e.g. "Get a snack", "Go outside") managed in Admin → Break Activities, using the same add/edit/delete interface as Morning Routine. These items are shown on the TV during any active break. - **Break Activities** — A global list of break-time activities (e.g. "Get a snack", "Go outside") managed in Admin → Break Activities, using the same add/edit/delete interface as Morning Routine. These items are shown on the TV during any active break.
- **Rules & Expectations** — Define a list of household rules or expectations in Admin → Rules & Expectations. Items can be reordered by dragging with the handle on the left. From the Dashboard, press the **Rules/Expectations** button in the Overlays card to show them as a full-screen numbered overlay on the TV. The button turns highlighted with a pulsing **LIVE** badge while the overlay is active. Press it again to dismiss. Tapping anywhere on the TV overlay also dismisses it locally. - **Rules & Expectations** — Define a list of household rules or expectations in Admin → Rules & Expectations. Items can be reordered by dragging with the handle on the left. From the Dashboard, press the **Rules/Expectations** button in the Overlays card to show them as a full-screen numbered overlay on the TV. The button turns highlighted with a pulsing **LIVE** badge while the overlay is active. Press it again to dismiss. Tapping anywhere on the TV overlay also dismisses it locally.
- **Dashboard Overlays** — A dedicated card on the parent Dashboard groups controls that push full-screen content to the TV. Currently contains the Rules/Expectations overlay button; designed to accommodate additional overlay types in the future. - **Dashboard Overlays** — A dedicated card on the parent Dashboard groups controls that push full-screen content to the TV. Currently contains the Rules/Expectations overlay button; designed to accommodate additional overlay types in the future.
- **Block Agenda Override** — Each block in the Today's Schedule card has a 📝 button. Clicking it opens a dialog where you can type a custom activity or note for that block for the current day (e.g. "Chapter 5 reading", "Worksheet page 12"). While an agenda is set, the TV center panel shows that text instead of the block's normal subject options. Agendas are saved per session and automatically forgotten when the day ends — they never carry over to future sessions. The TV updates live via WebSocket as soon as an agenda is saved or cleared.
- **Day Progress Bar** — Both the TV dashboard and the parent dashboard display a progress bar showing how far through the day the child is. Progress is calculated from total scheduled block time vs. remaining block time — not wall-clock time — so it advances only as blocks are actively worked. On the TV the bar is labeled **Start** and **Finish**. On the parent dashboard the left label shows the scheduled start time of the first block and the right label shows a live-updating **estimated finish time** computed as the current time plus all remaining block time and break time for incomplete blocks. - **Day Progress Bar** — Both the TV dashboard and the parent dashboard display a progress bar showing how far through the day the child is. Progress is calculated from total scheduled block time vs. remaining block time — not wall-clock time — so it advances only as blocks are actively worked. On the TV the bar is labeled **Start** and **Finish**. On the parent dashboard the left label shows the scheduled start time of the first block and the right label shows a live-updating **estimated finish time** computed as the current time plus all remaining block time and break time for incomplete blocks.
- **Schedule Builder** — Create named schedule templates with time blocks assigned to subjects. Each block supports an optional custom duration override, label, and break time setting. Managed inside the Admin page. - **Schedule Builder** — Create named schedule templates with time blocks assigned to subjects. Each block supports an optional custom duration override, label, and break time setting. Managed inside the Admin page.
- **Daily Sessions** — Start a school day against a schedule template. Click any block in the list to select it as the current block. Use the **Start** button to begin timing, **Pause** to stop, **Resume** to continue from where you left off, **Done** to mark it as fully complete, and **Reset** to clear the elapsed time back to zero (timer stays paused). Elapsed time per block is remembered across switches, so returning to a block picks up where it left off. - **Daily Sessions** — Start a school day against a schedule template. Click any block in the list to select it as the current block. Use the **Start** button to begin timing, **Pause** to stop, **Resume** to continue from where you left off, **Done** to mark it as fully complete, and **Reset** to clear the elapsed time back to zero (timer stays paused). Elapsed time per block is remembered across switches, so returning to a block picks up where it left off.
@@ -79,6 +80,7 @@ homeschool/
│ │ ├── morning_routine.py# MorningRoutineItem │ │ ├── morning_routine.py# MorningRoutineItem
│ │ ├── break_activity.py # BreakActivityItem │ │ ├── break_activity.py # BreakActivityItem
│ │ ├── rule.py # RuleItem (rules & expectations) │ │ ├── rule.py # RuleItem (rules & expectations)
│ │ ├── session_block_agenda.py # SessionBlockAgenda (per-session block overrides)
│ │ ├── strike.py # StrikeEvent (strike history) │ │ ├── strike.py # StrikeEvent (strike history)
│ │ └── user.py # User (incl. timezone, last_active_at) │ │ └── user.py # User (incl. timezone, last_active_at)
│ ├── schemas/ # Pydantic request/response schemas │ ├── schemas/ # Pydantic request/response schemas
@@ -87,7 +89,7 @@ homeschool/
│ │ ├── children.py # Children CRUD + strikes + midnight reset │ │ ├── children.py # Children CRUD + strikes + midnight reset
│ │ ├── subjects.py │ │ ├── subjects.py
│ │ ├── schedules.py │ │ ├── schedules.py
│ │ ├── sessions.py # Timer actions + break timer events │ │ ├── sessions.py # Timer actions + break timer events + block agenda upsert
│ │ ├── logs.py # Timeline + strike events │ │ ├── logs.py # Timeline + strike events
│ │ ├── morning_routine.py │ │ ├── morning_routine.py
│ │ ├── break_activity.py # Break activities CRUD │ │ ├── break_activity.py # Break activities CRUD
@@ -197,7 +199,7 @@ Open **http://localhost:8054/login** and register. This creates your admin accou
| URL | Description | | URL | Description |
|-----|-------------| |-----|-------------|
| `/dashboard` | Overview, start/stop sessions, select and time blocks, issue behavior strikes, trigger TV overlays | | `/dashboard` | Overview, start/stop sessions, select and time blocks, set per-block agendas, issue behavior strikes, trigger TV overlays |
| `/logs` | Browse timer and strike event history and manual notes; filter by child and date | | `/logs` | Browse timer and strike event history and manual notes; filter by child and date |
| `/admin` | Manage children, subjects (with activity options), morning routine, break activities, rules & expectations, schedule templates, and account settings (timezone, password). Includes a Buy Me a Coffee support link at the top of the page. | | `/admin` | Manage children, subjects (with activity options), morning routine, break activities, rules & expectations, schedule templates, and account settings (timezone, password). Includes a Buy Me a Coffee support link at the top of the page. |
@@ -274,7 +276,7 @@ The TV dashboard connects to `ws://host/ws/{child_id}` and receives JSON events:
| Event | Triggered by | Key payload fields | | Event | Triggered by | Key payload fields |
|-------|-------------|---------| |-------|-------------|---------|
| `session_update` | Session start | Full session snapshot including blocks, morning routine, break activities, and day times | | `session_update` | Session start | Full session snapshot including blocks, morning routine, break activities, block agendas, and day times |
| `start` | Block timer started | `block_id`, `current_block_id`, `block_elapsed_seconds`, `prev_block_id`, `prev_block_elapsed_seconds` | | `start` | Block timer started | `block_id`, `current_block_id`, `block_elapsed_seconds`, `prev_block_id`, `prev_block_elapsed_seconds` |
| `pause` | Block timer paused | `block_id`, `current_block_id` | | `pause` | Block timer paused | `block_id`, `current_block_id` |
| `resume` | Block timer resumed | `block_id`, `current_block_id` | | `resume` | Block timer resumed | `block_id`, `current_block_id` |
@@ -287,6 +289,7 @@ The TV dashboard connects to `ws://host/ws/{child_id}` and receives JSON events:
| `strikes_update` | Strike issued/cleared/midnight reset | `strikes` | | `strikes_update` | Strike issued/cleared/midnight reset | `strikes` |
| `show_rules` | Rules/Expectations overlay triggered from Dashboard | `rules` (array of rule text strings) | | `show_rules` | Rules/Expectations overlay triggered from Dashboard | `rules` (array of rule text strings) |
| `hide_rules` | Rules/Expectations overlay dismissed from Dashboard | — | | `hide_rules` | Rules/Expectations overlay dismissed from Dashboard | — |
| `agenda_update` | Block agenda saved or cleared from Dashboard | `block_id`, `text` (empty string = cleared) |
**Notes:** **Notes:**
@@ -296,6 +299,7 @@ The TV dashboard connects to `ws://host/ws/{child_id}` and receives JSON events:
- `select` events are broadcast via WebSocket but are **not** persisted to the database or shown in the activity log. - `select` events are broadcast via WebSocket but are **not** persisted to the database or shown in the activity log.
- Implicit `pause` events (written when switching blocks or starting a break) are only recorded if the block's timer was actually running — no duplicate pauses are written if the block was already paused or never started. - Implicit `pause` events (written when switching blocks or starting a break) are only recorded if the block's timer was actually running — no duplicate pauses are written if the block was already paused or never started.
- Break timer events (`break_*`) do not affect block selection or elapsed time for the main block timer. - Break timer events (`break_*`) do not affect block selection or elapsed time for the main block timer.
- `agenda_update` events are scoped to the current session — agendas are cleared automatically when the session ends and are never carried forward to future days.
--- ---

View File

@@ -10,6 +10,7 @@ from app.models.morning_routine import MorningRoutineItem
from app.models.break_activity import BreakActivityItem from app.models.break_activity import BreakActivityItem
from app.models.strike import StrikeEvent from app.models.strike import StrikeEvent
from app.models.rule import RuleItem from app.models.rule import RuleItem
from app.models.session_block_agenda import SessionBlockAgenda
__all__ = [ __all__ = [
"Base", "Base",
@@ -28,4 +29,5 @@ __all__ = [
"BreakActivityItem", "BreakActivityItem",
"StrikeEvent", "StrikeEvent",
"RuleItem", "RuleItem",
"SessionBlockAgenda",
] ]

View File

@@ -0,0 +1,18 @@
from sqlalchemy import ForeignKey, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
class SessionBlockAgenda(Base):
__tablename__ = "session_block_agendas"
__table_args__ = (UniqueConstraint("session_id", "block_id"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
session_id: Mapped[int] = mapped_column(
ForeignKey("daily_sessions.id", ondelete="CASCADE"), nullable=False
)
block_id: Mapped[int] = mapped_column(
ForeignKey("schedule_blocks.id", ondelete="CASCADE"), nullable=False
)
text: Mapped[str] = mapped_column(Text, nullable=False)

View File

@@ -14,6 +14,7 @@ from app.models.child import Child
from app.models.morning_routine import MorningRoutineItem from app.models.morning_routine import MorningRoutineItem
from app.models.break_activity import BreakActivityItem from app.models.break_activity import BreakActivityItem
from app.models.schedule import ScheduleBlock from app.models.schedule import ScheduleBlock
from app.models.session_block_agenda import SessionBlockAgenda
from app.models.subject import Subject # noqa: F401 — needed for selectinload chain from app.models.subject import Subject # noqa: F401 — needed for selectinload chain
from app.models.session import DailySession, TimerEvent from app.models.session import DailySession, TimerEvent
from app.schemas.session import DashboardSnapshot from app.schemas.session import DashboardSnapshot
@@ -109,6 +110,15 @@ async def get_dashboard(tv_token: int, db: AsyncSession = Depends(get_db)):
) )
break_activities = [item.text for item in break_result.scalars().all()] break_activities = [item.text for item in break_result.scalars().all()]
block_agendas: dict[str, str] = {}
if session:
agendas_result = await db.execute(
select(SessionBlockAgenda).where(SessionBlockAgenda.session_id == session.id)
)
block_agendas = {
str(item.block_id): item.text for item in agendas_result.scalars().all()
}
return DashboardSnapshot( return DashboardSnapshot(
session=session, session=session,
child=child, child=child,
@@ -121,4 +131,5 @@ async def get_dashboard(tv_token: int, db: AsyncSession = Depends(get_db)):
is_break_active=is_break_active, is_break_active=is_break_active,
break_elapsed_seconds=break_elapsed_seconds, break_elapsed_seconds=break_elapsed_seconds,
is_break_paused=is_break_paused, is_break_paused=is_break_paused,
block_agendas=block_agendas,
) )

View File

@@ -1,6 +1,7 @@
from datetime import date from datetime import date
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -12,6 +13,7 @@ from app.models.break_activity import BreakActivityItem
from app.models.schedule import ScheduleBlock from app.models.schedule import ScheduleBlock
from app.models.subject import Subject # noqa: F401 — needed for selectinload chain from app.models.subject import Subject # noqa: F401 — needed for selectinload chain
from app.models.session import DailySession, TimerEvent from app.models.session import DailySession, TimerEvent
from app.models.session_block_agenda import SessionBlockAgenda
from app.models.user import User from app.models.user import User
from app.schemas.session import DailySessionOut, SessionStart, TimerAction from app.schemas.session import DailySessionOut, SessionStart, TimerAction
from sqlalchemy import delete as sql_delete from sqlalchemy import delete as sql_delete
@@ -85,6 +87,13 @@ async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
) )
break_activities = [item.text for item in break_result.scalars().all()] break_activities = [item.text for item in break_result.scalars().all()]
agendas_result = await db.execute(
select(SessionBlockAgenda).where(SessionBlockAgenda.session_id == session.id)
)
block_agendas = {
str(item.block_id): item.text for item in agendas_result.scalars().all()
}
payload = { payload = {
"event": "session_update", "event": "session_update",
"session": { "session": {
@@ -98,6 +107,7 @@ async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
"completed_block_ids": completed_ids, "completed_block_ids": completed_ids,
"morning_routine": morning_routine, "morning_routine": morning_routine,
"break_activities": break_activities, "break_activities": break_activities,
"block_agendas": block_agendas,
} }
await manager.broadcast(session.child_id, payload) await manager.broadcast(session.child_id, payload)
@@ -308,3 +318,49 @@ async def timer_action(
await manager.broadcast(session.child_id, ws_payload) await manager.broadcast(session.child_id, ws_payload)
return session return session
class AgendaUpdate(BaseModel):
text: str
@router.put("/{session_id}/blocks/{block_id}/agenda", status_code=status.HTTP_204_NO_CONTENT)
async def set_block_agenda(
session_id: int,
block_id: int,
body: AgendaUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(DailySession)
.join(Child)
.where(DailySession.id == session_id, Child.user_id == current_user.id)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")
existing = await db.execute(
select(SessionBlockAgenda).where(
SessionBlockAgenda.session_id == session_id,
SessionBlockAgenda.block_id == block_id,
)
)
agenda = existing.scalar_one_or_none()
clean_text = body.text.strip()
if clean_text:
if agenda:
agenda.text = clean_text
else:
db.add(SessionBlockAgenda(session_id=session_id, block_id=block_id, text=clean_text))
elif agenda:
await db.delete(agenda)
await db.commit()
await manager.broadcast(session.child_id, {
"event": "agenda_update",
"block_id": block_id,
"text": clean_text,
})

View File

@@ -49,3 +49,4 @@ class DashboardSnapshot(BaseModel):
is_break_active: bool = False # whether break mode is currently active is_break_active: bool = False # whether break mode is currently active
break_elapsed_seconds: int = 0 # seconds already elapsed in the break timer break_elapsed_seconds: int = 0 # seconds already elapsed in the break timer
is_break_paused: bool = False # whether the break timer is paused is_break_paused: bool = False # whether the break timer is paused
block_agendas: dict[str, str] = {} # block_id → agenda text override for today

View File

@@ -20,6 +20,7 @@ export const useScheduleStore = defineStore('schedule', () => {
const breakElapsedCache = ref({}) // blockId → total break elapsed seconds const breakElapsedCache = ref({}) // blockId → total break elapsed seconds
const showRulesOverlay = ref(false) // whether the rules overlay is visible on TV const showRulesOverlay = ref(false) // whether the rules overlay is visible on TV
const rulesOverlayItems = ref([]) // list of rule text strings to display const rulesOverlayItems = ref([]) // list of rule text strings to display
const blockAgendas = ref({}) // blockId (string) → agenda text override
const currentBlock = computed(() => const currentBlock = computed(() =>
session.value?.current_block_id session.value?.current_block_id
@@ -47,6 +48,7 @@ export const useScheduleStore = defineStore('schedule', () => {
if (snapshot.child) child.value = snapshot.child if (snapshot.child) child.value = snapshot.child
morningRoutine.value = snapshot.morning_routine || [] morningRoutine.value = snapshot.morning_routine || []
breakActivities.value = snapshot.break_activities || [] breakActivities.value = snapshot.break_activities || []
blockAgendas.value = snapshot.block_agendas || {}
// Restore elapsed time from server-computed value and seed the per-block cache // Restore elapsed time from server-computed value and seed the per-block cache
const serverElapsed = snapshot.block_elapsed_seconds || 0 const serverElapsed = snapshot.block_elapsed_seconds || 0
if (snapshot.session?.current_block_id) { if (snapshot.session?.current_block_id) {
@@ -82,6 +84,17 @@ export const useScheduleStore = defineStore('schedule', () => {
} }
function applyWsEvent(event) { function applyWsEvent(event) {
if (event.event === 'agenda_update') {
const key = String(event.block_id)
if (event.text) {
blockAgendas.value = { ...blockAgendas.value, [key]: event.text }
} else {
const updated = { ...blockAgendas.value }
delete updated[key]
blockAgendas.value = updated
}
return
}
if (event.event === 'show_rules') { if (event.event === 'show_rules') {
rulesOverlayItems.value = event.rules || [] rulesOverlayItems.value = event.rules || []
showRulesOverlay.value = true showRulesOverlay.value = true
@@ -112,6 +125,7 @@ export const useScheduleStore = defineStore('schedule', () => {
breakStartedAt.value = null breakStartedAt.value = null
breakElapsedOffset.value = 0 breakElapsedOffset.value = 0
breakElapsedCache.value = {} breakElapsedCache.value = {}
blockAgendas.value = {}
return return
} }
// Break timer events // Break timer events
@@ -407,6 +421,7 @@ export const useScheduleStore = defineStore('schedule', () => {
breakElapsedCache, breakElapsedCache,
showRulesOverlay, showRulesOverlay,
rulesOverlayItems, rulesOverlayItems,
blockAgendas,
currentBlock, currentBlock,
progressPercent, progressPercent,
applySnapshot, applySnapshot,

View File

@@ -183,21 +183,50 @@
No blocks loaded. No blocks loaded.
</div> </div>
<div class="block-list" v-else> <div class="block-list" v-else>
<ScheduleBlock <div v-for="block in scheduleStore.blocks" :key="block.id" class="block-row-wrap">
v-for="block in scheduleStore.blocks" <ScheduleBlock
:key="block.id" :block="block"
:block="block" :is-current="block.id === scheduleStore.session?.current_block_id"
:is-current="block.id === scheduleStore.session?.current_block_id" :is-completed="scheduleStore.completedBlockIds.includes(block.id)"
:is-completed="scheduleStore.completedBlockIds.includes(block.id)" :elapsed-seconds="blockElapsed(block)"
:elapsed-seconds="blockElapsed(block)" @click="selectBlock(block)"
@click="selectBlock(block)" />
/> <button
class="agenda-btn"
:class="{ 'agenda-btn-set': scheduleStore.blockAgendas[String(block.id)] }"
@click.stop="openAgendaDialog(block)"
title="Set agenda for this block"
>📝</button>
</div>
</div> </div>
</div> </div>
</div><!-- end bottom-row --> </div><!-- end bottom-row -->
</div> </div>
<!-- Agenda dialog -->
<div class="dialog-overlay" v-if="agendaDialog.open" @click.self="closeAgendaDialog">
<div class="dialog">
<h2>Block Agenda</h2>
<p class="dialog-hint">{{ agendaDialog.blockLabel }}</p>
<div class="field">
<label>Today's activity or note</label>
<textarea
v-model="agendaDialog.text"
class="agenda-textarea"
placeholder="e.g. Chapter 5 reading, worksheet page 12..."
rows="4"
@keydown.ctrl.enter="saveAgenda"
></textarea>
</div>
<div class="dialog-actions">
<button class="btn-sm btn-danger" v-if="agendaDialog.existing" @click="clearAgenda">Clear</button>
<button class="btn-sm" @click="closeAgendaDialog">Cancel</button>
<button class="btn-primary" @click="saveAgenda">Save</button>
</div>
</div>
</div>
<!-- Start session dialog --> <!-- Start session dialog -->
<div class="dialog-overlay" v-if="showStartDialog" @click.self="showStartDialog = false"> <div class="dialog-overlay" v-if="showStartDialog" @click.self="showStartDialog = false">
<div class="dialog"> <div class="dialog">
@@ -370,6 +399,38 @@ const estimatedFinishTime = computed(() => {
return `${hour}:${String(finish.getMinutes()).padStart(2, '0')} ${period}` return `${hour}:${String(finish.getMinutes()).padStart(2, '0')} ${period}`
}) })
// Agenda dialog
const agendaDialog = ref({ open: false, blockId: null, blockLabel: '', text: '', existing: false })
function openAgendaDialog(block) {
if (!scheduleStore.session) return
const label = block.label || block.subject?.name || 'Block'
const existing = scheduleStore.blockAgendas[String(block.id)] || ''
agendaDialog.value = { open: true, blockId: block.id, blockLabel: label, text: existing, existing: !!existing }
}
function closeAgendaDialog() {
agendaDialog.value = { open: false, blockId: null, blockLabel: '', text: '', existing: false }
}
async function saveAgenda() {
if (!scheduleStore.session || agendaDialog.value.blockId === null) return
await api.put(
`/api/sessions/${scheduleStore.session.id}/blocks/${agendaDialog.value.blockId}/agenda`,
{ text: agendaDialog.value.text }
)
closeAgendaDialog()
}
async function clearAgenda() {
if (!scheduleStore.session || agendaDialog.value.blockId === null) return
await api.put(
`/api/sessions/${scheduleStore.session.id}/blocks/${agendaDialog.value.blockId}/agenda`,
{ text: '' }
)
closeAgendaDialog()
}
async function toggleRulesOverlay() { async function toggleRulesOverlay() {
if (!activeChild.value) return if (!activeChild.value) return
const endpoint = scheduleStore.showRulesOverlay const endpoint = scheduleStore.showRulesOverlay
@@ -529,6 +590,53 @@ h1 { font-size: 1.75rem; font-weight: 700; }
.block-list { display: flex; flex-direction: column; gap: 0.5rem; } .block-list { display: flex; flex-direction: column; gap: 0.5rem; }
.block-row-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
}
.block-row-wrap > :first-child { flex: 1; min-width: 0; }
.agenda-btn {
flex-shrink: 0;
width: 2rem;
height: 2rem;
border: 1px solid #334155;
background: transparent;
border-radius: 0.4rem;
cursor: pointer;
font-size: 0.95rem;
opacity: 0.4;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.agenda-btn:hover { opacity: 1; background: #334155; }
.agenda-btn-set { opacity: 1; border-color: #6366f1; background: #1e1b4b; }
.agenda-textarea {
width: 100%;
padding: 0.65rem 0.9rem;
background: #0f172a;
border: 1px solid #334155;
border-radius: 0.5rem;
color: #f1f5f9;
font-size: 0.9rem;
resize: vertical;
font-family: inherit;
line-height: 1.5;
box-sizing: border-box;
}
.agenda-textarea:focus { outline: none; border-color: #818cf8; }
.dialog-hint {
font-size: 0.82rem;
color: #64748b;
margin: -0.5rem 0 0.25rem;
}
.strikes-list { display: flex; flex-direction: column; gap: 0.6rem; } .strikes-list { display: flex; flex-direction: column; gap: 0.6rem; }
.strikes-row { .strikes-row {

View File

@@ -107,12 +107,20 @@
<!-- Subject options during active block --> <!-- Subject options during active block -->
<template v-else> <template v-else>
<div class="tv-options-title">Activities</div> <div class="tv-options-title">Activities</div>
<div v-if="currentSubjectOptions.length" class="tv-options-list"> <!-- Agenda override takes priority over subject options -->
<div v-for="opt in currentSubjectOptions" :key="opt.id" class="tv-option-item"> <template v-if="scheduleStore.blockAgendas[String(scheduleStore.currentBlock?.id)]">
{{ opt.text }} <div class="tv-option-item tv-agenda-text">
{{ scheduleStore.blockAgendas[String(scheduleStore.currentBlock?.id)] }}
</div> </div>
</div> </template>
<div v-else class="tv-options-empty">No activities listed for this subject.</div> <template v-else>
<div v-if="currentSubjectOptions.length" class="tv-options-list">
<div v-for="opt in currentSubjectOptions" :key="opt.id" class="tv-option-item">
{{ opt.text }}
</div>
</div>
<div v-else class="tv-options-empty">No activities listed for this subject.</div>
</template>
</template> </template>
</div> </div>
@@ -483,6 +491,12 @@ onMounted(async () => {
font-style: italic; font-style: italic;
} }
.tv-agenda-text {
white-space: pre-wrap;
line-height: 1.5;
border-bottom: none !important;
}
.tv-day-progress { .tv-day-progress {
background: #1e293b; background: #1e293b;
border-radius: 1rem; border-radius: 1rem;