Namaste Ji by Ayushman Dash

Docs / temple-live-darshan.md · mirrored from the repo

Temple Live Darshan 🛕📺

The devotional live-video darshan plane: a back-office–curated catalog of Indian temple live streams that consumers browse, save, and get “aarti is live” alerts for. This doc is the single anchoring reference — read it before touching any temple code. It captures the data model, the legal stance, the freshness mechanism, the full lifecycle/scenario matrix, the BO surface, and the consumer contract, so any session (human or agent) can carry the work on.

Status: BO plane in build. Contract + schema + seed + catalog helpers landed (PR1). BO API + console page next (PR2). Consumer “Live Darshan” surface is designed here, built later — the consumer app UX is being built in a separate track and integrates against §8’s contract.


1. What this is (and is NOT)

  • A catalog of pointers to official live darshan streams + rich metadata + verification state.
  • Rendered on the consumer through the source’s own embedded player (YouTube IFrame Player API).
  • NOT a video host. We never download, re-encode, proxy, or re-broadcast temple video. That is the whole legal foundation (§7). “The feed” is a list of stream references, never media bytes.

2. Why a separate plane (not the content feed)

The generated-content plane (content_assets, the ranked image feed) is built around image embeddings, an offline recommender, and a batch generation pipeline. Live streams have none of that: they are a small, curated, browsable set with a runtime liveness signal. Folding them into the vector feed would couple two very different lifecycles and defeat instant legal takedown. So Live Darshan is its own catalog domain in the same catalog D1, with its own browsable serving (not ranked). Decision confirmed with the founder (2026-07-19).

3. Data model — two entities, split on purpose

Migration: [packages/catalog/migrations/0006_temples.sql]. Contract: [packages/shared/src/temple.ts] (TEMPLE_SCHEMA_VERSION). Typed access: [packages/catalog/src/temples.ts]. Same evolvability principle as the rest of the catalog: each row is a JSON payload + a few promoted, indexed columns — shape changes need no migration.

EntityTableWhat it isLifetime
TempletemplesThe durable thing a user saves / watchlists / subscribes to. Stable temple_id (temple_<slug>).Permanent
Streamtemple_streamsAn ephemeral live-video pointer (channel + resolved video id). A temple has 0..N, ordered by priority.Rotates daily, dies often
Eventtemple_eventsAppend-only domain audit (created/verified/published/…): the BO Activity tab + takedown paper trail.Permanent

Principle P1 — decouple identity from source. A user references the temple, never a stream. This single decision is what makes every hard scenario in §5 degrade gracefully: streams rotate and die underneath a temple that stays saved, named, and notifiable.

3a. Two orthogonal state axes

Do not conflate these — a page or query that treats them as one will mislead operators and users.

  • status — the editorial lifecycle a BO operator drives: draft → in_review → published → unpublished → retired.
  • health — the runtime liveness the resolver derives: live | offline | degraded | dead | unknown.

A published temple is normally offline at 3pm (darshan hours ended). That is not an error; it is the resting state. The consumer shows “offline — next aarti 6:00pm”, not “unavailable”.

4. Editorial lifecycle (status)

draft ──▶ in_review ──▶ published ──▶ unpublished ──▶ published   (reversible)

                              └────────▶ retired   (permanent; tombstoned for savers)
  • draft — being added; visible only in the BO.
  • in_review — metadata + ≥1 stream present; awaiting verify + publish.
  • published — live to consumers (per-env status gate, §8). Requires a human verify first.
  • unpublished — pulled from consumers, reversible. A soft state, never a delete.
  • retired — permanently withdrawn (rights revoked, temple gone). Tombstoned for anyone who saved it; eligible for hard-prune only after a grace period with zero user references.

Principle P2 — soft states, never hard delete. There is intentionally no “delete temple” button that destroys a row users may reference. Unpublish/retire + tombstone (§5) is the path.

5. The lifecycle scenario matrix (the core design question)

The founder’s question: “a temple is on a user’s watch list and the BO admin drops it — what happens to the user’s experience?” Here is the full matrix. The consumer contract that makes these graceful is resolveSavedTemples() in [packages/catalog/src/temples.ts] and the SavedTemple shape in the contract (§8).

