mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 03:20:33 +08:00
Repos need not carry a `xyz.radicle.project` payload; the default branch can come from `xyz.radicle.crefs` alone. Such repos got no counts and no default branch, and failed schema validation in explorer, which broke the parse of a whole listing. httpd: - Add top-level `cobs` with issue, patch and bucketed release counts - Add top-level `defaultBranch`, qualified like the crefs payload - Omit a count the node cannot determine instead of reporting zero - Drop `head`: the tip is `refs[defaultBranch]` - Keep counts in the project payload for older clients until a major release - Keep the project payload when a COB read fails - Keep `meta.releases` a single count: delegate + other - Log and skip canonical ref rules and refs that fail to read, so one bad ref doesn't drop the default branch tip explorer: - Model `cobs` and `defaultBranch`, adapting older nodes to one shape - Resolve the default branch from the crefs HEAD on older nodes - Collect the shims for older httpd in http-client/lib/compat.ts, each naming the httpd release that the client can drop it with - Add `defaultBranchTip`, replacing `head`, with a fallback to the legacy `meta.head` - Make the project payload and its legacy meta optional - Fall back to the RID for the repo name, and show a placeholder for a missing name in repo cards and the header - Hide a counter instead of reading a missing count as zero - Fall back to a full-page check for "More" without counts - Add a `repo.sourceUnavailable` route for repos without a default branch or tip - Browse explicit revisions and offer the branch selector without a default branch - List these repos in repo grids and on the landing page - Say the node is older or built without releases - Scope release pages in the client, so views never call the shims - Read OG worker repos through the same compat shims - Share the repo name and default branch helpers with the OG worker CI: - Add check:open-graph, a wrangler deploy dry run - Run it in GitHub CI, Buildkite and check:ci - Install explorer's dependencies first: the worker resolves compat.ts imports, such as zod, from there Tests: - Test that a build without releases omits the count - Test that one failing rule keeps the others - Test the repo shape across httpd versions and the release shims - Test `hasMorePages` and `repoName` - Test repos without a project payload end to end
204 lines
4.7 KiB
Svelte
204 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import type { BaseUrl, Repo } from "@http-client";
|
|
import type { RepoInfo } from "@app/components/RepoCard";
|
|
import type { ExploreSection } from "./sections";
|
|
|
|
import { HttpdClient } from "@http-client";
|
|
import { fetchRepoInfos } from "@app/components/RepoCard";
|
|
import { loadRepoActivity } from "@app/lib/commit";
|
|
import * as router from "@app/lib/router";
|
|
import { handleError } from "@app/views/nodes/error";
|
|
|
|
import Icon from "@app/components/Icon.svelte";
|
|
import Link from "@app/components/Link.svelte";
|
|
import RepoCard from "@app/components/RepoCard.svelte";
|
|
import RepoCardSkeleton from "@app/components/RepoCardSkeleton.svelte";
|
|
|
|
export let baseUrl: BaseUrl;
|
|
export let section: ExploreSection;
|
|
|
|
const MAX_STATIC_RIDS = 9;
|
|
|
|
$: skeletonCount =
|
|
section.kind === "static"
|
|
? Math.min(section.rids.length, MAX_STATIC_RIDS)
|
|
: section.limit;
|
|
|
|
async function fetchStaticRepos(
|
|
url: BaseUrl,
|
|
rids: string[],
|
|
): Promise<RepoInfo[]> {
|
|
const api = new HttpdClient(url);
|
|
const limited = rids.slice(0, MAX_STATIC_RIDS);
|
|
|
|
const results = await Promise.allSettled(
|
|
limited.map(rid => api.repo.getByRid(rid)),
|
|
);
|
|
|
|
const repos: Repo[] = results
|
|
.filter(
|
|
(r): r is PromiseFulfilledResult<Repo> => r.status === "fulfilled",
|
|
)
|
|
.map(r => r.value);
|
|
|
|
return repos.map(repo => ({
|
|
repo,
|
|
baseUrl: url,
|
|
activity: loadRepoActivity(repo.rid, url).catch(() => []),
|
|
}));
|
|
}
|
|
|
|
async function fetchSectionRepos(
|
|
url: BaseUrl,
|
|
sec: ExploreSection,
|
|
): Promise<RepoInfo[]> {
|
|
if (sec.kind === "static") {
|
|
return fetchStaticRepos(url, sec.rids);
|
|
}
|
|
if (sec.kind === "pinned") {
|
|
// Omit perPage so httpd defaults to "all configured pinned repos".
|
|
return fetchRepoInfos(url, {
|
|
show: "pinned",
|
|
page: 0,
|
|
});
|
|
}
|
|
const sort = sec.kind === "recentlyActive" ? "activity" : "seeding";
|
|
return fetchRepoInfos(url, {
|
|
show: "all",
|
|
sort,
|
|
perPage: sec.limit,
|
|
page: 0,
|
|
});
|
|
}
|
|
|
|
$: dataPromise = fetchSectionRepos(baseUrl, section);
|
|
</script>
|
|
|
|
<style>
|
|
.section {
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
.section:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
.header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 0.25rem 1rem;
|
|
margin-bottom: 1rem;
|
|
padding: 0 1rem;
|
|
}
|
|
.title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font: var(--txt-header-medium);
|
|
}
|
|
.title-icon {
|
|
color: var(--color-text-tertiary);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.view-all {
|
|
font: var(--txt-body-m-regular);
|
|
color: var(--color-text-tertiary);
|
|
white-space: nowrap;
|
|
}
|
|
.view-all:hover {
|
|
color: var(--color-text-primary);
|
|
text-decoration: underline;
|
|
}
|
|
.repo-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(28rem, 1fr));
|
|
gap: 0;
|
|
}
|
|
.card-fade-in {
|
|
animation: card-fade-in 280ms ease-out;
|
|
}
|
|
.empty {
|
|
font: var(--txt-body-m-regular);
|
|
color: var(--color-text-tertiary);
|
|
padding: 1rem;
|
|
}
|
|
@keyframes card-fade-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.card-fade-in {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1010.98px) {
|
|
.repo-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
.header {
|
|
padding: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="section">
|
|
<div class="header">
|
|
<div class="title">
|
|
<span class="title-icon"><Icon name={section.icon} /></span>
|
|
{section.title}
|
|
</div>
|
|
{#if section.kind === "pinned"}
|
|
<Link
|
|
route={{
|
|
resource: "explore.repos",
|
|
params: { page: 0, sort: "rid" },
|
|
}}>
|
|
<span class="view-all">View all repositories →</span>
|
|
</Link>
|
|
{:else}
|
|
<Link
|
|
route={{
|
|
resource: "explore.repos",
|
|
params: {
|
|
page: 0,
|
|
sort: section.kind === "recentlyActive" ? "activity" : "seeding",
|
|
},
|
|
}}>
|
|
<span class="view-all">View all →</span>
|
|
</Link>
|
|
{/if}
|
|
</div>
|
|
|
|
{#await dataPromise}
|
|
<div class="repo-grid">
|
|
{#each Array.from({ length: skeletonCount }) as _}
|
|
<RepoCardSkeleton />
|
|
{/each}
|
|
</div>
|
|
{:then repoInfos}
|
|
{#if repoInfos.length > 0}
|
|
<div class="repo-grid">
|
|
{#each repoInfos as repoInfo (repoInfo.repo.rid)}
|
|
<div class="card-fade-in">
|
|
<RepoCard {baseUrl} {repoInfo} />
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="empty">No repos to show.</div>
|
|
{/if}
|
|
{:catch error}
|
|
{router.push(handleError(error, baseUrl))}
|
|
{/await}
|
|
</div>
|