Skip to content

Commit ed6efe3

Browse files
docs: add sync status page with live API fetch
- Add status.md with JavaScript to fetch from OSA /sync/status API - Display GitHub and papers sync status with health indicators - Show scheduler information and next run times - Add to navigation in mkdocs.yml Closes #3
1 parent 36b946e commit ed6efe3

2 files changed

Lines changed: 324 additions & 0 deletions

File tree

docs/osa/status.md

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# Sync Status
2+
3+
<div id="status-container">
4+
<div class="status-loading">Loading status...</div>
5+
</div>
6+
7+
<style>
8+
.status-loading {
9+
padding: 2rem;
10+
text-align: center;
11+
color: var(--md-default-fg-color--light);
12+
}
13+
14+
.status-error {
15+
padding: 1rem;
16+
background: var(--md-code-bg-color);
17+
border-left: 4px solid #f44336;
18+
margin: 1rem 0;
19+
}
20+
21+
.status-grid {
22+
display: grid;
23+
gap: 1.5rem;
24+
margin-top: 1rem;
25+
}
26+
27+
.status-card {
28+
background: var(--md-code-bg-color);
29+
border-radius: 8px;
30+
padding: 1.5rem;
31+
border: 1px solid var(--md-default-fg-color--lightest);
32+
}
33+
34+
.status-card h3 {
35+
margin-top: 0;
36+
display: flex;
37+
align-items: center;
38+
gap: 0.5rem;
39+
}
40+
41+
.status-indicator {
42+
display: inline-block;
43+
width: 12px;
44+
height: 12px;
45+
border-radius: 50%;
46+
margin-right: 0.5rem;
47+
}
48+
49+
.status-healthy { background: #4caf50; }
50+
.status-warning { background: #ff9800; }
51+
.status-error-indicator { background: #f44336; }
52+
53+
.status-details {
54+
display: grid;
55+
grid-template-columns: auto 1fr;
56+
gap: 0.5rem 1rem;
57+
margin-top: 1rem;
58+
font-size: 0.9rem;
59+
}
60+
61+
.status-label {
62+
color: var(--md-default-fg-color--light);
63+
}
64+
65+
.status-value {
66+
font-family: var(--md-code-font-family);
67+
}
68+
69+
.scheduler-info {
70+
margin-top: 1.5rem;
71+
padding-top: 1rem;
72+
border-top: 1px solid var(--md-default-fg-color--lightest);
73+
}
74+
75+
.repo-list, .source-list {
76+
margin-top: 0.5rem;
77+
padding-left: 1rem;
78+
font-size: 0.85rem;
79+
}
80+
81+
.repo-item, .source-item {
82+
padding: 0.25rem 0;
83+
color: var(--md-default-fg-color--light);
84+
}
85+
86+
.last-updated {
87+
margin-top: 1.5rem;
88+
font-size: 0.8rem;
89+
color: var(--md-default-fg-color--light);
90+
text-align: center;
91+
}
92+
93+
.refresh-btn {
94+
background: var(--md-primary-fg-color);
95+
color: var(--md-primary-bg-color);
96+
border: none;
97+
padding: 0.5rem 1rem;
98+
border-radius: 4px;
99+
cursor: pointer;
100+
font-size: 0.9rem;
101+
margin-top: 1rem;
102+
}
103+
104+
.refresh-btn:hover {
105+
opacity: 0.9;
106+
}
107+
108+
.refresh-btn:disabled {
109+
opacity: 0.5;
110+
cursor: not-allowed;
111+
}
112+
</style>
113+
114+
<script>
115+
// API endpoint - configure based on deployment
116+
const API_BASE = 'https://api.osc.earth/osa';
117+
118+
async function fetchStatus() {
119+
const container = document.getElementById('status-container');
120+
121+
try {
122+
const response = await fetch(`${API_BASE}/sync/status`);
123+
124+
if (!response.ok) {
125+
throw new Error(`API returned ${response.status}`);
126+
}
127+
128+
const data = await response.json();
129+
renderStatus(container, data);
130+
} catch (error) {
131+
renderError(container, error);
132+
}
133+
}
134+
135+
function formatDate(isoString) {
136+
if (!isoString) return 'Never';
137+
const date = new Date(isoString);
138+
return date.toLocaleString('en-US', {
139+
year: 'numeric',
140+
month: 'short',
141+
day: 'numeric',
142+
hour: '2-digit',
143+
minute: '2-digit',
144+
timeZoneName: 'short'
145+
});
146+
}
147+
148+
function formatAge(hours) {
149+
if (hours === null || hours === undefined) return 'Never synced';
150+
if (hours < 1) return 'Less than an hour ago';
151+
if (hours < 24) return `${Math.round(hours)} hours ago`;
152+
const days = Math.round(hours / 24);
153+
return `${days} day${days === 1 ? '' : 's'} ago`;
154+
}
155+
156+
function getHealthClass(healthy) {
157+
return healthy ? 'status-healthy' : 'status-error-indicator';
158+
}
159+
160+
function getHealthText(healthy, ageHours) {
161+
if (healthy) return 'Healthy';
162+
if (ageHours === null) return 'Pending';
163+
return 'Stale';
164+
}
165+
166+
function renderStatus(container, data) {
167+
const { github, papers, scheduler, health } = data;
168+
169+
container.innerHTML = `
170+
<div class="status-grid">
171+
<div class="status-card">
172+
<h3>
173+
<span class="status-indicator ${getHealthClass(health.github_healthy)}"></span>
174+
GitHub Sync
175+
</h3>
176+
<div class="status-details">
177+
<span class="status-label">Status:</span>
178+
<span class="status-value">${getHealthText(health.github_healthy, health.github_age_hours)}</span>
179+
180+
<span class="status-label">Last sync:</span>
181+
<span class="status-value">${formatAge(health.github_age_hours)}</span>
182+
183+
<span class="status-label">Total items:</span>
184+
<span class="status-value">${github.total_items.toLocaleString()}</span>
185+
186+
<span class="status-label">Issues:</span>
187+
<span class="status-value">${github.issues.toLocaleString()}</span>
188+
189+
<span class="status-label">Pull Requests:</span>
190+
<span class="status-value">${github.prs.toLocaleString()}</span>
191+
192+
<span class="status-label">Open items:</span>
193+
<span class="status-value">${github.open_items.toLocaleString()}</span>
194+
</div>
195+
196+
<div class="repo-list">
197+
<strong>Repositories:</strong>
198+
${Object.entries(github.repos).map(([repo, info]) => `
199+
<div class="repo-item">
200+
${repo}: ${info.items} items
201+
${info.last_sync ? `(synced ${formatDate(info.last_sync)})` : ''}
202+
</div>
203+
`).join('')}
204+
</div>
205+
</div>
206+
207+
<div class="status-card">
208+
<h3>
209+
<span class="status-indicator ${getHealthClass(health.papers_healthy)}"></span>
210+
Papers Sync
211+
</h3>
212+
<div class="status-details">
213+
<span class="status-label">Status:</span>
214+
<span class="status-value">${getHealthText(health.papers_healthy, health.papers_age_hours)}</span>
215+
216+
<span class="status-label">Last sync:</span>
217+
<span class="status-value">${formatAge(health.papers_age_hours)}</span>
218+
219+
<span class="status-label">Total papers:</span>
220+
<span class="status-value">${papers.total_items.toLocaleString()}</span>
221+
</div>
222+
223+
<div class="source-list">
224+
<strong>Sources:</strong>
225+
${Object.entries(papers.sources).map(([source, info]) => `
226+
<div class="source-item">
227+
${source}: ${info.items} papers
228+
${info.last_sync ? `(synced ${formatDate(info.last_sync)})` : ''}
229+
</div>
230+
`).join('')}
231+
</div>
232+
</div>
233+
234+
<div class="status-card scheduler-info">
235+
<h3>Scheduler</h3>
236+
<div class="status-details">
237+
<span class="status-label">Enabled:</span>
238+
<span class="status-value">${scheduler.enabled ? 'Yes' : 'No'}</span>
239+
240+
<span class="status-label">Running:</span>
241+
<span class="status-value">${scheduler.running ? 'Yes' : 'No'}</span>
242+
243+
<span class="status-label">GitHub schedule:</span>
244+
<span class="status-value">${scheduler.github_cron} (UTC)</span>
245+
246+
<span class="status-label">Papers schedule:</span>
247+
<span class="status-value">${scheduler.papers_cron} (UTC)</span>
248+
249+
${scheduler.next_github_sync ? `
250+
<span class="status-label">Next GitHub sync:</span>
251+
<span class="status-value">${formatDate(scheduler.next_github_sync)}</span>
252+
` : ''}
253+
254+
${scheduler.next_papers_sync ? `
255+
<span class="status-label">Next papers sync:</span>
256+
<span class="status-value">${formatDate(scheduler.next_papers_sync)}</span>
257+
` : ''}
258+
</div>
259+
</div>
260+
</div>
261+
262+
<div class="last-updated">
263+
Last checked: ${new Date().toLocaleString()}
264+
<br>
265+
<button class="refresh-btn" onclick="refreshStatus()">Refresh</button>
266+
</div>
267+
`;
268+
}
269+
270+
function renderError(container, error) {
271+
container.innerHTML = `
272+
<div class="status-error">
273+
<strong>Unable to fetch status</strong>
274+
<p>${error.message}</p>
275+
<p>The API may be temporarily unavailable. Try again later.</p>
276+
<button class="refresh-btn" onclick="refreshStatus()">Retry</button>
277+
</div>
278+
`;
279+
}
280+
281+
async function refreshStatus() {
282+
const btn = document.querySelector('.refresh-btn');
283+
if (btn) {
284+
btn.disabled = true;
285+
btn.textContent = 'Refreshing...';
286+
}
287+
288+
await fetchStatus();
289+
290+
if (btn) {
291+
btn.disabled = false;
292+
btn.textContent = 'Refresh';
293+
}
294+
}
295+
296+
// Fetch status on page load
297+
document.addEventListener('DOMContentLoaded', fetchStatus);
298+
</script>
299+
300+
## About This Page
301+
302+
This page shows the real-time status of OSA's knowledge sync jobs. The knowledge database automatically syncs:
303+
304+
- **GitHub Issues/PRs**: Daily at 2am UTC from HED repositories
305+
- **Academic Papers**: Weekly Sunday at 3am UTC from OpenALEX, Semantic Scholar, and PubMed
306+
307+
### Health Indicators
308+
309+
| Status | Meaning |
310+
|--------|---------|
311+
| :material-circle:{ style="color: #4caf50" } Healthy | Sync completed within expected timeframe |
312+
| :material-circle:{ style="color: #ff9800" } Pending | Never synced (new installation) |
313+
| :material-circle:{ style="color: #f44336" } Stale | Sync is overdue |
314+
315+
### API Endpoint
316+
317+
The status data is fetched from the OSA API:
318+
319+
```
320+
GET /sync/status
321+
```
322+
323+
See the [API Reference](api-reference.md) for more details.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ nav:
156156
- CLI Reference: osa/cli-reference.md
157157
- API Reference: osa/api-reference.md
158158
- Knowledge Sync: osa/knowledge-sync.md
159+
- Sync Status: osa/status.md
159160
- Tools:
160161
- osa/tools/index.md
161162
- HED Tools: osa/tools/hed.md

0 commit comments

Comments
 (0)