#EventCatalog effectWhat the saver/consumer sees
1Stream rotates (new live video id each session)Resolver updates current_video_idNothing. They watch the temple; the player resolves the current id at play time.
2Stream goes offline (darshan hours end)health = offline (normal)“Offline — next aarti 6:00 AM” from the schedule. Save intact. Optional notify-when-live.
3Stream dies (channel deleted / embedding disabled)consecutive_failures ≥ 5 → health = dead; a backup stream auto-promotesIf a backup exists, seamless. If not, temple health rolls up to degraded/dead → unavailable card (“temporarily unavailable”), still saved.
4Admin unpublishes a templestatus = unpublishedRemoved from discovery/feed. A saver sees a tombstone (“no longer available”) — never a silent vanish. Notifications suppressed. Save row retained; user may remove it.
5Admin retires a temple (permanent)status = retiredSame tombstone, marked permanent. Pruned only after grace + zero refs.
6Legal takedown / DMCA (§7)Instant unpublished + short serving cache TTLDisappears from consumers within the cache window. Provenance + takedown contact on record.
7Metadata edit (rename, new image, fixed deity)Same temple_idSaved entry updates in place. Seamless.
8Duplicate mergedmerged_into = <canonical>Saver is redirected to the canonical temple (redirect_to), not dropped.
9New temple addeddraft → verify → publishAppears in discovery once published.
10Notify-when-liveResolver flips offline → live → emit eventPush “Somnath is live now — evening aarti is on” (§9). On unpublish/retire, subscription cancelled with a courtesy notice.
11Stream embedding disabled (is_embeddable = false)health = degraded; fall back to next stream or “Watch on YouTube” linkNever a broken player.

Principle P4 — the client always renders something. Every saved temple_id resolves to available | unavailable | tombstoned. The consumer never shows a blank card or crashes on a dropped temple. This closes the pre-existing gap: interactions (likes/saves) live in the consumer D1 with no FK / no cascade / no cleanup to the catalog, so without this resolver a dropped item would silently disappear. resolveSavedTemples() is the reconciliation the platform lacked.

6. Freshness — “keep the list fresh”

Two loops, different cadences:

A. Per-stream resolve/health (frequent, automated). A stream’s live video id rotates and its liveness changes hourly. The resolver, for each published temple’s streams:

  1. Resolves the current live video for the channel — YouTube Data API search?channelId=…&eventType=live (authoritative), or the public /live canonical redirect as a keyless first cut.
  2. Checks videos?part=status for embeddable.
  3. Writes current_video_id, is_embeddable, health, last_checked_at; increments consecutive_failures on failure; marks dead after 5. rollupTempleHealth() recomputes the temple’s health + primary stream.

Implemented as streamsDueForCheck() + applyResolveResult() (data side landed). The runner (a Cron Trigger; ~10–15 min during darshan windows, backing off overnight) is PR3. Needs a YouTube Data API key in the Secrets Store (Google project manaste-ji, enable YouTube Data API v3). Quota note: search = 100 units/call; budget by resolving on a schedule + caching, not per view. Until PR3, the BO’s manual “Resolve now” / “Test” actions cover verification.

B. Catalog discovery (periodic, Claude). “Keep it up to date using Claude from time to time” — a scheduled pass that discovers new temples and re-checks existing channel URLs, proposing draft rows into the BO for human review (never auto-publishing). MVP = this seed + a manual refresh; a temple-scout agent is the natural home later (fits the agent-operated-product vision). The seed file [packages/catalog/seed/temples.seed.json] is the human/Claude-editable source of truth for bootstrap + discovery diffs.

The stance, and why each piece exists:

  1. Embed, never host. We render the source’s own player (YouTube IFrame Player API). YouTube’s ToS grants a sublicense to embed publicly-available, embeddable videos; SDNY has upheld this. We do not download, cache, proxy, or re-broadcast the video stream.
  2. Prefer authoritative sources. Every stream carries source_authority: official_temple_trust > government > verified_broadcaster > unverified. Embedding an infringing re-broadcast still exposes us, so unverified sources are never published as-is; a human tiers each stream during review. Great authoritative seeds: the Ministry of Culture portal [utsav.gov.in/public/livedarshan], official trust channels (SVBC/TTD, Somnath, Siddhivinayak, Shirdi, SGPC), and statutory shrine boards (SMVDSB, BKTC, TDB).
  3. Respect embeddable. If a channel disables embedding, we do not force it — we fall back or link out (“Watch on YouTube”). Enforced via is_embeddable.
  4. Instant takedown. Unpublish is one action and propagates within the serving cache TTL. Each stream records a takedown_contact / rights_holder and every action is logged in temple_events.
  5. Sensitive cases flagged in the seed. Jagannath Puri (sanctum telecast contested), SGPC/Golden Temple (Kirtan telecast rights litigated) are seeded with notes and low confidence — legal review before publishing those.
  6. Image licensing. Still images are freely-licensed (Wikimedia Commons/CC). We store license + attribution and must render the credit. Seeded licenses are marked pending verification; an operator confirms the license before publish. Optionally cache bytes to R2 (image.r2_key).
  7. DPDP alignment. No PII in stream/telemetry events; watchlist + notify are user-consented; the under-18 personalization ban is absolute (see [events-analytics-dpdp-plan]). Live darshan itself is non-personalized public content, which keeps this plane low-risk.

8. Consumer contract (built later; frozen here)

