JIRA: GNCT-4254 (Epic: GNCT-4375 — Asset Level Timeline View)
Author: Priyanshi Soni
Date: July 30, 2026 (v2 — updated from Jul 27 draft)
Sprint: Control Sprint 6
Purpose: Comprehensive design for the timeline service. Standardized template for all future BFF endpoint specs.
The Timeline Service stitches raw audit events (breadcrumbs) from DynamoDB into a coherent journey view for any asset — channels, catalogs, mappables, programs, images, or pre-PID entities being onboarded.
| # | Endpoint | Method | Purpose | Input from UI |
|---|---|---|---|---|
| 1 | /api/v1/timeline/{progservId}/runs |
GET | Run list for a channel | progservId |
| 2 | /api/v1/timeline/{progservId}/runs/{correlationId} |
GET | Run detail (milestones + lanes + days grid) | progservId + correlationId (from endpoint 1 response) |
| # | Endpoint | Method | Purpose | Input from UI |
|---|---|---|---|---|
| 3 | /api/v1/timeline/{progservId}/catalog-runs |
GET | Catalog UDIS run list | progservId |
| 4 | /api/v1/timeline/{progservId}/catalog-runs/{correlationId} |
GET | Catalog run detail (UDIS legs + threshold + downstream) | progservId + correlationId |
| # | Endpoint | Method | Purpose | Input from UI |
|---|---|---|---|---|
| 5 | /api/v1/timeline/mappable/{mappableId} |
GET | Mappable Flow (JIRA-style event log) | mappableId |
| 6 | /api/v1/timeline/mappable?remoteId={}&schemeId={} |
GET | Mappable Flow (resolve first) | remoteId + schemeId |
| 7 | /api/v1/timeline/program/{tmsId} |
GET | Program Flow (enrichment cycles) | tmsId |
| 8 | /api/v1/timeline/image/{imageId} |
GET | Image Flow (lifecycle) | imageId |
| # | Endpoint | Method | Purpose | Input from UI |
|---|---|---|---|---|
| 9 | /api/v1/timeline/source-tracking/{zendeskId} |
GET | Pre-PID onboarding timeline | zendeskId |
UI sends context ID (progservId / mappableId / tmsId / imageId / zendeskId)
│
▼
Step 1: RESOLVE — map context ID → correlationId(s)
│ Source: ct_context_map DDB
│ Example: progservId "199364" → ["Linear-CNN-2026-05-30T11:15:00", "Linear-CNN-2026-05-30T11:00:00", ...]
│
▼
Step 2: QUERY — fetch all raw events for correlationId(s)
│ Source: audit_event DDB
│ PK = "EVENT#{correlationId}#0"
│ Returns: ALL events from ALL systems for that run
│
▼
Step 3: ENRICH — apply display config to each event
│ Source: ct_stage_classification DDB (cached 5 min)
│ Lookup key: PK={system}, SK={eventName}
│ Applies: displayLabel, statusMapping, depth, lane, order, functionalStep, groupKey, iconHint, detailTemplate
│ Unknown events → fall to __DEFAULT__ entry (still visible, just raw)
│
▼
Step 4: GROUP — organize into lanes + milestones
│ Lane grouping: events grouped by config.lane field
│ Milestone strip: derived from config.functionalStep field
│ Composite cards: events with same groupKey within 5s → collapse
│
▼
Step 5: RETURN — assembled JSON response to UI
│ UI renders exactly what it receives — no transformations
| Endpoint | ID from UI | Resolution path |
|---|---|---|
| Channel runs | progservId | ct_context_map: PK=PROGSERV#{id}, SK begins_with JOB# → list of correlationIds |
| Channel run detail | correlationId | Direct — already resolved (came from run list response) |
| Catalog runs | progservId | Same as channel (different events come back — UDIS systems) |
| Mappable (by ID) | mappableId | ct_context_map: entity-index GSI → progservId → correlationIds |
| Mappable (by remoteId+scheme) | remoteId + schemeId | New NDS API → mappableId → then same as above |
| Program | tmsId | ct_context_map: entity-index GSI → progservId → correlationIds |
| Image | imageId | ct_context_map: entity-index GSI → progservId → correlationIds |
| Source Tracking | zendeskId | Zendesk API / CRM events (onboarding not yet in ct_context_map) |
One DDB table serves BOTH Lambda (metrics) AND BFF (display). Key: PK={system}, SK={eventName}.
Example entry:
{
"PK": "SIP",
"SK": "Scheduler",
"pipeline_stage": "INGESTION",
"subStage": "SCHEDULE",
"stageOrder": 1,
"displayLabel": "Feed fetch initiated by scheduler",
"functionalStep": "Fetching the feed",
"depth": 2,
"order": 10,
"lane": "ingestion",
"groupKey": "sip_fetch",
"detailTemplate": "run ID: ${correlationId} · source: ${attributes.feedName}",
"statusMapping": {"SUCCESS": "Done", "FAILURE": "Failed", "IN_PROGRESS": "In flight"},
"iconHint": "scheduler",
"enabled": true
}
| Field | What it does | Who decides the value |
|---|---|---|
| displayLabel | Text shown on UI for this event | Config (never hardcoded) |
| statusMapping | Translates raw status to display text | Config (e.g., SUCCESS → "Done") |
| functionalStep | Which milestone this event belongs to | Config |
| depth | Importance: 1=milestone, 2=sub-step, 3=detail | Config |
| lane | Which parallel track (ingestion/mapping/enrichment/schedule) | Config |
| order | Sort position in milestone strip | Config |
| groupKey | Events with same key within 5s → composite card | Config |
| detailTemplate | Detail line with ${field} interpolation | Config |
| iconHint | Which icon UI renders | Config |
| enabled | Show or hide this event type | Config |
NOT this (hardcoded):
if (status == "SUCCESS") return "Done";
if (system == "SIP") lane = "ingestion";
YES this (config-driven):
displayStatus = config.getStatusMapping().getOrDefault(event.getStatus(), event.getStatus());
lane = config.getLane();
The Java code is a generic engine that reads config and applies it. Adding a new system, changing a label, or adding a new lane = DDB row change, not code change.
{
"PK": "__DEFAULT__",
"SK": "__DEFAULT__",
"displayLabel": "${eventName}",
"depth": 3,
"lane": null,
"statusMapping": {"SUCCESS": "Done", "FAILURE": "Failed", "IN_PROGRESS": "Pending"}
}
Unknown events: visible at depth=3, raw eventName as label, in "main" lane. No crash.
| What we need | Source | Access Pattern | Latency |
|---|---|---|---|
| Correlation ID resolution | ct_context_map DDB | PK=PROGSERV#{id}, SK begins_with JOB# | < 5ms |
| Raw audit events | audit_event DDB | PK=EVENT#{correlationId}#0 | < 5ms |
| Display config | ct_stage_classification DDB | Full scan, cached 5 min | < 1ms (cached) |
| Channel/Catalog name | Source Service API (NDS) | GET /sources/{progservId} | 200-500ms (cached 1hr) |
| Mappable ID resolution | New NDS API (to be built) | GET /mappables?remoteId=&schemeId= | TBD |
| Zendesk ticket status | Zendesk API | GET /tickets/{zendeskId} | 200-500ms |
Infrastructure (already set up):
{
"openRuns": [
{
"correlationId": "Linear-CNN-2026-05-28T14:00:00",
"status": "Stuck",
"started": "2026-05-28T14:00:00Z",
"stuckSince": "2026-05-29T14:00:00Z",
"lastStep": "Mapping",
"stuckReason": "3 mappables paused on Allocation Engine",
"impactedScheduleDays": ["2026-06-01", "2026-06-04"]
}
],
"runs": [
{
"correlationId": "Linear-CNN-2026-05-30T11:15:00",
"status": "Successful",
"started": "2026-05-30T11:15:00Z",
"finished": "2026-05-30T11:23:48Z",
"duration": "8m 48s",
"source": "HBO-Feed-A",
"summary": {"scheduleDays": 5, "slots": 72, "mappables": 68, "paused": 3}
}
],
"total": 142,
"hasMore": true
}
{
"run": {
"correlationId": "Linear-CNN-2026-05-30T11:15:00",
"progservId": "199364",
"status": "Successful",
"started": "2026-05-30T11:15:00Z",
"finished": "2026-05-30T11:23:48Z",
"duration": "8m 48s",
"source": "HBO-Feed-A"
},
"milestoneStrip": [
{"step": "Fetching the feed", "status": "done", "timestamp": "2026-05-30T11:15:32Z", "order": 1},
{"step": "Parsing the feed", "status": "done", "timestamp": "2026-05-30T11:18:11Z", "order": 2},
{"step": "IDM Setup", "status": "done", "timestamp": "2026-05-30T11:21:04Z", "order": 3, "duration": "2m 53s"},
{"step": "Delivered to NED", "status": "done", "timestamp": "2026-05-30T11:21:08Z", "order": 4, "duration": "4s"},
{"step": "Ingestion of Mappables", "status": "done", "timestamp": "2026-05-30T11:23:48Z", "order": 5, "duration": "2m 40s", "detail": "68 mappables"},
{"step": "Mapping", "status": "in_progress", "timestamp": "2026-05-30T11:45:12Z", "order": 6, "duration": "21m 24s", "detail": "65/68 mapped"},
{"step": "Handoff", "status": "pending", "timestamp": null, "order": 7}
],
"scheduleDays": [
{"date": "2026-05-28", "dayOfWeek": "THU", "slotsInBatch": 14, "mappables": 14, "unmapped": 0},
{"date": "2026-05-31", "dayOfWeek": "SUN", "slotsInBatch": 16, "mappables": 15, "unmapped": 3}
],
"summary": {"scheduleDays": 5, "slots": 72, "mappables": 68, "paused": 3},
"dataPoints": {
"channelName": "CNN",
"channelType": "LINEAR",
"feedName": "HBO-Feed-A"
},
"meta": {
"total": 24,
"page": 1,
"size": 20,
"hasMore": true,
"distinctSystems": ["SIP", "Relay", "NED-NDL", "Starlight", "Automatch Consumer"],
"distinctStages": ["INGESTION", "MAPPING"]
}
}
{
"entity": {
"mappableId": "8c41ffc825aeba3ad95f519a6cb86188",
"title": "CNN Newsroom · 06:00 slot",
"remoteId": "CNN-EP-2026-0530-0600",
"scheme": "EPGS US Linear",
"mappedTmsId": null,
"currentStatus": "Automatch pending",
"scheduleDate": "2026-05-31",
"slotTime": "06:00 - 06:30",
"currentOwnerSystem": "Automatch Consumer",
"timeInCurrentStage": "3h 42m"
},
"stageStrip": [
{"name": "Ingestion", "status": "done"},
{"name": "Mapping", "status": "current"},
{"name": "Enrichment", "status": "pending"},
{"name": "Schedule Day", "status": "pending"}
],
"lastError": {
"code": "AUTOMATCH_CONFIDENCE_BELOW_THRESHOLD",
"system": "Starlight",
"timestamp": "2026-05-30T11:23:14Z",
"detail": "score: 0.42 · threshold: 0.85"
},
"events": [
{
"timestamp": "2026-05-30T11:23:14Z",
"system": "Starlight",
"displayLabel": "Automatch attempted · routed for manual allocation",
"detail": "corrId: corr-8c41ff-2026-05-30 · retry 0/3",
"status": "Pending",
"depth": 1,
"isComposite": true,
"childCount": 3,
"children": [...]
}
],
"meta": {
"total": 12,
"distinctSystems": ["SIP", "Starlight", "Automatch Consumer", "NED-NDL"]
}
}
ct-bff-timeline/src/main/java/com/ct/bff/timeline/
├── TimelineController.java ← All 9 endpoints
├── RunListService.java ← Endpoints 1, 3 (run list)
├── RunDetailService.java ← Endpoints 2, 4 (run detail)
├── EntityFlowService.java ← Endpoints 5-8 (mappable/program/image)
├── SourceTrackingService.java ← Endpoint 9 (pre-PID)
├── LaneGroupingService.java ← Groups enriched cards into lanes
├── MilestoneStripDeriver.java ← Derives milestone progress bar
├── DataPointResolutionService.java ← Config-driven supplementary data
├── RunSummaryAssembler.java ← Builds run metadata
└── model/
├── RunListResponse.java
├── RunDetailResponse.java
├── EntityFlowResponse.java
├── Lane.java
├── MilestoneStep.java
└── RunSummary.java
Timeline calls breadcrumb via HTTP interface bean (internal, same JVM):
BreadcrumbResponse response = breadcrumbApi.query(
correlationIds, entityFilterType, entityFilterId,
depth, systems, statuses, from, to, page, size
);
Breadcrumb handles: DDB query + config lookup + enrichment + composite grouping + filtering + pagination.
Timeline adds on top: lane grouping, milestone strip, run summary, data points.
Each event's lane comes from ct_stage_classification config. Events are grouped by lane value.
| Lane ID | Display Name | Systems that publish to this lane |
|---|---|---|
| ingestion | Ingestion | SIP, EPGS, Relay, NDL |
| mapping | Mapping | Starlight, Automatch, Crossmapper, myWork |
| enrichment | Enrichment | Program Gaps, MediaHub, Auto-Translation |
| schedule | Schedule Day | Rummage, TVPOP, CDC |
| request | Request | Zendesk, CRM |
| main | Primary | Anything without lane config (default) |
public List<Lane> groupIntoLanes(List<BreadcrumbCard> cards) {
// Group by card.getLane() (null → "main")
// Sort events within each lane by timestamp ASC
// Sort lanes by earliest event timestamp
// Lane with latest event IN_PROGRESS → endTime = null
}
| Scenario | Behavior |
|---|---|
| correlationId not found | Return 404 |
| ct_stage_classification empty | All events in "main" lane with raw labels |
| System API timeout (>500ms) | Return fallback value; response not blocked |
| Unknown lane value | Lane created dynamically |
| New NDS API not ready | Mappable search by remoteId returns 503 with message |
| 1000+ events | Pagination limits response |
Target: <2s p95
| Step | Latency |
|---|---|
| ct_context_map resolution | < 5ms |
| audit_event query (500 events) | < 50ms |
| Config cache lookup | < 1ms |
| Enrichment (500 events) | < 20ms |
| Lane grouping + milestone | < 10ms |
| Data points (cache hit) | < 5ms |
| Data points (cache miss, API) | 200-500ms |
| Total (warm) | < 100ms |
| Total (cold) | < 600ms |
| Dependency | Owner | Status |
|---|---|---|
| ct_context_map DDB read access | DevOps / Aravinda | Done |
| audit_event DDB read access | DevOps / Aravinda | Done |
| ct_stage_classification config entries | Aravinda | To be seeded |
| Source Service API (channel name) | NDS team | Available (NDS exists) |
| New NDS API (remoteId + scheme → mappableId) | NDS team | TO BE REQUESTED |
| Zendesk API access | External SaaS | API key needed |
| EKS + CI/CD pipeline | DevOps | Done |
| # | Question | For | Status |
|---|---|---|---|
| 1 | New NDS API for remoteId + scheme → mappableId | NDS team | Need to request |
| 2 | Per-lane pagination needed for v1? | Abhishek | Not decided |
| 3 | Zendesk API — do we have API key? | DevOps | Unknown |
| 4 | Catalog UDIS run detail — does it include threshold gate data? | Aravinda | Confirm from events |
| 5 | Who populates ct_stage_classification for UDIS/OVD events? | Aravinda | To discuss |