112112</style >
113113
114114<script >
115- // API endpoint - configure based on deployment
116- const API_BASE = ' https://api.osc.earth/osa' ;
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' ;
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+ }
117130
118131async function fetchStatus () {
119132 const container = document .getElementById (' status-container' );
@@ -134,15 +147,19 @@ async function fetchStatus() {
134147
135148function formatDate (isoString ) {
136149 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- });
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+ }
146163}
147164
148165function formatAge (hours ) {
@@ -164,7 +181,10 @@ function getHealthText(healthy, ageHours) {
164181}
165182
166183function renderStatus (container , data ) {
167- const { github , papers , scheduler , health } = data;
184+ const github = data? .github || {};
185+ const papers = data? .papers || {};
186+ const scheduler = data? .scheduler || {};
187+ const health = data? .health || {};
168188
169189 container .innerHTML = `
170190 <div class =" status-grid" >
@@ -175,30 +195,30 @@ function renderStatus(container, data) {
175195 </h3 >
176196 <div class =" status-details" >
177197 <span class =" status-label" >Status:</span >
178- <span class =" status-value" >${ getHealthText (health .github_healthy , health .github_age_hours )} </span >
198+ <span class =" status-value" >${ escapeHtml ( getHealthText (health .github_healthy , health .github_age_hours ) )} </span >
179199
180200 <span class =" status-label" >Last sync:</span >
181- <span class =" status-value" >${ formatAge (health .github_age_hours )} </span >
201+ <span class =" status-value" >${ escapeHtml ( formatAge (health .github_age_hours ) )} </span >
182202
183203 <span class =" status-label" >Total items:</span >
184- <span class =" status-value" >${ github .total_items .toLocaleString ()} </span >
204+ <span class =" status-value" >${ ( github .total_items ?? 0 ) .toLocaleString ()}< / span>
185205
186206 < span class = " status-label" > Issues: < / span>
187- <span class =" status-value" >${ github .issues .toLocaleString ()} </span >
207+ < span class = " status-value" > ${( github .issues ?? 0 ) .toLocaleString ()}< / span>
188208
189209 < span class = " status-label" > Pull Requests: < / span>
190- <span class =" status-value" >${ github .prs .toLocaleString ()} </span >
210+ < span class = " status-value" > ${( github .prs ?? 0 ) .toLocaleString ()}< / span>
191211
192212 < span class = " status-label" > Open items: < / span>
193- <span class =" status-value" >${ github .open_items .toLocaleString ()} </span >
213+ < span class = " status-value" > ${( github .open_items ?? 0 ) .toLocaleString ()}< / span>
194214 < / div>
195215
196216 < div class = " repo-list" >
197217 < strong> Repositories: < / strong>
198- ${ Object .entries (github .repos ).map (([repo , info ]) => `
218+ ${Object .entries (github .repos || {} ).map (([repo , info ]) => `
199219 <div class="repo-item">
200- ${ repo} : ${ info .items } items
201- ${ info .last_sync ? ` (synced ${ formatDate (info .last_sync )} )` : ' ' }
220+ ${ escapeHtml ( repo) } : ${ ( info? .items ?? 0 ) } items
221+ ${info? .last_sync ? ` (synced ${ escapeHtml ( formatDate (info .last_sync ) )} )` : ' ' }
202222 < / div>
203223 ` ).join('')}
204224 </div>
@@ -211,21 +231,21 @@ function renderStatus(container, data) {
211231 </h3>
212232 <div class="status-details">
213233 <span class="status-label">Status:</span>
214- <span class =" status-value" >${ getHealthText (health .papers_healthy , health .papers_age_hours )} </span >
234+ <span class="status-value">${ escapeHtml ( getHealthText (health .papers_healthy , health .papers_age_hours ) )} </span>
215235
216236 <span class="status-label">Last sync:</span>
217- <span class =" status-value" >${ formatAge (health .papers_age_hours )} </span >
237+ <span class="status-value">${ escapeHtml ( formatAge (health .papers_age_hours ) )} </span>
218238
219239 <span class="status-label">Total papers:</span>
220- <span class =" status-value" >${ papers .total_items .toLocaleString ()} </span >
240+ <span class="status-value">${ ( papers .total_items ?? 0 ) .toLocaleString ()}< / span>
221241 < / div>
222242
223243 < div class = " source-list" >
224244 < strong> Sources: < / strong>
225- ${ Object .entries (papers .sources ).map (([source , info ]) => `
245+ ${Object .entries (papers .sources || {} ).map (([source , info ]) => `
226246 <div class="source-item">
227- ${ source} : ${ info .items } papers
228- ${ info .last_sync ? ` (synced ${ formatDate (info .last_sync )} )` : ' ' }
247+ ${ escapeHtml ( source) } : ${ ( info? .items ?? 0 ) } papers
248+ ${info? .last_sync ? ` (synced ${ escapeHtml ( formatDate (info .last_sync ) )} )` : ' ' }
229249 < / div>
230250 ` ).join('')}
231251 </div>
@@ -241,38 +261,44 @@ function renderStatus(container, data) {
241261 <span class="status-value">${ scheduler .running ? ' Yes' : ' No' } </span>
242262
243263 <span class="status-label">GitHub schedule:</span>
244- <span class =" status-value" >${ scheduler .github_cron } (UTC)</span >
264+ <span class="status-value">${ escapeHtml ( scheduler .github_cron ?? ' Not configured ' ) } (UTC )< / span>
245265
246266 < span class = " status-label" > Papers schedule: < / span>
247- <span class =" status-value" >${ scheduler .papers_cron } (UTC)</span >
267+ < span class = " status-value" > ${escapeHtml ( scheduler .papers_cron ?? ' Not configured ' ) } (UTC )< / span>
248268
249269 ${scheduler .next_github_sync ? `
250270 <span class="status-label">Next GitHub sync:</span>
251- <span class="status-value">${ formatDate (scheduler .next_github_sync )} </span>
271+ <span class="status-value">${ escapeHtml ( formatDate (scheduler .next_github_sync ) )} </span>
252272 ` : ' ' }
253273
254274 ${scheduler .next_papers_sync ? `
255275 <span class="status-label">Next papers sync:</span>
256- <span class="status-value">${ formatDate (scheduler .next_papers_sync )} </span>
276+ <span class="status-value">${ escapeHtml ( formatDate (scheduler .next_papers_sync ) )} </span>
257277 ` : ' ' }
258278 < / div>
259279 < / div>
260280 < / div>
261281
262282 < div class = " last-updated" >
263- Last checked: ${ new Date ().toLocaleString ()}
283+ Last checked: ${escapeHtml ( new Date ().toLocaleString () )}
264284 < br>
265285 < button class = " refresh-btn" onclick= " refreshStatus()" > Refresh< / button>
266286 < / div>
267287 ` ;
268288}
269289
270290function 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+
271297 container.innerHTML = `
272298 < div class = " status-error" >
273299 < strong> Unable to fetch status< / strong>
274- <p >${ error .message } </p >
275- <p >The API may be temporarily unavailable. Try again later. </p >
300+ < p> ${escapeHtml ( error .message ) }< / p>
301+ < p> ${ escapeHtml (hint)} < / p>
276302 < button class = " refresh-btn" onclick= " refreshStatus()" > Retry< / button>
277303 < / div>
278304 ` ;
@@ -320,4 +346,7 @@ The status data is fetched from the OSA API:
320346GET / sync/ status
321347` ` `
322348
323- See the [ API Reference] ( api-reference.md ) for more details.
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.
0 commit comments