The consumer “Live Darshan” surface reads the same catalog D1 over its existing read-only CATALOG_DB binding (exactly like content_assets), gated by status:

  • Env gate. Serve status = 'published' only in production (mirror the FEED_STATUSES discipline: published in prod; published,in_review allowed in staging for QA). CONSUMER_VISIBLE_STATUSES in the contract is the guard.
  • Browse. listPublishedTemples() — filter by deity/state/significance/health, “live now” first.
  • Watch. Resolve the primary stream’s current_video_idyoutubeEmbedUrl(); if none playable, show the schedule + a “notify me” affordance.
  • Saved/watchlist. resolveSavedTemples(userSavedIds) returns, per id, one of available | unavailable | tombstoned (+ redirect_to for merges) so the client renders every entry gracefully (§5, P4). This is the endpoint the existing consumer save/notify UX binds to — the notification demo already anticipates it (saved-temple, “aarti goes live”).

Endpoints to add to consumer-api in the consumer track (not yet built): GET /api/darshan/temples, GET /api/darshan/temples/:id, GET /api/darshan/saved.

9. Schedule & notifications

schedule.aarti_times (per-temple, Asia/Kolkata) powers “next aarti” hints and the notify-when-live signal. The push service is a stub today ([packages/notification]); this plane defines the event it will emit: on a resolver offline → live flip for a temple a user subscribed to, enqueue a saved-temple/aarti-live notification. On unpublish/retire, cancel the subscription with a courtesy notice. Wiring is deferred to the notification track; the trigger lives in the resolver (PR3).

10. Back-office surface (PR2)

Follows the console conventions ([docs/console/DESIGN-PATTERNS.md]): server-authoritative collection, URL-persisted filters, DrawerStack detail, RBAC-gated actions, --color-* tokens only.

  • /temples — collection. Header counts (published · live now · needs-attention · draft). URL-state filters: deity, state, significance, source-authority, status, health, q, sort. Grid/table rows: image, name, deity + authority + status + health chips, last verified. Bulk actions (RBAC): Publish · Unpublish · Verify · Resolve-now · Retire. “Add temple”.
  • /temples/:id (+ drawer, one shared body) — tabs:
    • Overview — metadata editor (name/deity/tradition/location/languages/schedule/description/image
      • license + attribution).
    • Streams — per-stream health, priority, source_authority, embeddable. Test renders the YouTube embed inline so the operator watches it and confirms it’s the right temple + it plays → Verify stamps verified_at/by. Add/remove/reorder streams, Resolve now.
    • Legal / Provenance — authority, rights holder, license, takedown contact, notes.
    • Activitytemple_events log.
    • Publish gate — Test → Publish/Unpublish/Retire with a dry-run confirm (mirrors Heimdall’s run-scoped publish gate). Only verified temples are publishable.

RBAC (PR2)

New permissions in [packages/rbac] + a console-api seed migration (mirrors 0013/0014): temple:read (viewer+), temple:edit (editor+), temple:publish (admin). Routes live in a console-api sub-router services/console-api/src/temples/routes.ts, mounted at /api/temples, each withIdentity() + requirePermission(...), writing to CATALOG_DB.

Deploy (PR2)

Catalog migration 0006 is applied by deploy-content-creator.yml (landed). Because console-api is the temple writer, also apply 0006 in deploy-console.yml (idempotent) so the table leads the routes, and add the temple:* RBAC seed migration to the console DB migration list.

11. Build plan (stacked PRs)

  • PR1 — data foundation (this PR). Contract (temple.ts), migration 0006, typed helpers (temples.ts), seed data (38 temples / 41 streams from an authoritative crawl), idempotent seeder (scripts/seed-temples.mjs), migration wired into content-creator deploy, this doc.
  • PR2 — BO plane. RBAC perms + seed migration, console-api /api/temples routes, the /temples console page (collection + detail + test-embed + publish gate), deploy wiring.
  • PR3 — freshness runner. Cron resolver (YouTube Data API key in Secrets Store), health rollups, notify-when-live event emission.
  • Consumer track (separate). The Live Darshan surface + saved/tombstone rendering against §8.

12. Seed provenance snapshot (2026-07-19)

38 temples (14 Shiva/Jyotirlinga, ~12 Vishnu/Krishna/Sai, ~12 Devi/Ganesha/Sikh/other). Streams by authority: 21 official-trust, 18 government, 1 verified-broadcaster, 1 unverified. Confidence: 10 high / 18 medium / 10 low. 5 temples intentionally seeded with no stream (identity solid, no credible official source found — e.g. Bhimashankar, Grishneshwar, Kamakhya). Every temple is draft; image licenses are pending until an operator verifies them. The crawl deliberately stored channel URLs/handles + an authority tier rather than fabricating live video ids (which rotate and which the operator confirms in the BO anyway).