Skip to content

Commit cb5cb5d

Browse files
Track and display last sync results in Overview
- Add last_sync_at and last_sync_results fields to Config model - Save sync results to database after each sync operation - Display last sync timestamp and results in Overview page - Show model count changes and error status with visual badges 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 36c5509 commit cb5cb5d

File tree

4 files changed

+84
-3
lines changed

4 files changed

+84
-3
lines changed

frontend/api.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,20 @@ async def index(request: Request, session = Depends(get_session)):
152152
for p in providers_list
153153
]
154154

155+
# Prepare last sync info
156+
last_sync_info = None
157+
if config.last_sync_at:
158+
last_sync_info = {
159+
"timestamp": config.last_sync_at,
160+
"results": config.last_sync_results_dict
161+
}
162+
155163
return templates.TemplateResponse("index.html", {
156164
"request": request,
157165
"providers": providers_list,
158166
"config": config_dict,
159167
"stats": stats,
160-
"last_synced": None
168+
"last_sync": last_sync_info
161169
})
162170

163171
@app.get("/sources", response_class=HTMLResponse)
@@ -373,6 +381,12 @@ async def manual_sync(request: Request, session = Depends(get_session)):
373381
results["providers"].append(provider_result)
374382

375383
results["success"] = len(results["errors"]) == 0
384+
385+
# Save sync results to config
386+
config.last_sync_at = datetime.now(timezone.utc)
387+
config.last_sync_results_dict = results
388+
await session.commit()
389+
376390
return results
377391

378392
@app.post("/admin/providers")

frontend/templates/index.html

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,26 @@ <h3>Sync</h3>
6767
{% else %}
6868
<p class="muted">Automatic sync disabled</p>
6969
{% endif %}
70-
{% if last_synced %}
71-
<p class="muted">Last synced at {{ last_synced }}</p>
70+
{% if last_sync %}
71+
<div class="last-sync-info">
72+
<p class="muted" style="margin-bottom: 0.5rem;">
73+
Last synced: {{ last_sync.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}
74+
</p>
75+
<div class="sync-stats">
76+
<span class="sync-stat-badge success">
77+
+{{ last_sync.results.total_models_fetched }} models
78+
</span>
79+
{% if last_sync.results.errors|length > 0 %}
80+
<span class="sync-stat-badge error">
81+
{{ last_sync.results.errors|length }} errors
82+
</span>
83+
{% else %}
84+
<span class="sync-stat-badge success">
85+
✓ OK
86+
</span>
87+
{% endif %}
88+
</div>
89+
</div>
7290
{% else %}
7391
<p class="muted">No sync completed yet</p>
7492
{% endif %}
@@ -226,6 +244,37 @@ <h2>Providers</h2>
226244
cursor: pointer;
227245
}
228246

247+
/* Sync Info Styles */
248+
.last-sync-info {
249+
margin-top: 0.5rem;
250+
}
251+
252+
.sync-stats {
253+
display: flex;
254+
gap: 0.5rem;
255+
flex-wrap: wrap;
256+
}
257+
258+
.sync-stat-badge {
259+
display: inline-block;
260+
padding: 0.3rem 0.7rem;
261+
border-radius: 12px;
262+
font-size: 0.85rem;
263+
font-weight: 500;
264+
}
265+
266+
.sync-stat-badge.success {
267+
background: #e8f5e9;
268+
color: #2e7d32;
269+
border: 1px solid #81c784;
270+
}
271+
272+
.sync-stat-badge.error {
273+
background: #ffebee;
274+
color: #c62828;
275+
border: 1px solid #ef5350;
276+
}
277+
229278
/* Sync Modal Styles */
230279
.sync-modal {
231280
display: none;

shared/database.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ async def ensure_minimum_schema(engine: AsyncEngine) -> None:
141141
await conn.exec_driver_sql("ALTER TABLE config ADD COLUMN default_pricing_profile VARCHAR")
142142
if "default_pricing_override" not in config_columns:
143143
await conn.exec_driver_sql("ALTER TABLE config ADD COLUMN default_pricing_override TEXT")
144+
if "last_sync_at" not in config_columns:
145+
await conn.exec_driver_sql("ALTER TABLE config ADD COLUMN last_sync_at DATETIME")
146+
if "last_sync_results" not in config_columns:
147+
await conn.exec_driver_sql("ALTER TABLE config ADD COLUMN last_sync_results TEXT")
144148

145149
# Manually commit all changes
146150
await conn.commit()

shared/db_models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ class Config(Base):
104104
sync_interval_seconds: Mapped[int] = mapped_column(Integer, default=300, nullable=False)
105105
default_pricing_profile: Mapped[str | None] = mapped_column(String, nullable=True)
106106
default_pricing_override: Mapped[str | None] = mapped_column(Text, nullable=True)
107+
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
108+
last_sync_results: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON object
107109
created_at: Mapped[datetime] = mapped_column(
108110
DateTime, default=lambda: datetime.now(UTC), nullable=False
109111
)
@@ -130,6 +132,18 @@ def default_pricing_override_dict(self, value: dict[str, Any] | None) -> None:
130132
"""Store global pricing override as JSON."""
131133
self.default_pricing_override = json.dumps(value) if value else None
132134

135+
@property
136+
def last_sync_results_dict(self) -> dict[str, Any]:
137+
"""Parsed last sync results JSON."""
138+
if not self.last_sync_results:
139+
return {}
140+
return json.loads(self.last_sync_results)
141+
142+
@last_sync_results_dict.setter
143+
def last_sync_results_dict(self, value: dict[str, Any] | None) -> None:
144+
"""Store last sync results as JSON."""
145+
self.last_sync_results = json.dumps(value) if value else None
146+
133147

134148
class Model(Base):
135149
"""Model metadata and configuration."""

0 commit comments

Comments
 (0)