Changelog
2026-07-01
Initial implementation — ScheduleSlot CRUD + EPG queries
Why: PlayTelly channels needed a program timetable layer so the OTT client can display a guide overlay alongside the live stream. EPG data is orthogonal to stream configuration — a channel plays regardless of whether EPG exists, and EPG is purely informational metadata on top of whatever is airing.
What was built:
models.go — ScheduleSlot struct, table schedule_slots. Loose FK to channels.id (integer only, no struct import). All times stored as UTC — frontend converts to viewer local timezone.
handler_schedule.go — full CRUD with overlap validation. Two slots cannot overlap on the same channel — 409 Conflict returned if they would. On update, the current slot is excluded from the overlap check so title/genre updates never trigger a false positive.
handler_epg.go — three EPG query endpoints:
- GET /epg?date= — EPG grid, all channels for a day. Single call drives the full grid UI. Joins channels table via raw SQL (no playback model import). Returns empty channels array with hint if no channels exist.
- GET /epg/:channelId?date= — single channel, one day.
- GET /epg/:channelId/now — currently airing program + next program. now is null if nothing is scheduled for the current time.
Design decisions:
- Scheduling domain does not import
domain/playback— channel identity is accessed via raw SQL join onid,sid,nameonly. Zero circular imports. channelRowlocal struct inGetEPGGriduses explicitgorm:"column:..."tags (gorm:"column:sid") to avoid GORM's all-caps naming convention convertingSID→s_idin raw scan. Pattern mirrors howChannel.SIDis declared in the playback domain model.EPGEntry,BlackoutRule,RecurrenceRuleare spec'd entities deferred to future iterations — stubs exist inhandler_blackout.goandhandler_recurrence.go.SCHEDULING_NATS_ENABLEDenv var is wired inconfig.gobut not yet acted on — placeholder for the "emits facts onto the bus" behaviour the domain spec requires.
Bug fixed during initial build:
UpdateSlot had a duplicate time-parsing block — start/end times were parsed twice, first without setting newStart/newEnd, then again correctly. The first redundant block was removed, keeping only the block that sets both updates map and the newStart/newEnd variables used by the overlap check.