fix: blank company charts + competitor GUIDs instead of tickers
Trend charts blank: - trend_windows uses upsert (1 row per ticker/window), so charts had at most 1 data point. Added trend_history table (migration 024) that appends every snapshot. New /api/trends/history endpoint serves the time series. Frontend now uses useTrendHistory for charts and useTrends for the latest summary card. Competitor GUIDs: - list_competitors query returned raw company_b_id UUIDs without joining companies table. Added LEFT JOIN with CASE to resolve the other company's ticker and legal_name. Updated Pydantic model to include enriched fields. Frontend fallback changed from truncated UUID to ticker/legal_name/Unknown.
This commit is contained in:
@@ -742,11 +742,23 @@ RETURNING id
|
||||
"""
|
||||
|
||||
|
||||
_INSERT_TREND_HISTORY = """
|
||||
INSERT INTO trend_history (
|
||||
entity_type, entity_id, "window", trend_direction,
|
||||
trend_strength, confidence, contradiction_score,
|
||||
dominant_catalysts, material_risks, generated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
|
||||
"""
|
||||
|
||||
|
||||
async def persist_trend_summary(
|
||||
pool: asyncpg.Pool,
|
||||
summary: TrendSummary,
|
||||
) -> str:
|
||||
"""Insert a trend summary row and return its UUID."""
|
||||
"""Insert a trend summary row and return its UUID.
|
||||
|
||||
Also appends a snapshot to trend_history for time-series charting.
|
||||
"""
|
||||
row = await pool.fetchrow(
|
||||
_UPSERT_TREND,
|
||||
summary.entity_type,
|
||||
@@ -764,7 +776,28 @@ async def persist_trend_summary(
|
||||
json.dumps(summary.market_context.model_dump() if summary.market_context else {}, default=str),
|
||||
summary.generated_at,
|
||||
)
|
||||
return str(row["id"])
|
||||
trend_id = str(row["id"])
|
||||
|
||||
# Append to trend_history for time-series charts
|
||||
try:
|
||||
await pool.execute(
|
||||
_INSERT_TREND_HISTORY,
|
||||
summary.entity_type,
|
||||
summary.entity_id,
|
||||
summary.window.value,
|
||||
summary.trend_direction.value,
|
||||
summary.trend_strength,
|
||||
summary.confidence,
|
||||
summary.contradiction_score,
|
||||
json.dumps(summary.dominant_catalysts),
|
||||
json.dumps(summary.material_risks),
|
||||
summary.generated_at,
|
||||
)
|
||||
except Exception:
|
||||
# Don't fail the main upsert if history insert fails (table may not exist yet)
|
||||
logger.debug("Could not insert trend history for %s/%s", summary.entity_id, summary.window.value, exc_info=True)
|
||||
|
||||
return trend_id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -415,6 +415,56 @@ async def list_trends(
|
||||
return results
|
||||
|
||||
|
||||
@app.get("/api/trends/history")
|
||||
async def list_trend_history(
|
||||
ticker: Optional[str] = None,
|
||||
window: Optional[str] = None,
|
||||
limit: int = Query(default=200, le=1000),
|
||||
):
|
||||
"""Return historical trend snapshots for charting.
|
||||
|
||||
Unlike /api/trends which returns the latest snapshot per entity/window,
|
||||
this endpoint returns the time series from the trend_history table.
|
||||
"""
|
||||
conditions: list[str] = []
|
||||
params: list[Any] = []
|
||||
idx = 1
|
||||
|
||||
if ticker:
|
||||
conditions.append(f"entity_id = ${idx}")
|
||||
params.append(ticker.upper())
|
||||
idx += 1
|
||||
if window:
|
||||
conditions.append(f"\"window\" = ${idx}")
|
||||
params.append(window)
|
||||
idx += 1
|
||||
|
||||
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
||||
|
||||
try:
|
||||
rows = await pool.fetch(
|
||||
f"""SELECT id, entity_type, entity_id, "window", trend_direction,
|
||||
trend_strength, confidence, contradiction_score,
|
||||
dominant_catalysts, material_risks, generated_at
|
||||
FROM trend_history
|
||||
{where}
|
||||
ORDER BY generated_at ASC
|
||||
LIMIT ${idx}""",
|
||||
*params, limit,
|
||||
)
|
||||
except Exception:
|
||||
# Table may not exist yet (pre-migration 024)
|
||||
return []
|
||||
|
||||
results = []
|
||||
for r in rows:
|
||||
d = _row_to_dict(r)
|
||||
d["dominant_catalysts"] = _parse_jsonb(d.get("dominant_catalysts"))
|
||||
d["material_risks"] = _parse_jsonb(d.get("material_risks"))
|
||||
results.append(d)
|
||||
return results
|
||||
|
||||
|
||||
@app.get("/api/trends/{trend_id}")
|
||||
async def get_trend(trend_id: str):
|
||||
"""Get a single trend summary by ID."""
|
||||
|
||||
@@ -53,6 +53,9 @@ class CompetitorRelationship(BaseModel):
|
||||
active: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
# Enriched from companies table
|
||||
ticker: str | None = None
|
||||
legal_name: str | None = None
|
||||
|
||||
|
||||
def _row_dict(row: asyncpg.Record) -> dict[str, Any]:
|
||||
@@ -128,18 +131,31 @@ async def create_competitor(company_id: str, body: CompetitorRelationshipCreate,
|
||||
|
||||
@router.get("/companies/{company_id}/competitors", response_model=List[CompetitorRelationship])
|
||||
async def list_competitors(company_id: str, request: Request):
|
||||
"""List active competitor relationships for a company, ordered by strength descending."""
|
||||
"""List active competitor relationships for a company, ordered by strength descending.
|
||||
|
||||
Enriches each relationship with the ticker and legal_name of the
|
||||
*other* company (the one that isn't company_id).
|
||||
"""
|
||||
pool = _get_pool(request)
|
||||
|
||||
if not await _company_exists(pool, company_id):
|
||||
raise HTTPException(404, "Company not found")
|
||||
|
||||
rows = await pool.fetch(
|
||||
"""SELECT id, company_a_id, company_b_id, relationship_type, strength,
|
||||
bidirectional, source, active, created_at, updated_at
|
||||
FROM competitor_relationships
|
||||
WHERE (company_a_id = $1 OR company_b_id = $1) AND active = TRUE
|
||||
ORDER BY strength DESC""",
|
||||
"""SELECT cr.id, cr.company_a_id, cr.company_b_id,
|
||||
cr.relationship_type, cr.strength,
|
||||
cr.bidirectional, cr.source, cr.active,
|
||||
cr.created_at, cr.updated_at,
|
||||
c.ticker, c.legal_name
|
||||
FROM competitor_relationships cr
|
||||
LEFT JOIN companies c
|
||||
ON c.id = CASE
|
||||
WHEN cr.company_a_id = $1 THEN cr.company_b_id
|
||||
ELSE cr.company_a_id
|
||||
END
|
||||
WHERE (cr.company_a_id = $1 OR cr.company_b_id = $1)
|
||||
AND cr.active = TRUE
|
||||
ORDER BY cr.strength DESC""",
|
||||
company_id,
|
||||
)
|
||||
return [_row_dict(r) for r in rows]
|
||||
|
||||
Reference in New Issue
Block a user