/api/status, for Homepage's dashboard (#67)
The iPX tile on Homepage was a bare link: nothing in ipx gave a summary a customapi widget could read. /api/status serves what `ipx status` prints (feeds, items pending, files downloaded), from the same function the control socket answers with, plus the version. It sits behind sign-in like the rest of /api; Homepage sends the shared [web] token as the ipx_token cookie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `/api/status` gives the number of feeds, items waiting to download and files downloaded, and
|
||||||
|
the version, for a dashboard such as Homepage.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- The Modern theme takes its colours from the new logo: its navy, the blue of its bars and the
|
- The Modern theme takes its colours from the new logo: its navy, the blue of its bars and the
|
||||||
|
|||||||
@@ -369,9 +369,9 @@ async fn run(ctx: &Arc<Ctx>, cmd: Cmd) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The counts `ipx status` prints. A running daemon's socket answers with this directly rather
|
/// The counts `ipx status` prints, and /api/status serves. A running daemon's socket answers with
|
||||||
/// than through the job queue.
|
/// this directly rather than through the job queue.
|
||||||
async fn status(ctx: &Ctx) -> Event {
|
pub(crate) async fn status(ctx: &Ctx) -> Event {
|
||||||
match ctx.db.counts().await {
|
match ctx.db.counts().await {
|
||||||
Ok((pending, downloaded)) => {
|
Ok((pending, downloaded)) => {
|
||||||
let feeds = subscriptions(ctx).await.map(|s| s.len()).unwrap_or(0);
|
let feeds = subscriptions(ctx).await.map(|s| s.len()).unwrap_or(0);
|
||||||
|
|||||||
17
src/web.rs
17
src/web.rs
@@ -52,6 +52,7 @@ pub fn router(state: WebState) -> Router {
|
|||||||
.route("/api/fetch", post(fetch_now))
|
.route("/api/fetch", post(fetch_now))
|
||||||
.route("/api/opml", get(export_opml).post(import_opml))
|
.route("/api/opml", get(export_opml).post(import_opml))
|
||||||
.route("/api/settings", get(get_settings).patch(patch_settings))
|
.route("/api/settings", get(get_settings).patch(patch_settings))
|
||||||
|
.route("/api/status", get(status))
|
||||||
.route("/api/popular", get(get_popular))
|
.route("/api/popular", get(get_popular))
|
||||||
.route("/api/directory", get(get_directory))
|
.route("/api/directory", get(get_directory))
|
||||||
.route("/api/popular/{id}", post(subscribe_popular))
|
.route("/api/popular/{id}", post(subscribe_popular))
|
||||||
@@ -1808,6 +1809,22 @@ async fn import_opml(
|
|||||||
Ok(Json(serde_json::json!({ "added": added, "already": already })))
|
Ok(Json(serde_json::json!({ "added": added, "already": already })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The numbers `ipx status` prints, and the version, for a dashboard such as Homepage's
|
||||||
|
/// customapi widget. Behind sign-in like the rest of /api; Homepage sends the shared token.
|
||||||
|
async fn status(State(state): State<WebState>) -> Response {
|
||||||
|
match crate::status(&state.ctx).await {
|
||||||
|
Event::Status { feeds, pending, downloaded } => Json(serde_json::json!({
|
||||||
|
"feeds": feeds,
|
||||||
|
"pending": pending,
|
||||||
|
"downloaded": downloaded,
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
}))
|
||||||
|
.into_response(),
|
||||||
|
Event::Error { msg } => (StatusCode::INTERNAL_SERVER_ERROR, msg).into_response(),
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Settings {
|
struct Settings {
|
||||||
schedule: String,
|
schedule: String,
|
||||||
|
|||||||
@@ -1197,6 +1197,17 @@ test('the logo is the dark one in dark mode and the light one in light mode', as
|
|||||||
await page.evaluate(() => setTheme('modern', 'dark'));
|
await page.evaluate(() => setTheme('modern', 'dark'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('/api/status gives a dashboard the counts, with the shared token as a cookie', async ({ page }) => {
|
||||||
|
// As Homepage's customapi widget asks: no session, only the token in a Cookie header.
|
||||||
|
const r = await page.request.get('/api/status', { headers: { cookie: `ipx_token=${TOKEN}` } });
|
||||||
|
expect(r.status()).toBe(200);
|
||||||
|
const s = await r.json();
|
||||||
|
for (const k of ['feeds', 'pending', 'downloaded']) expect(typeof s[k], k).toBe('number');
|
||||||
|
expect(s.version).toMatch(/^\d+\.\d+\.\d+/);
|
||||||
|
const none = await page.request.get('/api/status', { headers: { cookie: '' } });
|
||||||
|
expect(none.status()).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
test('the favicon is the logo, square, from both pages', async ({ page }) => {
|
test('the favicon is the logo, square, from both pages', async ({ page }) => {
|
||||||
await page.evaluate(() => setTheme('modern', 'light'));
|
await page.evaluate(() => setTheme('modern', 'light'));
|
||||||
await expect(page.locator('#favicon')).toHaveAttribute('href', /^\/favicon\.png\?v=[0-9a-f]{12}$/);
|
await expect(page.locator('#favicon')).toHaveAttribute('href', /^\/favicon\.png\?v=[0-9a-f]{12}$/);
|
||||||
|
|||||||
Reference in New Issue
Block a user