|
1 | | -# Sync Status |
| 1 | +# Service Status |
2 | 2 |
|
3 | | -<div id="status-container"> |
4 | | - <div class="status-loading">Loading status...</div> |
5 | | -</div> |
| 3 | +!!! warning "This page has moved" |
| 4 | + The OSA status dashboard has moved to a new location with expanded metrics, |
| 5 | + usage charts, and per-community views. |
6 | 6 |
|
7 | | -<style> |
8 | | -.status-loading { |
9 | | - padding: 2rem; |
10 | | - text-align: center; |
11 | | - color: var(--md-default-fg-color--light); |
12 | | -} |
| 7 | + **[View the new dashboard at status.osc.earth/osa](https://status.osc.earth/osa/)** |
13 | 8 |
|
14 | | -.status-error { |
15 | | - padding: 1rem; |
16 | | - background: var(--md-code-bg-color); |
17 | | - border-left: 4px solid #f44336; |
18 | | - margin: 1rem 0; |
19 | | -} |
| 9 | +The new dashboard includes: |
20 | 10 |
|
21 | | -.status-grid { |
22 | | - display: grid; |
23 | | - gap: 1.5rem; |
24 | | - margin-top: 1rem; |
25 | | -} |
| 11 | +- **Aggregate overview** with total requests, error rates, and community breakdown |
| 12 | +- **Per-community views** with usage charts and tool statistics |
| 13 | +- **Sync health status** for GitHub and papers knowledge sources |
| 14 | +- **Admin section** for token usage and cost metrics (requires API key) |
26 | 15 |
|
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 - allow override via query param for development/testing |
116 | | -// Usage: ?api=http://localhost:38528 for local testing |
117 | | -const urlParams = new URLSearchParams(window.location.search); |
118 | | -const API_BASE = urlParams.get('api') || 'https://api.osc.earth/osa-dev'; |
119 | | - |
120 | | -// HTML escape to prevent XSS from API response data |
121 | | -function escapeHtml(str) { |
122 | | - if (str === null || str === undefined) return ''; |
123 | | - return String(str) |
124 | | - .replace(/&/g, '&') |
125 | | - .replace(/</g, '<') |
126 | | - .replace(/>/g, '>') |
127 | | - .replace(/"/g, '"') |
128 | | - .replace(/'/g, '''); |
129 | | -} |
130 | | - |
131 | | -async function fetchStatus() { |
132 | | - const container = document.getElementById('status-container'); |
133 | | - |
134 | | - try { |
135 | | - const response = await fetch(`${API_BASE}/sync/status`); |
136 | | - |
137 | | - if (!response.ok) { |
138 | | - throw new Error(`API returned ${response.status}`); |
139 | | - } |
140 | | - |
141 | | - const data = await response.json(); |
142 | | - renderStatus(container, data); |
143 | | - } catch (error) { |
144 | | - renderError(container, error); |
145 | | - } |
146 | | -} |
147 | | - |
148 | | -function formatDate(isoString) { |
149 | | - if (!isoString) return 'Never'; |
150 | | - try { |
151 | | - const date = new Date(isoString); |
152 | | - return date.toLocaleString('en-US', { |
153 | | - year: 'numeric', |
154 | | - month: 'short', |
155 | | - day: 'numeric', |
156 | | - hour: '2-digit', |
157 | | - minute: '2-digit', |
158 | | - timeZoneName: 'short' |
159 | | - }); |
160 | | - } catch { |
161 | | - return 'Invalid date'; |
162 | | - } |
163 | | -} |
164 | | - |
165 | | -function formatAge(hours) { |
166 | | - if (hours === null || hours === undefined) return 'Never synced'; |
167 | | - if (hours < 1) return 'Less than an hour ago'; |
168 | | - if (hours < 24) return `${Math.round(hours)} hours ago`; |
169 | | - const days = Math.round(hours / 24); |
170 | | - return `${days} day${days === 1 ? '' : 's'} ago`; |
171 | | -} |
172 | | - |
173 | | -function getHealthClass(healthy) { |
174 | | - return healthy ? 'status-healthy' : 'status-error-indicator'; |
175 | | -} |
176 | | - |
177 | | -function getHealthText(healthy, ageHours) { |
178 | | - if (healthy) return 'Healthy'; |
179 | | - if (ageHours === null) return 'Pending'; |
180 | | - return 'Stale'; |
181 | | -} |
182 | | - |
183 | | -function renderStatus(container, data) { |
184 | | - const github = data?.github || {}; |
185 | | - const papers = data?.papers || {}; |
186 | | - const scheduler = data?.scheduler || {}; |
187 | | - const health = data?.health || {}; |
188 | | - |
189 | | - container.innerHTML = ` |
190 | | - <div class="status-grid"> |
191 | | - <div class="status-card"> |
192 | | - <h3> |
193 | | - <span class="status-indicator ${getHealthClass(health.github_healthy)}"></span> |
194 | | - GitHub Sync |
195 | | - </h3> |
196 | | - <div class="status-details"> |
197 | | - <span class="status-label">Status:</span> |
198 | | - <span class="status-value">${escapeHtml(getHealthText(health.github_healthy, health.github_age_hours))}</span> |
199 | | -
|
200 | | - <span class="status-label">Last sync:</span> |
201 | | - <span class="status-value">${escapeHtml(formatAge(health.github_age_hours))}</span> |
202 | | -
|
203 | | - <span class="status-label">Total items:</span> |
204 | | - <span class="status-value">${(github.total_items ?? 0).toLocaleString()}</span> |
205 | | -
|
206 | | - <span class="status-label">Issues:</span> |
207 | | - <span class="status-value">${(github.issues ?? 0).toLocaleString()}</span> |
208 | | -
|
209 | | - <span class="status-label">Pull Requests:</span> |
210 | | - <span class="status-value">${(github.prs ?? 0).toLocaleString()}</span> |
211 | | -
|
212 | | - <span class="status-label">Open items:</span> |
213 | | - <span class="status-value">${(github.open_items ?? 0).toLocaleString()}</span> |
214 | | - </div> |
215 | | -
|
216 | | - <div class="repo-list"> |
217 | | - <strong>Repositories:</strong> |
218 | | - ${Object.entries(github.repos || {}).map(([repo, info]) => ` |
219 | | - <div class="repo-item"> |
220 | | - ${escapeHtml(repo)}: ${(info?.items ?? 0)} items |
221 | | - ${info?.last_sync ? `(synced ${escapeHtml(formatDate(info.last_sync))})` : ''} |
222 | | - </div> |
223 | | - `).join('')} |
224 | | - </div> |
225 | | - </div> |
226 | | -
|
227 | | - <div class="status-card"> |
228 | | - <h3> |
229 | | - <span class="status-indicator ${getHealthClass(health.papers_healthy)}"></span> |
230 | | - Papers Sync |
231 | | - </h3> |
232 | | - <div class="status-details"> |
233 | | - <span class="status-label">Status:</span> |
234 | | - <span class="status-value">${escapeHtml(getHealthText(health.papers_healthy, health.papers_age_hours))}</span> |
235 | | -
|
236 | | - <span class="status-label">Last sync:</span> |
237 | | - <span class="status-value">${escapeHtml(formatAge(health.papers_age_hours))}</span> |
238 | | -
|
239 | | - <span class="status-label">Total papers:</span> |
240 | | - <span class="status-value">${(papers.total_items ?? 0).toLocaleString()}</span> |
241 | | - </div> |
242 | | -
|
243 | | - <div class="source-list"> |
244 | | - <strong>Sources:</strong> |
245 | | - ${Object.entries(papers.sources || {}).map(([source, info]) => ` |
246 | | - <div class="source-item"> |
247 | | - ${escapeHtml(source)}: ${(info?.items ?? 0)} papers |
248 | | - ${info?.last_sync ? `(synced ${escapeHtml(formatDate(info.last_sync))})` : ''} |
249 | | - </div> |
250 | | - `).join('')} |
251 | | - </div> |
252 | | - </div> |
253 | | -
|
254 | | - <div class="status-card scheduler-info"> |
255 | | - <h3>Scheduler</h3> |
256 | | - <div class="status-details"> |
257 | | - <span class="status-label">Enabled:</span> |
258 | | - <span class="status-value">${scheduler.enabled ? 'Yes' : 'No'}</span> |
259 | | -
|
260 | | - <span class="status-label">Running:</span> |
261 | | - <span class="status-value">${scheduler.running ? 'Yes' : 'No'}</span> |
262 | | -
|
263 | | - <span class="status-label">GitHub schedule:</span> |
264 | | - <span class="status-value">${escapeHtml(scheduler.github_cron ?? 'Not configured')} (UTC)</span> |
265 | | -
|
266 | | - <span class="status-label">Papers schedule:</span> |
267 | | - <span class="status-value">${escapeHtml(scheduler.papers_cron ?? 'Not configured')} (UTC)</span> |
268 | | -
|
269 | | - ${scheduler.next_github_sync ? ` |
270 | | - <span class="status-label">Next GitHub sync:</span> |
271 | | - <span class="status-value">${escapeHtml(formatDate(scheduler.next_github_sync))}</span> |
272 | | - ` : ''} |
273 | | -
|
274 | | - ${scheduler.next_papers_sync ? ` |
275 | | - <span class="status-label">Next papers sync:</span> |
276 | | - <span class="status-value">${escapeHtml(formatDate(scheduler.next_papers_sync))}</span> |
277 | | - ` : ''} |
278 | | - </div> |
279 | | - </div> |
280 | | - </div> |
281 | | -
|
282 | | - <div class="last-updated"> |
283 | | - Last checked: ${escapeHtml(new Date().toLocaleString())} |
284 | | - <br> |
285 | | - <button class="refresh-btn" onclick="refreshStatus()">Refresh</button> |
286 | | - </div> |
287 | | - `; |
288 | | -} |
289 | | -
|
290 | | -function renderError(container, error) { |
291 | | - // Detect likely CORS or network issues |
292 | | - let hint = 'The API may be temporarily unavailable. Try again later.'; |
293 | | - if (error instanceof TypeError && error.message.includes('fetch')) { |
294 | | - hint = 'Unable to reach the API. This may be a network or CORS issue.'; |
295 | | - } |
296 | | -
|
297 | | - container.innerHTML = ` |
298 | | - <div class="status-error"> |
299 | | - <strong>Unable to fetch status</strong> |
300 | | - <p>${escapeHtml(error.message)}</p> |
301 | | - <p>${escapeHtml(hint)}</p> |
302 | | - <button class="refresh-btn" onclick="refreshStatus()">Retry</button> |
303 | | - </div> |
304 | | - `; |
305 | | -} |
306 | | -
|
307 | | -async function refreshStatus() { |
308 | | - const btn = document.querySelector('.refresh-btn'); |
309 | | - if (btn) { |
310 | | - btn.disabled = true; |
311 | | - btn.textContent = 'Refreshing...'; |
312 | | - } |
313 | | -
|
314 | | - await fetchStatus(); |
315 | | -
|
316 | | - if (btn) { |
317 | | - btn.disabled = false; |
318 | | - btn.textContent = 'Refresh'; |
319 | | - } |
320 | | -} |
321 | | -
|
322 | | -// Fetch status on page load |
323 | | -document.addEventListener('DOMContentLoaded', fetchStatus); |
324 | | -</script> |
325 | | -
|
326 | | -## About This Page |
327 | | -
|
328 | | -This page shows the real-time status of OSA's knowledge sync jobs. The knowledge database automatically syncs: |
329 | | -
|
330 | | -- **GitHub Issues/PRs**: Daily at 2am UTC from HED repositories |
331 | | -- **Academic Papers**: Weekly Sunday at 3am UTC from OpenALEX, Semantic Scholar, and PubMed |
332 | | -
|
333 | | -### Health Indicators |
334 | | -
|
335 | | -| Status | Meaning | |
336 | | -|--------|---------| |
337 | | -| :material-circle:{ style="color: #4caf50" } Healthy | Sync completed within expected timeframe | |
338 | | -| :material-circle:{ style="color: #ff9800" } Pending | Never synced (new installation) | |
339 | | -| :material-circle:{ style="color: #f44336" } Stale | Sync is overdue | |
340 | | -
|
341 | | -### API Endpoint |
342 | | -
|
343 | | -The status data is fetched from the OSA API: |
344 | | -
|
345 | | -``` |
346 | | -GET /sync/status |
347 | | -``` |
348 | | -
|
349 | | -Returns JSON with `github`, `papers`, `scheduler`, and `health` objects. |
350 | | -
|
351 | | -!!! tip "Local Testing" |
352 | | - To test with a local API server, add `?api=http://localhost:38528` to this page's URL. |
| 16 | +To view a specific community, visit `https://status.osc.earth/osa/{community_id}` (e.g., [status.osc.earth/osa/hed](https://status.osc.earth/osa/hed)). |
0 commit comments