diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 9ad040df5..9ee99680a 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -529,11 +529,11 @@ Sources: [status transitions](app/Actions/UpdateCommitStatuses.php), [timeouts]( ### About, changelog, and policies -The public **About** page describes the livestream project and its contributors. The **Changelog**, opened from the navigation or the side column, lists the pull requests merged into `production` on GitHub, a few days at a time. The **Brand** page at `/brand` offers the logo, wordmark, colors, typography, and wallpapers; right-clicking the logo in the navigation also copies it or links there. **Terms** and **Privacy** explain acceptable use and data handling. Public posts may be indexed or copied outside /dev/null; content and search queries can be processed by automated or AI services. Read those policies before posting sensitive information or relying on deletion to remove every external copy. +The public **About** page describes the livestream project and lists up to 1,000 GitHub contributor accounts, excluding bots and known agents, most commits first; the list is cached for an hour and GitHub may take several hours to update its counts; when GitHub cannot be reached the page says so instead of showing the list. The **Changelog**, opened from the navigation or the side column, lists the pull requests merged into `production` on GitHub, a few days at a time. The **Brand** page at `/brand` offers the logo, wordmark, colors, typography, and wallpapers; right-clicking the logo in the navigation also copies it or links there. **Terms** and **Privacy** explain acceptable use and data handling. Public posts may be indexed or copied outside /dev/null; content and search queries can be processed by automated or AI services. Read those policies before posting sensitive information or relying on deletion to remove every external copy. To count active people, /dev/null remembers only the **last day** (not the time) you were signed in and used the site or the API; background refreshes do not count. That day is never shown to other people, is included in your export, and is deleted with your account. Daily active-people numbers are kept as anonymous counts. Visitor analytics come from **Fathom**, which counts anonymously: the browser sends a `guest-visit` event once per session when you are not signed in and a `signup` event right after an account is created, with nothing that identifies you. -Sources: [last seen tracking](app/Http/Middleware/UpdateLastSeenAt.php), [Fathom events](resources/js/lib/analytics.ts), [public page routes](routes/web.php), [About](resources/js/pages/about/show.tsx), [privacy policy](resources/js/pages/privacy/show.tsx), [terms](resources/js/pages/terms/show.tsx). +Sources: [last seen tracking](app/Http/Middleware/UpdateLastSeenAt.php), [Fathom events](resources/js/lib/analytics.ts), [public page routes](routes/web.php), [About](resources/js/pages/about/show.tsx), [contributors](app/Queries/ContributorsQuery.php), [privacy policy](resources/js/pages/privacy/show.tsx), [terms](resources/js/pages/terms/show.tsx). ### Tom's repository insights and community statistics diff --git a/app/Http/Controllers/AboutController.php b/app/Http/Controllers/AboutController.php index 98b461175..19239dd92 100644 --- a/app/Http/Controllers/AboutController.php +++ b/app/Http/Controllers/AboutController.php @@ -4,15 +4,18 @@ namespace App\Http\Controllers; +use App\Queries\ContributorsQuery; use App\Services\PageMetadata; use Inertia\Inertia; use Inertia\Response; final readonly class AboutController { - public function show(PageMetadata $metadata): Response + public function show(ContributorsQuery $contributors, PageMetadata $metadata): Response { - return Inertia::render('about/show')->withViewData(['preview' => $metadata->page( + return Inertia::render('about/show', [ + 'contributors' => $contributors->get(...), + ])->withViewData(['preview' => $metadata->page( 'About - /dev/null', 'How /dev/null was built in the open: from the first commit on stream to the developers who joined and the people who shipped it.', route('about.show'), diff --git a/app/Queries/ContributorsQuery.php b/app/Queries/ContributorsQuery.php new file mode 100644 index 000000000..1e9b64159 --- /dev/null +++ b/app/Queries/ContributorsQuery.php @@ -0,0 +1,90 @@ +|null + */ + public function get(): ?array + { + $repository = config()->string('services.github.repository'); + $key = "about:contributors:{$repository}"; + + /** @var list|false|null $contributors */ + $contributors = Cache::get($key); + + if ($contributors === null) { + $contributors = []; + + try { + for ($page = 1; $page <= self::MaxPages; $page++) { + $accounts = $this->github->request()->get("repos/{$repository}/contributors", [ + 'per_page' => self::PerPage, + 'page' => $page, + ])->throw()->collect(); + + if ($accounts->isNotEmpty() && ! array_is_list($accounts->all())) { + $contributors = false; + + break; + } + + $contributors = [...$contributors, ...$accounts->where('type', 'User') + ->mapInto(Fluent::class) + ->filter(fn (Fluent $account): bool => $account->string('login')->isNotEmpty() + && is_int($account->get('contributions')) + && ! in_array($account->string('login')->lower()->value(), self::Agents, true)) + ->map(fn (Fluent $account): array => [ + 'login' => $account->string('login')->value(), + 'commits' => $account->integer('contributions'), + ]) + ->values() + ->all()]; + + if ($accounts->count() < self::PerPage) { + break; + } + } + } catch (HttpClientException) { + $contributors = false; + } + + Cache::put($key, $contributors, $contributors === false ? self::FailedTtl : self::Ttl); + } + + return $contributors === false ? null : $contributors; + } +} diff --git a/resources/js/lib/about.ts b/resources/js/lib/about.ts index 0a187650c..067c5475d 100644 --- a/resources/js/lib/about.ts +++ b/resources/js/lib/about.ts @@ -266,70 +266,3 @@ export const storyAfter: StoryEvent[] = [ }, }, ]; - -/** - * Everyone with a commit on production, most commits first. Bots and agents are left out. - */ -export const storyContributors: StoryContributor[] = [ - { login: 'xHeaven', commits: 217 }, - { login: 'nunomaduro', commits: 189 }, - { login: 'mnapoli', commits: 83 }, - { login: 'brendt', commits: 49 }, - { login: 'paulinevos', commits: 28 }, - { login: 'jubianchi', commits: 24 }, - { login: 'MrPunyapal', commits: 19 }, - { login: 'simonhamp', commits: 19 }, - { login: 'hbenabdallah', commits: 10 }, - { login: 'sadiqk2', commits: 8 }, - { login: 'SanderMuller', commits: 7 }, - { login: 'fernandofreamunde', commits: 6 }, - { login: 'camilleislasse', commits: 6 }, - { login: 'HelgeSverre', commits: 5 }, - { login: 'jjaybrown', commits: 5 }, - { login: 'richardkeep', commits: 5 }, - { login: 'vtlxx', commits: 5 }, - { login: 'robelsust', commits: 4 }, - { login: 'prateekbhujel', commits: 4 }, - { login: 'MaximeGoncalves', commits: 3 }, - { login: 'mathiasonea', commits: 3 }, - { login: 'virtualtear', commits: 2 }, - { login: 'sulimanbenhalim', commits: 2 }, - { login: 'mon4ssi', commits: 2 }, - { login: 'slovenianGooner', commits: 2 }, - { login: 'hbenjacquet', commits: 2 }, - { login: 'ashkesarmaa', commits: 2 }, - { login: 'Akiyo974', commits: 2 }, - { login: 'Chemaclass', commits: 2 }, - { login: 'eznix86', commits: 2 }, - { login: 'BENNIEV-ZA', commits: 2 }, - { login: 'ahosker', commits: 2 }, - { login: 'aaqidmasoodi', commits: 2 }, - { login: 'kachelle', commits: 1 }, - { login: 'lazarospsa', commits: 1 }, - { login: 'Naimur404', commits: 1 }, - { login: 'OpadaAlzaiede', commits: 1 }, - { login: 'ondrejmirtes', commits: 1 }, - { login: 'Heyosseus', commits: 1 }, - { login: 'RobinMalfait', commits: 1 }, - { login: 'rohitsasi720', commits: 1 }, - { login: 'murrant', commits: 1 }, - { login: 'tooinfinity', commits: 1 }, - { login: 'utsavsomaiya', commits: 1 }, - { login: 'vmitchell85', commits: 1 }, - { login: 'anatolesns', commits: 1 }, - { login: 'eramitgupta', commits: 1 }, - { login: 'kadirtikil', commits: 1 }, - { login: 'jigar-dhulla', commits: 1 }, - { login: 'hosmelq', commits: 1 }, - { login: 'hafeez-bw', commits: 1 }, - { login: 'GTS-MEGHNI', commits: 1 }, - { login: 'faissaloux', commits: 1 }, - { login: 'fabpl', commits: 1 }, - { login: 'Taldres', commits: 1 }, - { login: 'danielh-official', commits: 1 }, - { login: 'chrisgocode', commits: 1 }, - { login: 'chiragparekh', commits: 1 }, - { login: 'AmanuelLegese', commits: 1 }, - { login: 'albertoarena', commits: 1 }, - { login: 'alaminfirdows', commits: 1 }, -]; diff --git a/resources/js/pages/about/show.tsx b/resources/js/pages/about/show.tsx index ccc9fc620..b52abf33f 100644 --- a/resources/js/pages/about/show.tsx +++ b/resources/js/pages/about/show.tsx @@ -17,6 +17,7 @@ import { WordmarkText } from '@/components/wordmark-text'; import AppLayout from '@/layouts/app-layout'; import type { StoryCommit, + StoryContributor, StoryEvent, StoryEventKind, StoryStream, @@ -25,7 +26,6 @@ import { storyAfter, storyAfterLane, storyBefore, - storyContributors, storyFounders, storyStreams, } from '@/lib/about'; @@ -265,10 +265,10 @@ function Stream({ stream }: { stream: StoryStream }) { ); } -function Contributors() { +function Contributors({ contributors }: { contributors: StoryContributor[] }) { return (