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:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-10 12:49:22 -04:00
1 parent 47aabd6635
commit daebcb02fb
7 files changed
+33 -2

No files matched your search

+2
View File
@@ -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
+2
View File
@@ -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)]
+3
View File
@@ -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`.
+6 -1
View File
@@ -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());
+9
View File
@@ -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));
}
}
+9
View File
@@ -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);
+2 -1
View File
@@ -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;