From cb24217bef12d00ba28a3b4958dd493de9e86a2c Mon Sep 17 00:00:00 2001 From: Raphael <68374617+raphaelscholle@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:49:59 +0100 Subject: [PATCH] feat: show open reports with optional GitHub activity --- BUG_TRACKER.md | 2 +- src/pages/bugs.tsx | 18 +++++++++--------- tests/bugs.spec.ts | 29 +++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/BUG_TRACKER.md b/BUG_TRACKER.md index 08c994e..3599605 100644 --- a/BUG_TRACKER.md +++ b/BUG_TRACKER.md @@ -2,7 +2,7 @@ The website uses `https://openhd.tech/api/bugs` by default. Set `BUG_REPORT_API_URL` at build time to change it. No credentials belong in the website. This extends PR #12; FleetControl owns the backend, so the unused Python requirements are removed. -`/bugs` supports search, type, status, repository and component filters with server pagination. GitHub search caps results at 1,000; narrow filters for older results. GitHub data is cached for one minute, with rate-limit backoff. `/bugs/detail?repo=OpenHD/OpenHD&number=123` shows descriptions as escaped text, metadata, related pull requests and the original link. +`/bugs` shows open website bug reports by default. The Yes/No GitHub toggle includes or hides public GitHub issues and pull requests; closed and merged entries are always hidden. Search, repository and component filters use server pagination. GitHub search caps results at 1,000; narrow filters for older results. GitHub data is cached for one minute, with rate-limit backoff. `/bugs/detail?repo=OpenHD/OpenHD&number=123` shows descriptions as escaped text, metadata, related pull requests and the original link. `/bugs/report` requires title, description, reproduction steps, version, component, Air hardware, Ground hardware, and both archives. ZIP, tar.gz and tgz are accepted; 50 MiB per archive is the default. The separately marked incomplete-diagnostics workflow requires an explanation of at least 20 characters, holds reports privately for administrator approval, and accepts any available archive. diff --git a/src/pages/bugs.tsx b/src/pages/bugs.tsx index a902dc2..a4738eb 100644 --- a/src/pages/bugs.tsx +++ b/src/pages/bugs.tsx @@ -8,30 +8,30 @@ export default function Bugs() { const endpoint = useBugApi(); const [items, setItems] = useState([]), [total, setTotal] = useState(0), [partial, setPartial] = useState(false); const [error, setError] = useState(''), [loading, setLoading] = useState(false); - const [query, setQuery] = useState(''), [search, setSearch] = useState(''), [type, setType] = useState('all'), [status, setStatus] = useState('all'), [repo, setRepo] = useState(''), [repository, setRepository] = useState(''), [component, setComponent] = useState(''), [components, setComponents] = useState([]), [page, setPage] = useState(1); + const [query, setQuery] = useState(''), [search, setSearch] = useState(''), [showGitHub, setShowGitHub] = useState(false), [repo, setRepo] = useState(''), [repository, setRepository] = useState(''), [component, setComponent] = useState(''), [components, setComponents] = useState([]), [page, setPage] = useState(1); useEffect(() => { if (query === search && repo === repository) return; const timer = setTimeout(() => { setSearch(query); setRepository(repo); setPage(1); }, 450); return () => clearTimeout(timer); }, [query, repo, search, repository]); useEffect(() => { if (endpoint) bugFetch(`${endpoint}/config`).then(result => setComponents(result.components)).catch(() => {}); }, [endpoint]); useEffect(() => { if (!endpoint) return; - const abort = new AbortController(); setLoading(true); setError(''); - const params = new URLSearchParams({q: search, type, status, repository, component, page: String(page)}); + const abort = new AbortController(); setLoading(true); setError(''); setItems([]); + const params = new URLSearchParams({q: search, type: showGitHub ? 'all' : 'bug', status: 'open', repository, component, page: String(page)}); bugFetch(`${endpoint}?${params}`, {signal: abort.signal}).then(result => { setItems(result.items || []); setTotal(result.total_count); setPartial(Boolean(result.incomplete_results)); }).catch(e => { if (!abort.signal.aborted) { setError(e.message); setItems([]); } }).finally(() => { if (!abort.signal.aborted) setLoading(false); }); return () => abort.abort(); - }, [endpoint, search, type, status, repository, component, page]); + }, [endpoint, search, showGitHub, repository, component, page]); + const visibleItems = items.filter(item => item.state === 'open' && !item.merged && !item.pull_request?.merged_at && (showGitHub || entryType(item) === 'Bug')); const pages = Math.min(34, Math.ceil(Math.min(total, 1000) / 30)); return
-

