Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tools/star-tracker/.dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ JWT_SECRET=replace-with-32-plus-bytes-of-randomness
# Overrides the wrangler.toml [vars] value during local dev so the OAuth
# redirect_uri matches your dev GitHub OAuth App's callback URL.
PUBLIC_URL=http://localhost:8787

# Comma-separated GitHub logins allowed to access /_admin (operator view of
# every registered tenant). Leave unset to disable the page entirely.
# Examples:
# ADMIN_USERNAMES=octocat
# ADMIN_USERNAMES=octocat,hubot,monalisa
ADMIN_USERNAMES=your-github-login
30 changes: 30 additions & 0 deletions tools/star-tracker/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,36 @@ export async function listAllTenants(db: D1Database): Promise<Tenant[]> {
return results ?? [];
}

export type TenantWithStats = Tenant & {
owner_username: string | null;
owner_avatar: string | null;
public_repos: number;
private_repos: number;
total_stars: number;
};

// Cross-tenant rollup for the operator admin view. One SQL pass joins the
// owner, counts public/private repos, and sums public stargazers so the
// page can render without per-tenant fan-out queries.
export async function listAllTenantsWithStats(db: D1Database): Promise<TenantWithStats[]> {
const { results } = await db
.prepare(
`SELECT t.slug, t.owner_user_id, t.webhook_secret, t.display_name,
t.created_at, t.last_ping_at, t.last_event_at,
u.username AS owner_username, u.avatar_url AS owner_avatar,
COALESCE(SUM(CASE WHEN r.private = 0 THEN 1 ELSE 0 END), 0) AS public_repos,
COALESCE(SUM(CASE WHEN r.private = 1 THEN 1 ELSE 0 END), 0) AS private_repos,
COALESCE(SUM(CASE WHEN r.private = 0 THEN r.stargazers_count ELSE 0 END), 0) AS total_stars
FROM tenants t
LEFT JOIN users u ON u.id = t.owner_user_id
LEFT JOIN repos r ON r.tenant_slug = t.slug
GROUP BY t.slug
ORDER BY t.created_at DESC`,
)
.all<TenantWithStats>();
return results ?? [];
}

// -- Lite analytics ---------------------------------------------------------

export type ViewKind = 'chart' | 'page';
Expand Down
22 changes: 22 additions & 0 deletions tools/star-tracker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface Env {
GITHUB_CLIENT_SECRET: string;
JWT_SECRET: string;
PUBLIC_URL: string;
ADMIN_USERNAMES?: string;
};
Variables: { user: db.User | null };
}
Expand Down Expand Up @@ -270,6 +271,27 @@ app.get('/dashboard', async (c) => {
return c.html(pages.dashboard(user, tenants, c.env.PUBLIC_URL));
});

// -- Admin ------------------------------------------------------------------

// Parses ADMIN_USERNAMES (comma-separated GitHub logins) and returns true
// if the signed-in user is on the list. Case-insensitive match.
function isAdmin(user: db.User | null, raw: string | undefined): boolean {
if (!user || !raw) return false;
const allow = raw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
return allow.includes(user.username.toLowerCase());
}

// Operator-only cross-tenant view: every registered tenant, newest first,
// with repo counts + total stars. Path uses an underscore prefix so it
// can't collide with a tenant slug (SLUG_RE requires [a-z0-9] first char).
// Non-admins get 404 rather than 403 — no need to advertise the page.
app.get('/_admin', async (c) => {
const user = c.get('user');
if (!isAdmin(user, c.env.ADMIN_USERNAMES)) return c.notFound();
const tenants = await db.listAllTenantsWithStats(c.env.DB);
return c.html(pages.adminTenants(user!, tenants));
});

// -- Tenant creation --------------------------------------------------------

