mirror of
https://github.com/storytold/craft-repo-status-app.git
synced 2026-10-13 23:17:45 +00:00
Poll craft-launcher and figcraft; show inaccessible repos as empty rows
Add storytold/craft-launcher and storytold/figcraft to the default repo list and config template. GitHub answers 404 / NOT_FOUND for private repos the token can't see. Treat that as a successful poll with no data: the row renders blank (with a "Not accessible" tooltip) instead of as an error, and doesn't count toward the failed-repos banner or retry backoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
47aabd6635
commit
daebcb02fb
7 files changed
+33
-2
No files matched your search
@@ -17,6 +17,8 @@ repos = [
|
||||
"storytold/cadcraft",
|
||||
"storytold/soundcraft",
|
||||
"storytold/craft-libs",
|
||||
"storytold/craft-launcher",
|
||||
"storytold/figcraft",
|
||||
]
|
||||
|
||||
# How often to poll GitHub (minutes, at least 1). Each poll costs roughly
|
||||
|
||||
@@ -25,6 +25,8 @@ pub const DEFAULT_REPOS: &[&str] = &[
|
||||
"storytold/cadcraft",
|
||||
"storytold/soundcraft",
|
||||
"storytold/craft-libs",
|
||||
"storytold/craft-launcher",
|
||||
"storytold/figcraft",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -113,6 +113,9 @@ pub struct RepoEntry {
|
||||
pub last_attempt_at: Option<Time>,
|
||||
/// Error from the most recent attempt, cleared on success.
|
||||
pub error: Option<String>,
|
||||
/// GitHub reported the repo missing (e.g. private and not visible to the
|
||||
/// token); shown as an empty row rather than an error.
|
||||
pub inaccessible: bool,
|
||||
/// The app icon as a `data:` URI, downloaded once per blob id.
|
||||
pub icon: Option<String>,
|
||||
/// The blob `icon` was downloaded from; compared with `stats.icon_oid`.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use chrono::Utc;
|
||||
use craft_core::{Config, LagBasis, RepoEntry, Snapshot, ALL_TIME};
|
||||
use craft_github::{resolve_token, GitHub};
|
||||
use craft_github::{resolve_token, Error, GitHub};
|
||||
|
||||
fn main() {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
@@ -77,6 +77,11 @@ fn main() {
|
||||
entry.stats = Some(s);
|
||||
entry.fetched_at = Some(now);
|
||||
}
|
||||
Err(Error::NotFound) => {
|
||||
println!("{:<12} (not accessible)", entry.name());
|
||||
entry.fetched_at = Some(now);
|
||||
entry.inaccessible = true;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{:<12} ERROR {e}", entry.name());
|
||||
entry.error = Some(e.to_string());
|
||||
|
||||
@@ -35,6 +35,9 @@ pub enum Error {
|
||||
Unauthorized,
|
||||
#[error("rate limited by GitHub until {0}")]
|
||||
RateLimited(String),
|
||||
/// The repo doesn't exist or the token can't see it (GitHub answers 404 for private repos).
|
||||
#[error("repository not found or not accessible")]
|
||||
NotFound,
|
||||
#[error("GitHub HTTP {0}: {1}")]
|
||||
Http(u16, String),
|
||||
#[error("network: {0}")]
|
||||
@@ -114,6 +117,9 @@ impl GitHub {
|
||||
if status.as_u16() == 401 {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
if status.as_u16() == 404 {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
let remaining = resp
|
||||
.headers()
|
||||
.get("x-ratelimit-remaining")
|
||||
@@ -158,6 +164,9 @@ impl GitHub {
|
||||
return Err(Error::RateLimited("the top of the hour".into()));
|
||||
}
|
||||
if v["data"]["repository"].is_null() {
|
||||
if errs.iter().any(|e| e["type"] == "NOT_FOUND") {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
return Err(Error::GraphQl(msg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,11 +294,20 @@ fn run_cycle(app: &AppHandle) -> i64 {
|
||||
entry.stats = Some(stats);
|
||||
entry.fetched_at = Some(now);
|
||||
entry.error = None;
|
||||
entry.inaccessible = false;
|
||||
if let Some(left) = budget.graphql_remaining {
|
||||
snap.rate_limit_remaining =
|
||||
Some(snap.rate_limit_remaining.map_or(left, |r| r.min(left)));
|
||||
}
|
||||
}
|
||||
Err(GhError::NotFound) => {
|
||||
entry.stats = None;
|
||||
entry.icon = None;
|
||||
entry.icon_oid = None;
|
||||
entry.fetched_at = Some(now);
|
||||
entry.error = None;
|
||||
entry.inaccessible = true;
|
||||
}
|
||||
Err(e) => {
|
||||
entry.error = Some(e.to_string());
|
||||
failures.lock().unwrap().push(e);
|
||||
|
||||
@@ -85,10 +85,11 @@ function cell(entry, col) {
|
||||
if (col.key === 'name') {
|
||||
const tip = entry.error
|
||||
? `Last attempt failed: ${entry.error}`
|
||||
: entry.inaccessible ? 'Not accessible (private, or not found)'
|
||||
: entry.fetched_at ? `Fetched ${localTime(entry.fetched_at)}` : 'Not fetched yet';
|
||||
return el('span', { class: 'repo', title: tip }, el('span', { class: 'dot' }), appIcon(entry), link(v, gh(entry.repo)));
|
||||
}
|
||||
if (!s) return el('span', { class: 'muted' }, '—');
|
||||
if (!s) return entry.inaccessible ? '' : el('span', { class: 'muted' }, '—');
|
||||
switch (col.key) {
|
||||
case 'urgency': {
|
||||
const a = Math.min(1, v / 350) * 0.45;
|
||||
|
||||
Reference in new issue
Block a user