Compare commits
4 Commits
03482693ea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 89bb69bcfa | |||
| 18aa517319 | |||
| a51bb6f86f | |||
| 4f6460297f |
@@ -0,0 +1,653 @@
|
||||
# Bug Reporting Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a "Report a Bug" footer link that opens the Bugpin screenshot widget (forwards to GitHub Issues), plus auto error capture via Bugsink (Sentry-compatible) on every page.
|
||||
|
||||
**Architecture:** Two independent flows. Bugpin = user-initiated, lazy-loaded `<script>` widget from `bugpin.half.st`, triggered by a footer `<button>`. Bugsink = background, `@sentry/browser` SDK initialised on page load against a DSN at `bugsink.half.st`. Both credentials are public project tokens and live in `src/world/ui.md`. Both flows are no-ops when not configured.
|
||||
|
||||
**Tech Stack:** Astro 6, TypeScript, Zod 4, Vitest 4 (node env, no jsdom), `@sentry/browser` (new dep), Bugpin widget script.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-17-bug-reporting-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
- `src/world/schema.ts` — **modify**: extend `uiFrontmatterSchema` with optional `bugReport` block.
|
||||
- `src/world/schema.test.ts` — **modify**: add accept/reject cases for `bugReport`.
|
||||
- `src/world/types.ts` — **modify**: extend `UiConfig` to expose `bugReport`.
|
||||
- `src/world/ui.md` — **modify**: add `bugReport` frontmatter.
|
||||
- `src/ui/error-tracking.ts` — **create**: reads body data attrs, calls `Sentry.init`. No-op when DSN missing.
|
||||
- `src/ui/bug-report.ts` — **create**: wires footer button click to lazy-load Bugpin widget, then call `BugPin.open()`. No-op when config missing.
|
||||
- `src/pages/index.astro` — **modify**: render the footer button conditionally, attach data attrs to `<body>`, import the two new modules.
|
||||
- `package.json` / `package-lock.json` — **modify**: add `@sentry/browser`.
|
||||
- `src/world/TODOs.md:45` — **modify**: rewrite to reflect actual approach.
|
||||
|
||||
No DOM tests for the two new UI modules: the project's Vitest config runs in node with no jsdom. Coverage for those modules is `astro check` (type safety) + a manual smoke step at the end of the plan. Schema changes get full unit test coverage via the existing test file.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Extend `uiFrontmatterSchema` with `bugReport` (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/world/schema.ts`
|
||||
- Modify: `src/world/schema.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add the failing tests**
|
||||
|
||||
Append these inside the existing `describe('uiFrontmatterSchema', ...)` block in `src/world/schema.test.ts` (just before the closing `})` on line 120):
|
||||
|
||||
```ts
|
||||
it('accepts a bugReport block with bugpin and bugsink subconfigs', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: {
|
||||
copyright: '© 2026 Ethan J Lewis',
|
||||
links: [],
|
||||
},
|
||||
bugReport: {
|
||||
enabled: true,
|
||||
label: 'Report a Bug',
|
||||
bugpin: {
|
||||
serverUrl: 'https://bugpin.half.st',
|
||||
apiKey: 'proj_07df4bf91f12445b8ef8c723e865ed7b',
|
||||
},
|
||||
bugsink: {
|
||||
enabled: true,
|
||||
dsn: 'https://231ef18b6b4f426ca249778cfddf821c@bugsink.half.st/1',
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it('accepts ui config with no bugReport block at all', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a bugpin block with a non-url serverUrl', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
|
||||
bugReport: {
|
||||
enabled: true,
|
||||
bugpin: { serverUrl: 'not-a-url', apiKey: 'proj_x' },
|
||||
},
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
|
||||
})
|
||||
|
||||
it('rejects a bugsink block with a non-url dsn', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
|
||||
bugReport: {
|
||||
enabled: true,
|
||||
bugsink: { dsn: 'whatever' },
|
||||
},
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests, confirm they fail**
|
||||
|
||||
```
|
||||
npm test -- src/world/schema.test.ts
|
||||
```
|
||||
|
||||
Expected: the four new tests fail because `bugReport` is currently unknown (zod by default strips extras, so the "accepts" cases probably pass — but the "rejects" cases will fail because zod will accept anything for unknown fields). Either way, confirm the test runner reports at least the two "rejects" cases as failing.
|
||||
|
||||
- [ ] **Step 3: Add the schema fields**
|
||||
|
||||
In `src/world/schema.ts`, find `uiFrontmatterSchema` (starts at line 48). Add a `bugReport` field at the end of the object (after `features`, before the closing `})`). Final block:
|
||||
|
||||
```ts
|
||||
export const uiFrontmatterSchema = z.object({
|
||||
pageTitle: z.string().trim().min(1),
|
||||
description: z.string().trim().min(1),
|
||||
robots: z.string().trim().min(1).default('noindex'),
|
||||
themeColor: z.string().trim().min(1).default('#1a0d00'),
|
||||
footer: z.object({
|
||||
copyright: z.string().trim().min(1),
|
||||
copyrightHref: z.url().optional(),
|
||||
buildLabel: z.string().trim().min(1).default('Build #'),
|
||||
showBuild: z.boolean().default(true),
|
||||
links: z.array(z.object({
|
||||
label: z.string().trim().min(1),
|
||||
href: z.url(),
|
||||
})).default([]),
|
||||
}),
|
||||
features: z.object({
|
||||
chips: z.boolean().default(true),
|
||||
lightMeter: z.boolean().default(true),
|
||||
typedEffect: z.boolean().default(true),
|
||||
roomScroll: z.boolean().default(true),
|
||||
}).default({
|
||||
chips: true,
|
||||
lightMeter: true,
|
||||
typedEffect: true,
|
||||
roomScroll: true,
|
||||
}),
|
||||
bugReport: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
label: z.string().trim().min(1).default('Report a Bug'),
|
||||
bugpin: z.object({
|
||||
serverUrl: z.url(),
|
||||
apiKey: z.string().trim().min(1),
|
||||
}).optional(),
|
||||
bugsink: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
dsn: z.url(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests, confirm pass**
|
||||
|
||||
```
|
||||
npm test -- src/world/schema.test.ts
|
||||
```
|
||||
|
||||
Expected: all `uiFrontmatterSchema` tests pass (the original two + the four new ones).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
git add src/world/schema.ts src/world/schema.test.ts
|
||||
git commit -m "feat(world): add bugReport block to ui schema"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Surface `bugReport` on the `UiConfig` type
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/world/types.ts:64-85`
|
||||
|
||||
- [ ] **Step 1: Extend the interface**
|
||||
|
||||
In `src/world/types.ts`, replace the existing `UiConfig` interface (currently lines 64–85) with:
|
||||
|
||||
```ts
|
||||
export interface UiConfig {
|
||||
pageTitle: string
|
||||
description: string
|
||||
robots: string
|
||||
themeColor: string
|
||||
footer: {
|
||||
copyright: string
|
||||
copyrightHref?: string
|
||||
buildLabel: string
|
||||
showBuild: boolean
|
||||
links: Array<{
|
||||
label: string
|
||||
href: string
|
||||
}>
|
||||
}
|
||||
features: {
|
||||
chips: boolean
|
||||
lightMeter: boolean
|
||||
typedEffect: boolean
|
||||
roomScroll: boolean
|
||||
}
|
||||
bugReport?: {
|
||||
enabled: boolean
|
||||
label: string
|
||||
bugpin?: {
|
||||
serverUrl: string
|
||||
apiKey: string
|
||||
}
|
||||
bugsink?: {
|
||||
enabled: boolean
|
||||
dsn: string
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify type check passes**
|
||||
|
||||
```
|
||||
npx astro check
|
||||
```
|
||||
|
||||
Expected: 0 errors, 0 warnings (or at least no new errors introduced by this change).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```
|
||||
git add src/world/types.ts
|
||||
git commit -m "feat(world): expose bugReport on UiConfig"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add real config to `ui.md`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/world/ui.md`
|
||||
|
||||
- [ ] **Step 1: Edit frontmatter**
|
||||
|
||||
Insert this block in `src/world/ui.md` between the `features:` block and the closing `---`, so the frontmatter ends like:
|
||||
|
||||
```yaml
|
||||
features:
|
||||
chips: true
|
||||
lightMeter: true
|
||||
typedEffect: true
|
||||
roomScroll: true
|
||||
bugReport:
|
||||
enabled: true
|
||||
label: "Report a Bug"
|
||||
bugpin:
|
||||
serverUrl: "https://bugpin.half.st"
|
||||
apiKey: "proj_07df4bf91f12445b8ef8c723e865ed7b"
|
||||
bugsink:
|
||||
enabled: true
|
||||
dsn: "https://231ef18b6b4f426ca249778cfddf821c@bugsink.half.st/1"
|
||||
---
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify the world loads**
|
||||
|
||||
```
|
||||
npm test -- src/world/buildWorld.test.ts
|
||||
npm test -- src/world/loader.test.ts
|
||||
```
|
||||
|
||||
Expected: all tests pass — the new frontmatter is valid.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```
|
||||
git add src/world/ui.md
|
||||
git commit -m "feat(world): wire bugpin + bugsink credentials in ui.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Install `@sentry/browser`
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`, `package-lock.json`
|
||||
|
||||
- [ ] **Step 1: Install**
|
||||
|
||||
```
|
||||
npm install @sentry/browser
|
||||
```
|
||||
|
||||
Expected: a single new dependency added under `"dependencies"` in `package.json`; lockfile updates.
|
||||
|
||||
- [ ] **Step 2: Verify**
|
||||
|
||||
```
|
||||
node -e "require('@sentry/browser')"
|
||||
```
|
||||
|
||||
Expected: no output and exit code 0.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```
|
||||
git add package.json package-lock.json
|
||||
git commit -m "chore: add @sentry/browser for bugsink integration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Create the Bugsink init module
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ui/error-tracking.ts`
|
||||
|
||||
No unit test — Vitest runs in node with no jsdom, and this module only does work in a browser. Coverage is `astro check` (types) + the manual smoke at the end.
|
||||
|
||||
- [ ] **Step 1: Write the module**
|
||||
|
||||
```ts
|
||||
// src/ui/error-tracking.ts
|
||||
import * as Sentry from '@sentry/browser'
|
||||
|
||||
const dsn = document.body?.dataset.bugsinkDsn ?? ''
|
||||
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn,
|
||||
tracesSampleRate: 0,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
integrations: [],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Type-check**
|
||||
|
||||
```
|
||||
npx astro check
|
||||
```
|
||||
|
||||
Expected: 0 new errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```
|
||||
git add src/ui/error-tracking.ts
|
||||
git commit -m "feat(ui): initialise sentry browser sdk against bugsink dsn"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Create the Bugpin trigger module
|
||||
|
||||
**Files:**
|
||||
- Create: `src/ui/bug-report.ts`
|
||||
|
||||
Module reads config from the footer button's `data-*` attributes, lazy-loads the Bugpin widget on first click, then calls `BugPin.open()`. Subsequent clicks just call `open()` again.
|
||||
|
||||
- [ ] **Step 1: Write the module**
|
||||
|
||||
```ts
|
||||
// src/ui/bug-report.ts
|
||||
|
||||
interface BugPinAPI {
|
||||
open: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
BugPin?: BugPinAPI
|
||||
}
|
||||
}
|
||||
|
||||
const button = document.querySelector<HTMLButtonElement>('[data-bug-report-trigger]')
|
||||
|
||||
if (button) {
|
||||
const serverUrl = button.dataset.bugpinServer
|
||||
const apiKey = button.dataset.bugpinKey
|
||||
|
||||
if (serverUrl && apiKey) {
|
||||
let loadPromise: Promise<void> | null = null
|
||||
|
||||
const ensureLoaded = (): Promise<void> => {
|
||||
if (window.BugPin) return Promise.resolve()
|
||||
if (loadPromise) return loadPromise
|
||||
|
||||
loadPromise = new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = `${serverUrl.replace(/\/$/, '')}/widget.js`
|
||||
script.async = true
|
||||
script.dataset.apiKey = apiKey
|
||||
script.addEventListener('load', () => resolve())
|
||||
script.addEventListener('error', () => {
|
||||
loadPromise = null
|
||||
reject(new Error(`Failed to load Bugpin widget from ${script.src}`))
|
||||
})
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
button.addEventListener('click', async (event) => {
|
||||
event.preventDefault()
|
||||
button.disabled = true
|
||||
try {
|
||||
await ensureLoaded()
|
||||
window.BugPin?.open()
|
||||
} catch (err) {
|
||||
console.error('[bug-report]', err)
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Type-check**
|
||||
|
||||
```
|
||||
npx astro check
|
||||
```
|
||||
|
||||
Expected: 0 new errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```
|
||||
git add src/ui/bug-report.ts
|
||||
git commit -m "feat(ui): lazy-load bugpin widget on footer click"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Wire footer button and module imports in `index.astro`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/index.astro`
|
||||
|
||||
- [ ] **Step 1: Add config bindings to the frontmatter script**
|
||||
|
||||
In the `---` block at the top of `src/pages/index.astro`, after `const remainingFooterLinks = footerLinks.slice(1)`, add:
|
||||
|
||||
```ts
|
||||
const bugReport = ui?.bugReport
|
||||
const bugpinConfig = bugReport?.enabled ? bugReport.bugpin : undefined
|
||||
const bugsinkDsn =
|
||||
bugReport?.enabled && bugReport.bugsink?.enabled ? bugReport.bugsink.dsn : ''
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the `data-bugsink-dsn` attribute to `<body>`**
|
||||
|
||||
Replace the existing `<body>` opening tag with:
|
||||
|
||||
```astro
|
||||
<body data-bugsink-dsn={bugsinkDsn}>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the footer button**
|
||||
|
||||
In the `<footer class="mystery-footer">` block (currently lines 89–109), append this AFTER the existing `{remainingFooterLinks.map(...)}` block, before the closing `</footer>`:
|
||||
|
||||
```astro
|
||||
{bugpinConfig && (
|
||||
<>
|
||||
<span aria-hidden="true">|</span>
|
||||
<button
|
||||
type="button"
|
||||
class="mystery-footer-bug-report"
|
||||
data-bug-report-trigger
|
||||
data-bugpin-server={bugpinConfig.serverUrl}
|
||||
data-bugpin-key={bugpinConfig.apiKey}
|
||||
>{bugReport?.label ?? 'Report a Bug'}</button>
|
||||
</>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Import the two new modules**
|
||||
|
||||
Replace the existing module-import script block (currently lines 130–133):
|
||||
|
||||
```astro
|
||||
<script>
|
||||
import '../ui/terminal.ts'
|
||||
import '../ui/theme.ts'
|
||||
</script>
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```astro
|
||||
<script>
|
||||
import '../ui/terminal.ts'
|
||||
import '../ui/theme.ts'
|
||||
import '../ui/error-tracking.ts'
|
||||
import '../ui/bug-report.ts'
|
||||
</script>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add CSS so the button matches the surrounding links**
|
||||
|
||||
Open `src/ui/crt.css` and grep for `.mystery-footer` to find the existing footer styles. Add a rule that makes the button look identical to the footer anchors. Append this block at the end of the existing `.mystery-footer` rules (search for `.mystery-footer a` and add directly after that selector's rule):
|
||||
|
||||
```css
|
||||
.mystery-footer-bug-report {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mystery-footer-bug-report:hover,
|
||||
.mystery-footer-bug-report:focus-visible {
|
||||
text-decoration: none;
|
||||
}
|
||||
.mystery-footer-bug-report:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: progress;
|
||||
}
|
||||
```
|
||||
|
||||
If `.mystery-footer a` styles its anchors differently (e.g. specific color, no underline by default), match those properties instead. Look at the actual `a` rule and copy its `color`/`text-decoration` declarations into `.mystery-footer-bug-report` so the two render visually identically.
|
||||
|
||||
- [ ] **Step 6: Run the full type check and build**
|
||||
|
||||
```
|
||||
npx astro check
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: 0 errors, build succeeds.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```
|
||||
git add src/pages/index.astro src/ui/crt.css
|
||||
git commit -m "feat(ui): add Report a Bug footer button and wire bug tracking modules"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Update TODOs
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/world/TODOs.md:45`
|
||||
|
||||
- [ ] **Step 1: Rewrite the TODO line**
|
||||
|
||||
Replace line 45 of `src/world/TODOs.md`:
|
||||
|
||||
```
|
||||
- [ ] Set up BugPin as a self-hosted visual bug reporter for the site, then have incoming reports create markdown files under `src/world/bugs/` via a webhook or API bridge so bugs can be tracked in git alongside the game content. Include screenshot/annotation metadata in the markdown and decide whether these bug docs stay outside the world loader or get their own loader later.
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```
|
||||
- [ ] Add a "Report a Bug" footer link backed by Bugpin (widget at bugpin.half.st → forwards to GitHub Issues). Add Bugsink (@sentry/browser → bugsink.half.st) for automatic JS error capture. Mark complete after manual verification: a test report appears in the Bugpin portal AND a GitHub issue is created AND a thrown error appears in the Bugsink portal.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```
|
||||
git add src/world/TODOs.md
|
||||
git commit -m "docs: rewrite TODO 45 to reflect Bugpin/Bugsink approach"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Manual smoke test
|
||||
|
||||
**Pre-req:** the Bugpin GitHub integration must be configured server-side before user reports will create issues. See spec §5. Steps below test the widget plumbing regardless.
|
||||
|
||||
- [ ] **Step 1: Start dev server**
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open the URL it prints (default `http://localhost:4321`).
|
||||
|
||||
- [ ] **Step 2: Verify the footer button renders**
|
||||
|
||||
In the rendered footer at the bottom of the page, confirm "Report a Bug" appears after "Source Code", styled like the surrounding links.
|
||||
|
||||
- [ ] **Step 3: Verify the widget lazy-loads**
|
||||
|
||||
Open DevTools → Network. Filter for `widget.js`. Confirm NO request to `bugpin.half.st/widget.js` on cold page load.
|
||||
|
||||
Click "Report a Bug". Confirm:
|
||||
- A single GET to `https://bugpin.half.st/widget.js` (status 200).
|
||||
- The Bugpin dialog opens.
|
||||
- Take a screenshot, write "smoke test" in the note, submit.
|
||||
- The dialog closes / shows a success state.
|
||||
|
||||
Re-click "Report a Bug". Confirm NO second `widget.js` request — only the dialog re-opens.
|
||||
|
||||
- [ ] **Step 4: Verify Bugpin received the report**
|
||||
|
||||
Open `https://bugpin.half.st/portal`. Confirm the "smoke test" report appears with the screenshot attached.
|
||||
|
||||
If the GitHub integration is configured: confirm a matching issue appears in the halfstreet GitHub repo.
|
||||
|
||||
- [ ] **Step 5: Verify Bugsink captures an uncaught error**
|
||||
|
||||
In the dev server's running page, open DevTools console and run:
|
||||
|
||||
```js
|
||||
setTimeout(() => { throw new Error('bugsink smoke test') }, 0)
|
||||
```
|
||||
|
||||
Open `https://bugsink.half.st`. Confirm a "bugsink smoke test" event appears within ~30 seconds.
|
||||
|
||||
- [ ] **Step 6: Verify the launcher styling is acceptable**
|
||||
|
||||
After step 3, the Bugpin widget's built-in floating launcher may now also be visible on the page. If its presence is visually objectionable, add a CSS rule to `src/ui/crt.css` targeting the Bugpin host element (inspect the DOM to find the selector — Bugpin uses Shadow DOM with a custom-element host like `<bugpin-widget>` or similar). Example:
|
||||
|
||||
```css
|
||||
bugpin-widget,
|
||||
[data-bugpin-launcher] {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
|
||||
If this turns out to be needed, commit the CSS as `style(ui): hide bugpin built-in launcher (we have our own trigger)`. If the launcher's appearance is fine as-is, skip this step.
|
||||
|
||||
- [ ] **Step 7: Mark TODO #45 complete**
|
||||
|
||||
Once steps 1–5 all pass, edit `src/world/TODOs.md` line 45, change `- [ ]` to `- [x]`, and commit:
|
||||
|
||||
```
|
||||
git add src/world/TODOs.md
|
||||
git commit -m "chore: mark TODO 45 complete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Done criteria
|
||||
|
||||
All of the following are true:
|
||||
|
||||
- `npm test` passes.
|
||||
- `npx astro check` reports 0 errors.
|
||||
- `npm run build` succeeds.
|
||||
- Manual smoke (Task 9 steps 1–5) all pass.
|
||||
- TODO #45 is `[x]`.
|
||||
@@ -0,0 +1,152 @@
|
||||
# Bug reporting: Bugpin (user) + Bugsink (auto)
|
||||
|
||||
**Status:** Approved design, ready for implementation plan.
|
||||
**Date:** 2026-05-17
|
||||
|
||||
## Goal
|
||||
|
||||
Let visitors report bugs in the Halfstreet site with a screenshot, and capture uncaught JavaScript errors automatically. Reports should flow to systems already running on the `half.st` host so there is no new infrastructure to manage.
|
||||
|
||||
## Two distinct flows
|
||||
|
||||
| | Bugpin | Bugsink |
|
||||
|---|---|---|
|
||||
| Trigger | Visitor clicks "Report a Bug" in footer | Uncaught JS error |
|
||||
| Capture | Screenshot, annotations, free-text note, console/network metadata | Stack trace, breadcrumbs |
|
||||
| Destination | Bugpin portal → auto-creates a GitHub Issue in the halfstreet repo | Bugsink portal only |
|
||||
| Library | `bugpin.half.st/widget.js` (lazy-loaded on click) | `@sentry/browser` npm package (loads on every page) |
|
||||
|
||||
The two are intentionally separate. Bugpin owns user-facing reports; Bugsink owns automatic exception capture. Sentry's user-feedback widget is **not** used — that would overlap Bugpin and muddle the responsibilities.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Footer link
|
||||
|
||||
`src/pages/index.astro`
|
||||
|
||||
Add "Report a Bug" to the footer chain after the existing links. Render as a `<button>` styled to match the surrounding `<a>` elements so it can dispatch JS without a hash-link navigation.
|
||||
|
||||
Final order:
|
||||
`© 2026 Ethan J Lewis | GNU 3.0 | Build #<n> | Source Code | Report a Bug`
|
||||
|
||||
The button only renders when `world.ui.bugReport?.enabled` is true and a Bugpin server URL is configured. If either is missing, the button is omitted (local dev stays clean).
|
||||
|
||||
### 2. Config
|
||||
|
||||
`src/world/ui.md` — add a `bugReport` frontmatter block:
|
||||
|
||||
```yaml
|
||||
bugReport:
|
||||
enabled: true
|
||||
label: "Report a Bug"
|
||||
bugpin:
|
||||
serverUrl: "https://bugpin.half.st"
|
||||
apiKey: "proj_07df4bf91f12445b8ef8c723e865ed7b"
|
||||
bugsink:
|
||||
enabled: true
|
||||
dsn: "https://231ef18b6b4f426ca249778cfddf821c@bugsink.half.st/1"
|
||||
```
|
||||
|
||||
The Bugpin project API key and the Bugsink DSN are both designed to ship to clients (they are public credentials that authenticate the project, not the operator), so checking them into the repo is acceptable.
|
||||
|
||||
`src/world/schema.ts` — extend `uiFrontmatterSchema` with an optional `bugReport` object:
|
||||
|
||||
```ts
|
||||
bugReport: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
label: z.string().trim().min(1).default('Report a Bug'),
|
||||
bugpin: z.object({
|
||||
serverUrl: z.url(),
|
||||
apiKey: z.string().trim().min(1),
|
||||
}).optional(),
|
||||
bugsink: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
dsn: z.url(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
```
|
||||
|
||||
`src/world/types.ts` — extend `UiConfig` with the matching TypeScript shape.
|
||||
|
||||
### 3. Bugpin lazy loader
|
||||
|
||||
New module: `src/ui/bug-report.ts`
|
||||
|
||||
Responsibilities:
|
||||
- Read Bugpin config from a `data-*` attribute on the footer button (so the module stays free of Astro imports).
|
||||
- On first click, inject `<script src="${serverUrl}/widget.js" data-api-key="${apiKey}" async>` into `<head>`. Hide Bugpin's built-in floating button using whatever option the widget exposes (likely `data-hidden="true"` or a CSS rule scoped via the widget's Shadow DOM hook — confirm against the install guide during implementation).
|
||||
- Once the script resolves, call `window.BugPin.open()`.
|
||||
- Cache the loaded state on a module-level flag; subsequent clicks just call `open()` again.
|
||||
- If the script fails to load (offline, server down), log to the console and surface a minimal inline message ("Couldn't open bug reporter — try refreshing"). Do not break the page.
|
||||
|
||||
Wire-up in `src/pages/index.astro`:
|
||||
- The footer button gets `data-bug-report-trigger`, `data-bugpin-server`, `data-bugpin-key` attributes.
|
||||
- A new `<script>` block imports `../ui/bug-report.ts` alongside the existing terminal/theme imports.
|
||||
|
||||
The lazy-load is deliberate: ~150 KB of widget code stays off the cold load for visitors who never click.
|
||||
|
||||
### 4. Bugsink init
|
||||
|
||||
New module: `src/ui/error-tracking.ts`
|
||||
|
||||
Responsibilities:
|
||||
- Add `@sentry/browser` as a dependency.
|
||||
- Read the DSN from a `data-bugsink-dsn` attribute set on `<body>` (or a meta tag) so the module is server-render-agnostic.
|
||||
- On import, call:
|
||||
```ts
|
||||
Sentry.init({
|
||||
dsn,
|
||||
tracesSampleRate: 0,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
integrations: [],
|
||||
})
|
||||
```
|
||||
- If `dsn` is missing or `bugsink.enabled` is false, no-op.
|
||||
|
||||
Error-capture only — no performance tracing, no session replay. Keeps the bundle small and avoids accidentally shipping a replay tool we didn't design for.
|
||||
|
||||
Wire-up in `src/pages/index.astro`:
|
||||
- A new `<script>` block imports `../ui/error-tracking.ts` near the existing imports.
|
||||
|
||||
### 5. Bugpin GitHub wiring
|
||||
|
||||
Out of scope for the code change, but recorded here so it isn't lost during implementation:
|
||||
|
||||
1. In the halfstreet GitHub repo, create a classic personal access token (or fine-grained equivalent) with `repo` scope.
|
||||
2. In Bugpin admin (https://bugpin.half.st/portal): Projects → halfstreet → Integrations → GitHub. Paste the token, select the halfstreet repo, set default labels (e.g., `bug`, `user-report`).
|
||||
3. Verify by submitting a test report from the footer link and confirming an issue appears in GitHub.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Markdown files under `src/world/bugs/`.** Original TODO #45 proposed this; explicitly dropped — Bugpin's portal + GitHub Issues replace that storage path.
|
||||
- **Gitea mirroring.** Bugpin does not natively support Gitea. Deferred until there's clear value in a Bugpin → Gitea bridge. The front-end design does not preclude this; it would be a server-side addition on the Bugpin host.
|
||||
- **Sentry user-feedback widget.** Bugpin owns user reports; Bugsink stays auto-only.
|
||||
- **PII redaction policy.** Bugpin's widget includes a built-in blur tool; Bugsink only captures stack traces, not request bodies. No additional scrubbing layer.
|
||||
|
||||
## Risks / things to verify during implementation
|
||||
|
||||
- **Bugpin "hide default button" mechanism.** Docs mention a floating launcher; the exact opt-out attribute needs to be confirmed against the install guide or the widget source. If no opt-out exists, fall back to CSS that hides the widget's launcher selector inside its Shadow DOM root (or wrap with a custom-element rule).
|
||||
- **Bugsink Sentry SDK compatibility.** Bugsink claims Sentry-SDK compatibility but tested feature surface varies. Verify with `@sentry/browser` v9 (current stable as of 2026-05) — if a specific minor version is needed, pin it.
|
||||
- **Cold-start cost.** Bugsink adds ~30–40 KB gzipped to the page. Acceptable for an authored, low-traffic site; flag if budget gets tight.
|
||||
|
||||
## TODOs.md update
|
||||
|
||||
Replace line 45 with:
|
||||
|
||||
```
|
||||
- [ ] Add a "Report a Bug" footer link backed by Bugpin (widget at bugpin.half.st → forwards to GitHub Issues). Add Bugsink (@sentry/browser → bugsink.half.st) for automatic JS error capture.
|
||||
```
|
||||
|
||||
Mark complete once the implementation lands and a test report has appeared in both Bugpin's portal and GitHub.
|
||||
|
||||
## Files touched (preview)
|
||||
|
||||
- `src/world/ui.md` — add `bugReport` block.
|
||||
- `src/world/schema.ts` — extend `uiFrontmatterSchema`.
|
||||
- `src/world/types.ts` — extend `UiConfig`.
|
||||
- `src/pages/index.astro` — render the button conditionally, wire data attributes, import the two new UI modules.
|
||||
- `src/ui/bug-report.ts` — **new**, lazy loads the Bugpin widget on click.
|
||||
- `src/ui/error-tracking.ts` — **new**, initializes Sentry SDK against Bugsink DSN.
|
||||
- `package.json` / `package-lock.json` — add `@sentry/browser`.
|
||||
- `src/world/TODOs.md` — rewrite line 45.
|
||||
Generated
+76
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^10.53.1",
|
||||
"astro": "^6.1.9",
|
||||
"yaml": "^2.8.4",
|
||||
"zod": "^4.4.3"
|
||||
@@ -1766,6 +1767,81 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.53.1.tgz",
|
||||
"integrity": "sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.53.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.53.1.tgz",
|
||||
"integrity": "sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "10.53.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.53.1.tgz",
|
||||
"integrity": "sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "10.53.1",
|
||||
"@sentry/core": "10.53.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.53.1.tgz",
|
||||
"integrity": "sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "10.53.1",
|
||||
"@sentry/core": "10.53.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.53.1.tgz",
|
||||
"integrity": "sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "10.53.1",
|
||||
"@sentry-internal/feedback": "10.53.1",
|
||||
"@sentry-internal/replay": "10.53.1",
|
||||
"@sentry-internal/replay-canvas": "10.53.1",
|
||||
"@sentry/core": "10.53.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "10.53.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.53.1.tgz",
|
||||
"integrity": "sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/browser": "^10.53.1",
|
||||
"astro": "^6.1.9",
|
||||
"yaml": "^2.8.4",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
+21
-1
@@ -7,6 +7,10 @@ const ui = world.ui
|
||||
const footerLinks = ui?.footer.links ?? []
|
||||
const firstFooterLink = footerLinks[0]
|
||||
const remainingFooterLinks = footerLinks.slice(1)
|
||||
const bugReport = ui?.bugReport
|
||||
const bugpinConfig = bugReport?.enabled ? bugReport.bugpin : undefined
|
||||
const bugsinkDsn =
|
||||
bugReport?.enabled && bugReport.bugsink?.enabled ? bugReport.bugsink.dsn : ''
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
@@ -23,7 +27,7 @@ const remainingFooterLinks = footerLinks.slice(1)
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content={ui?.themeColor ?? '#1a0d00'} />
|
||||
</head>
|
||||
<body>
|
||||
<body data-bugsink-dsn={bugsinkDsn}>
|
||||
<div class="mystery-root" data-mystery-root>
|
||||
<div class="mystery-bezel">
|
||||
<div class="mystery-options" data-mystery-options>
|
||||
@@ -106,6 +110,20 @@ const remainingFooterLinks = footerLinks.slice(1)
|
||||
<a href={link.href}>{link.label}</a>
|
||||
</>
|
||||
))}
|
||||
{bugpinConfig && (
|
||||
<>
|
||||
<span aria-hidden="true">|</span>
|
||||
<button
|
||||
type="button"
|
||||
class="mystery-footer-bug-report"
|
||||
data-bug-report-trigger
|
||||
data-bugpin-server={bugpinConfig.serverUrl}
|
||||
data-bugpin-key={bugpinConfig.apiKey}
|
||||
aria-describedby="bug-report-status"
|
||||
>{bugReport?.label ?? 'Report a Bug'}</button>
|
||||
<span id="bug-report-status" class="mystery-footer-status" aria-live="polite"></span>
|
||||
</>
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
<script>
|
||||
@@ -130,6 +148,8 @@ const remainingFooterLinks = footerLinks.slice(1)
|
||||
<script>
|
||||
import '../ui/terminal.ts'
|
||||
import '../ui/theme.ts'
|
||||
import '../ui/error-tracking.ts'
|
||||
import '../ui/bug-report.ts'
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
interface BugPinAPI {
|
||||
init: (config: { apiKey: string, serverUrl: string }) => Promise<void> | void
|
||||
open: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
BugPin?: BugPinAPI
|
||||
}
|
||||
}
|
||||
|
||||
const launcherSelector = [
|
||||
'.fixed.bottom-5.right-5',
|
||||
'.fixed.bottom-5.left-5',
|
||||
'.fixed.top-5.right-5',
|
||||
'.fixed.top-5.left-5',
|
||||
].join(',')
|
||||
const button = document.querySelector<HTMLButtonElement>('[data-bug-report-trigger]')
|
||||
let loadPromise: Promise<void> | null = null
|
||||
let initialized = false
|
||||
let launcherObserver: MutationObserver | null = null
|
||||
|
||||
function setStatus(message: string): void {
|
||||
const targetId = button?.getAttribute('aria-describedby')
|
||||
const target = targetId ? document.getElementById(targetId) : null
|
||||
if (target) target.textContent = message
|
||||
}
|
||||
|
||||
function buildWidgetSrc(serverUrl: string): string {
|
||||
return `${serverUrl.replace(/\/$/, '')}/widget.js`
|
||||
}
|
||||
|
||||
function loadWidget(serverUrl: string): Promise<void> {
|
||||
if (window.BugPin) return Promise.resolve()
|
||||
if (loadPromise) return loadPromise
|
||||
|
||||
loadPromise = new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = buildWidgetSrc(serverUrl)
|
||||
script.async = true
|
||||
script.addEventListener('load', () => resolve())
|
||||
script.addEventListener('error', () => {
|
||||
loadPromise = null
|
||||
reject(new Error(`Failed to load BugPin widget from ${script.src}`))
|
||||
})
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
function hideBugPinLauncher(): void {
|
||||
const host = document.getElementById('bugpin-widget')
|
||||
const root = host?.shadowRoot
|
||||
if (!root) return
|
||||
|
||||
for (const launcher of root.querySelectorAll<HTMLElement>(launcherSelector)) {
|
||||
launcher.hidden = true
|
||||
launcher.style.display = 'none'
|
||||
launcher.setAttribute('aria-hidden', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
function watchBugPinLauncher(): void {
|
||||
const host = document.getElementById('bugpin-widget')
|
||||
const root = host?.shadowRoot
|
||||
if (!root || launcherObserver) return
|
||||
|
||||
hideBugPinLauncher()
|
||||
launcherObserver = new MutationObserver(() => hideBugPinLauncher())
|
||||
launcherObserver.observe(root, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
function waitForBugPinEffects(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => setTimeout(resolve, 0))
|
||||
})
|
||||
}
|
||||
|
||||
async function openBugReport(serverUrl: string, apiKey: string): Promise<void> {
|
||||
await loadWidget(serverUrl)
|
||||
|
||||
if (!window.BugPin) {
|
||||
throw new Error('BugPin widget loaded, but window.BugPin is unavailable')
|
||||
}
|
||||
|
||||
if (!initialized) {
|
||||
await window.BugPin.init({ apiKey, serverUrl })
|
||||
initialized = true
|
||||
watchBugPinLauncher()
|
||||
await waitForBugPinEffects()
|
||||
}
|
||||
|
||||
hideBugPinLauncher()
|
||||
window.BugPin.open()
|
||||
hideBugPinLauncher()
|
||||
}
|
||||
|
||||
if (button) {
|
||||
const serverUrl = button.dataset.bugpinServer
|
||||
const apiKey = button.dataset.bugpinKey
|
||||
|
||||
if (serverUrl && apiKey) {
|
||||
button.addEventListener('click', async (event) => {
|
||||
event.preventDefault()
|
||||
setStatus('')
|
||||
button.disabled = true
|
||||
|
||||
try {
|
||||
await openBugReport(serverUrl, apiKey)
|
||||
} catch (err) {
|
||||
console.error('[bug-report]', err)
|
||||
setStatus("Couldn't open bug reporter - try refreshing.")
|
||||
} finally {
|
||||
button.disabled = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (window.BugPin) {
|
||||
requestAnimationFrame(() => watchBugPinLauncher())
|
||||
}
|
||||
|
||||
export {}
|
||||
+25
-1
@@ -89,11 +89,35 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mystery-footer a {
|
||||
.mystery-footer a,
|
||||
.mystery-footer-bug-report {
|
||||
color: var(--m-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mystery-footer-bug-report {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mystery-footer-bug-report:hover,
|
||||
.mystery-footer-bug-report:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mystery-footer-bug-report:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.mystery-footer-status {
|
||||
color: var(--m-fg);
|
||||
}
|
||||
|
||||
.mystery-footer span {
|
||||
margin: 0 0.5ch;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as Sentry from '@sentry/browser'
|
||||
|
||||
const dsn = document.body?.dataset.bugsinkDsn ?? ''
|
||||
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn,
|
||||
tracesSampleRate: 0,
|
||||
replaysSessionSampleRate: 0,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
})
|
||||
}
|
||||
Vendored
+4
-4
@@ -4,7 +4,7 @@
|
||||
"showTags": false,
|
||||
"showAttachments": false,
|
||||
"hideUnresolved": false,
|
||||
"showOrphans": true,
|
||||
"showOrphans": false,
|
||||
"collapse-color-groups": false,
|
||||
"colorGroups": [
|
||||
{
|
||||
@@ -37,9 +37,9 @@
|
||||
}
|
||||
],
|
||||
"collapse-display": false,
|
||||
"showArrow": false,
|
||||
"textFadeMultiplier": 0,
|
||||
"nodeSizeMultiplier": 1.82265625,
|
||||
"showArrow": true,
|
||||
"textFadeMultiplier": -3,
|
||||
"nodeSizeMultiplier": 2.2138427734375,
|
||||
"lineSizeMultiplier": 5,
|
||||
"collapse-forces": false,
|
||||
"centerStrength": 0.518713248970312,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
short:
|
||||
readable:
|
||||
takeable:
|
||||
names:
|
||||
---
|
||||
@@ -17,6 +17,7 @@ views:
|
||||
- exitW
|
||||
- exitU
|
||||
- exitD
|
||||
- items
|
||||
- type: cards
|
||||
name: Items
|
||||
filters:
|
||||
@@ -29,3 +30,11 @@ views:
|
||||
- takeable
|
||||
- names
|
||||
- formula.Room
|
||||
- type: cards
|
||||
name: Items By Room
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("items")
|
||||
order:
|
||||
- file.name
|
||||
- formula.Room
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@
|
||||
- [ ] FEATURE: Add item Revolver, which can be used to kill the thing at the end. It comes with no bullets, so players need to defeat the safe-cracking minjgame. Place the revolver somewhere it takes the player some effort to find.
|
||||
- [ ] Feature: A faceless voice that speaks in a whisper in the smoking room, which demands whiskey and dispenses riddles if the player finds a bottle of whiskey (but hasn't drunk it yet). The riddles are randomly chosen from a list of 25 difficult riddles (source them). If the riddle is answered correctly there needs to be some reward and maybe a major plot point is revealed.
|
||||
- [x] Add item Whiskey bottle, half full of something smoky. In the kitchen. Drinking it gets the player drunk, which causes them to unlock the drunk rooms, which are a series of rooms where they can go many directions, but somehow end up in seemingly back in the same spot. They are essentially a maze of doors and hallways, ladders and levels. There should be boundaries established though, it shouldn't be endless. After 20 or so moves the player passes out and wakes up back in the foyer, with the whiskey bottle returned to the kitchen, somehow still half full. Add a random encounter in the dark rooms, a creaking floorboard helps you find a secret door that opens with a faceless man inside who gives you major plot info. After this encounter you pass out and wake up in the lobby as before. When the player is "drunk" the text on the terminal should appear blurry, like the user has double vision.
|
||||
- [ ] Set up BugPin as a self-hosted visual bug reporter for the site, then have incoming reports create markdown files under `src/world/bugs/` via a webhook or API bridge so bugs can be tracked in git alongside the game content. Include screenshot/annotation metadata in the markdown and decide whether these bug docs stay outside the world loader or get their own loader later.
|
||||
- [ ] Add a "Report a Bug" footer link backed by Bugpin (widget at bugpin.half.st -> forwards to GitHub Issues). Add Bugsink (@sentry/browser -> bugsink.half.st) for automatic JS error capture. Mark complete after manual verification: a test report appears in the Bugpin portal AND a GitHub issue is created AND a thrown error appears in the Bugsink portal.
|
||||
- [ ] BUG: It says the door closes behind you when you enter the lobby, but you can still exit S to the gate.
|
||||
- [x] FEATURE: Add a short "typed" effect to the text. Make it look like it's being typed out, if that makes sense, one character at a time. The effect should be brief.
|
||||
- [x] FEATURE: Whenever you change rooms, scroll the text so the name of the room you're in is at the top. Users can scroll up to see the history. This should be an effect where the old text slides up to make room for the new text, and this should happen before the "typed" effect.
|
||||
|
||||
@@ -117,6 +117,65 @@ describe('uiFrontmatterSchema', () => {
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
|
||||
})
|
||||
|
||||
it('accepts a bugReport block with bugpin and bugsink subconfigs', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: {
|
||||
copyright: '© 2026 Ethan J Lewis',
|
||||
links: [],
|
||||
},
|
||||
bugReport: {
|
||||
enabled: true,
|
||||
label: 'Report a Bug',
|
||||
bugpin: {
|
||||
serverUrl: 'https://bugpin.half.st',
|
||||
apiKey: 'proj_07df4bf91f12445b8ef8c723e865ed7b',
|
||||
},
|
||||
bugsink: {
|
||||
enabled: true,
|
||||
dsn: 'https://231ef18b6b4f426ca249778cfddf821c@bugsink.half.st/1',
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it('accepts ui config with no bugReport block at all', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a bugpin block with a non-url serverUrl', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
|
||||
bugReport: {
|
||||
enabled: true,
|
||||
bugpin: { serverUrl: 'not-a-url', apiKey: 'proj_x' },
|
||||
},
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
|
||||
})
|
||||
|
||||
it('rejects a bugsink block with a non-url dsn', () => {
|
||||
const data = {
|
||||
pageTitle: 'Halfstreet',
|
||||
description: 'A gothic mystery.',
|
||||
footer: { copyright: '© 2026 Ethan J Lewis', links: [] },
|
||||
bugReport: {
|
||||
enabled: true,
|
||||
bugsink: { dsn: 'whatever' },
|
||||
},
|
||||
}
|
||||
expect(() => uiFrontmatterSchema.parse(data)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('lightMechanicFrontmatterSchema', () => {
|
||||
|
||||
@@ -71,6 +71,18 @@ export const uiFrontmatterSchema = z.object({
|
||||
typedEffect: true,
|
||||
roomScroll: true,
|
||||
}),
|
||||
bugReport: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
label: z.string().trim().min(1).default('Report a Bug'),
|
||||
bugpin: z.object({
|
||||
serverUrl: z.url(),
|
||||
apiKey: z.string().trim().min(1),
|
||||
}).optional(),
|
||||
bugsink: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
dsn: z.url(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
export type UiFrontmatter = z.infer<typeof uiFrontmatterSchema>
|
||||
|
||||
@@ -82,6 +82,18 @@ export interface UiConfig {
|
||||
typedEffect: boolean
|
||||
roomScroll: boolean
|
||||
}
|
||||
bugReport?: {
|
||||
enabled: boolean
|
||||
label: string
|
||||
bugpin?: {
|
||||
serverUrl: string
|
||||
apiKey: string
|
||||
}
|
||||
bugsink?: {
|
||||
enabled: boolean
|
||||
dsn: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type LightBurnTrigger = 'move' | 'wait'
|
||||
|
||||
@@ -18,6 +18,15 @@ features:
|
||||
lightMeter: true
|
||||
typedEffect: true
|
||||
roomScroll: true
|
||||
bugReport:
|
||||
enabled: true
|
||||
label: "Report a Bug"
|
||||
bugpin:
|
||||
serverUrl: "https://bugpin.half.st"
|
||||
apiKey: "proj_07df4bf91f12445b8ef8c723e865ed7b"
|
||||
bugsink:
|
||||
enabled: true
|
||||
dsn: "https://231ef18b6b4f426ca249778cfddf821c@bugsink.half.st/1"
|
||||
---
|
||||
|
||||
# UI
|
||||
|
||||
Reference in New Issue
Block a user