OpenHD Bug Tracker

Community bug reports, GitHub issues and pull requests in one place.

Report a bug
+

OpenHD Bug Tracker

Open bug reports, with optional GitHub issues and pull requests.

Report a bug
setQuery(e.target.value)}/> - - + setRepo(e.target.value)}/>
{!endpoint &&

The tracker API is being configured.

} {loading &&

Loading GitHub activity…

}{error &&

{error}

} - {!loading && !error && endpoint && !items.length &&

No entries match these filters.

} - {!error &&
{items.map(item => + {!loading && !error && endpoint && !visibleItems.length &&

No open entries match these filters.

} + {!error &&
Number / TitleTypeRepository / ComponentStatusPriorityUpdated
{visibleItems.map(item => )}
Number / TitleTypeRepository / ComponentStatusPriorityUpdated
#{item.number} {item.title}{entryType(item)}{item.repository_url.split('/repos/')[1]}{labelValue(item, 'component')}{item.pull_request?.merged_at ? 'merged' : item.state}{labelValue(item, 'priority')}{new Date(item.updated_at).toLocaleDateString()}
} {pages > 0 && } diff --git a/tests/bugs.spec.ts b/tests/bugs.spec.ts index 315c947..8f25ece 100644 --- a/tests/bugs.spec.ts +++ b/tests/bugs.spec.ts @@ -16,6 +16,15 @@ async function mocks(page: Page, enabled = true) { result = {id: 'report', number: 42, status: 'created', url: 'https://github.com/OpenHD/OpenHD/issues/42', message: 'Bug report created. Notification queued.'}; } else if (url.pathname.includes('/entry/')) result = {id: 1, number: 42, title: 'Camera fails', body: 'Steps: boot the unit. ', state: 'open', html_url: 'https://github.com/OpenHD/OpenHD/issues/42', updated_at: '2026-11-02T10:00:00Z', repository_url: 'https://api.github.com/repos/OpenHD/OpenHD', labels: [{name: 'website-report'}, {name: 'component:video'}]}; else result = {total_count: 70, items: [{id: 1, number: 42, title: 'Camera fails', state: 'open', html_url: 'https://github.com/OpenHD/OpenHD/issues/42', updated_at: '2026-11-02T10:00:00Z', repository_url: 'https://api.github.com/repos/OpenHD/OpenHD', labels: [{name: 'website-report'}, {name: 'component:video'}]}, {id: 2, number: 43, title: 'Fix camera', state: 'closed', html_url: 'https://github.com/OpenHD/OpenHD/pull/43', updated_at: '2026-11-02T10:00:00Z', repository_url: 'https://api.github.com/repos/OpenHD/OpenHD', labels: [], pull_request: {merged_at: '2026-11-02T10:00:00Z'}}]}; + if (url.pathname === '/api/bugs' && req.method() === 'GET') { + const list = result as {items: unknown[]}; + const baseEntry = {updated_at: '2026-11-02T10:00:00Z', repository_url: 'https://api.github.com/repos/OpenHD/OpenHD', labels: []}; + list.items.push( + {...baseEntry, id: 3, number: 44, title: 'Radio issue', state: 'open', html_url: 'https://github.com/OpenHD/OpenHD/issues/44'}, + {...baseEntry, id: 4, number: 45, title: 'Radio fix', state: 'open', html_url: 'https://github.com/OpenHD/OpenHD/pull/45', pull_request: {}}, + {...baseEntry, id: 5, number: 46, title: 'Old website report', state: 'closed', html_url: 'https://github.com/OpenHD/OpenHD/issues/46', labels: [{name: 'website-report'}]}, + ); + } return route.fulfill({json: result, headers}); }); } @@ -38,13 +47,25 @@ test('normal report requires all fields and both archives before submission', as await expect(submit).toBeEnabled(); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({path: info.outputPath('report.png'), fullPage: true}); await submit.click(); await expect(page.getByRole('heading', {name: 'Bug #42 created'})).toBeVisible(); await expect(page.getByRole('link', {name: 'View your issue on GitHub'})).toHaveAttribute('href', 'https://github.com/OpenHD/OpenHD/issues/42'); }); -test('tracker renders distinct types, filters, pagination and escaped detail content', async ({page}, info) => { +test('tracker shows open reports, optionally includes GitHub activity, and paginates details', async ({page}, info) => { await mocks(page); await page.goto('/bugs'); await expect(page.getByRole('link', {name: '#42 Camera fails'})).toBeVisible(); - await expect(page.getByRole('cell', {name: 'Pull Request', exact: true})).toBeVisible(); await expect(page.getByRole('cell', {name: 'merged', exact: true})).toBeVisible(); - const request = page.waitForRequest(r => r.url().includes('/api/bugs?') && r.url().includes('type=pull')); - await page.getByLabel('Type', {exact: true}).selectOption('pull'); await request; + await expect(page.getByLabel('Show GitHub issues and pull requests')).toHaveValue('no'); + await expect(page.getByRole('link', {name: '#43 Fix camera'})).toHaveCount(0); + await expect(page.getByRole('link', {name: '#44 Radio issue'})).toHaveCount(0); + await expect(page.getByRole('link', {name: '#45 Radio fix'})).toHaveCount(0); + await expect(page.getByRole('link', {name: '#46 Old website report'})).toHaveCount(0); + const request = page.waitForRequest(r => {const url = new URL(r.url()); return url.pathname === '/api/bugs' && url.searchParams.get('type') === 'all' && url.searchParams.get('status') === 'open';}); + await page.getByLabel('Show GitHub issues and pull requests').selectOption('yes'); await request; + await expect(page.getByRole('link', {name: '#44 Radio issue'})).toBeVisible(); + await expect(page.getByRole('link', {name: '#45 Radio fix'})).toBeVisible(); + await expect(page.getByRole('link', {name: '#43 Fix camera'})).toHaveCount(0); + await expect(page.getByRole('link', {name: '#46 Old website report'})).toHaveCount(0); await page.getByRole('button', {name: 'Next', exact: true}).click(); await expect(page.getByText('Page 2 of 3')).toBeVisible(); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({path: info.outputPath('tracker.png'), fullPage: true}); + const reportsOnly = page.waitForRequest(r => {const url = new URL(r.url()); return url.pathname === '/api/bugs' && url.searchParams.get('type') === 'bug' && url.searchParams.get('status') === 'open' && url.searchParams.get('page') === '1';}); + await page.getByLabel('Show GitHub issues and pull requests').selectOption('no'); await reportsOnly; + await expect(page.getByRole('link', {name: '#44 Radio issue'})).toHaveCount(0); + await expect(page.getByRole('link', {name: '#45 Radio fix'})).toHaveCount(0); await page.getByRole('link', {name: '#42 Camera fails'}).click(); await expect(page.getByRole('heading', {name: '#42 Camera fails'})).toBeVisible(); await expect(page.getByText('Steps: boot the unit. ', {exact: true})).toBeVisible(); await expect(page.getByRole('heading', {name: 'Diagnostic files'})).toBeVisible(); });