app.post('/tenants', async (c) => {
Expand Down
59 changes: 58 additions & 1 deletion tools/star-tracker/src/pages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Server-rendered HTML pages. Inline CSS, no JS — keeps the bundle small and
// the UX dependable inside a Worker.

import type { EventCounts, RepoRecent, RepoRow, Tenant, User, ViewsSummary } from './db';
import type { EventCounts, RepoRecent, RepoRow, Tenant, TenantWithStats, User, ViewsSummary } from './db';

// Human-friendly relative timestamp ("3 minutes ago"). Used for webhook
// status — absolute UTC strings are precise but require mental math; "5
Expand Down Expand Up @@ -807,6 +807,63 @@ ${views ? viewsPanel(views) : ''}
);
}

// Operator admin: every registered tenant, newest first. Reached only by
// users listed in ADMIN_USERNAMES; non-admins 404 before this renders.
export function adminTenants(user: User, tenants: TenantWithStats[]): string {
const sevenDays = 7 * 86400_000;
const now = Date.now();
const active7d = tenants.filter((t) => t.last_event_at && now - Date.parse(t.last_event_at) <= sevenDays).length;
const pinged = tenants.filter((t) => t.last_ping_at).length;
const totalRepos = tenants.reduce((s, t) => s + t.public_repos + t.private_repos, 0);
const totalStars = tenants.reduce((s, t) => s + t.total_stars, 0);

const summary = `
<div class="card">
<div style="display:flex; flex-wrap:wrap; gap:1.25rem 2rem;">
<div><strong>${tenants.length}</strong> <span class="muted">tenants</span></div>
<div><strong>${pinged}</strong> <span class="muted">with ping</span></div>
<div><strong>${active7d}</strong> <span class="muted">active 7d</span></div>
<div><strong>${totalRepos.toLocaleString('en-US')}</strong> <span class="muted">repos</span></div>
<div><strong>${totalStars.toLocaleString('en-US')}</strong> <span class="muted">stars tracked</span></div>
</div>
</div>`;

const rows = tenants.length === 0
? `<p class="muted">No tenants registered yet.</p>`
: `<ul class="tenants">${tenants.map((t) => {
const owner = t.owner_username
? `<a href="https://github.com/${esc(t.owner_username)}" target="_blank" rel="noopener">@${esc(t.owner_username)}</a>`
: `<span class="muted">unknown owner</span>`;
const repoBits: string[] = [];
if (t.public_repos) repoBits.push(`${t.public_repos} public`);
if (t.private_repos) repoBits.push(`${t.private_repos} private`);
const repoLine = repoBits.length ? repoBits.join(' · ') : 'no repos';
const stars = t.total_stars > 0 ? ` · ${t.total_stars.toLocaleString('en-US')}★` : '';
const created = new Date(t.created_at).toISOString().slice(0, 10);
return `<li class="card" style="display:block;">
<div style="display:flex; justify-content:space-between; align-items:baseline; gap:1rem; flex-wrap:wrap;">
<span>
<strong><a href="/${esc(t.slug)}">${esc(t.slug)}</a></strong>
<span class="muted"> — ${owner}</span>
</span>
<span class="muted" style="font-size:0.85em;">registered ${created}</span>
</div>
<div class="muted" style="font-size:0.9em; margin-top:4px;">
${esc(repoLine)}${stars} · last event ${esc(relTime(t.last_event_at))} · last ping ${esc(relTime(t.last_ping_at))}
</div>
</li>`;
}).join('')}</ul>`;

return shell(
'Admin · tenants',
user,
`<h1>All tenants</h1>
<p class="muted">Operator view — every org/user that's registered a tracker, newest first.</p>
${summary}
${rows}`,
);
}

export function error(user: User | null, status: number, message: string): string {
return shell(`Error ${status}`, user, `<h1>${status}</h1><p>${esc(message)}</p>`);
}
2 changes: 2 additions & 0 deletions tools/star-tracker/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ custom_domain = true
# GITHUB_CLIENT_ID — OAuth App client ID
# GITHUB_CLIENT_SECRET — OAuth App client secret
# JWT_SECRET — random 32+ byte string for signing session cookies
# ADMIN_USERNAMES — comma-separated GitHub logins allowed to view /_admin (optional)
# e.g. "octocat" or "octocat,hubot,monalisa"
[vars]
PUBLIC_URL = "https://stars.wavekat.com"
Loading