feat(ui): add bug reporting integrations
This commit is contained in:
Generated
+76
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sentry/browser": "^10.53.1",
|
||||||
"astro": "^6.1.9",
|
"astro": "^6.1.9",
|
||||||
"yaml": "^2.8.4",
|
"yaml": "^2.8.4",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
@@ -1766,6 +1767,81 @@
|
|||||||
"win32"
|
"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": {
|
"node_modules/@shikijs/core": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sentry/browser": "^10.53.1",
|
||||||
"astro": "^6.1.9",
|
"astro": "^6.1.9",
|
||||||
"yaml": "^2.8.4",
|
"yaml": "^2.8.4",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|||||||
+21
-1
@@ -7,6 +7,10 @@ const ui = world.ui
|
|||||||
const footerLinks = ui?.footer.links ?? []
|
const footerLinks = ui?.footer.links ?? []
|
||||||
const firstFooterLink = footerLinks[0]
|
const firstFooterLink = footerLinks[0]
|
||||||
const remainingFooterLinks = footerLinks.slice(1)
|
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">
|
<html lang="en">
|
||||||
@@ -23,7 +27,7 @@ const remainingFooterLinks = footerLinks.slice(1)
|
|||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta name="theme-color" content={ui?.themeColor ?? '#1a0d00'} />
|
<meta name="theme-color" content={ui?.themeColor ?? '#1a0d00'} />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-bugsink-dsn={bugsinkDsn}>
|
||||||
<div class="mystery-root" data-mystery-root>
|
<div class="mystery-root" data-mystery-root>
|
||||||
<div class="mystery-bezel">
|
<div class="mystery-bezel">
|
||||||
<div class="mystery-options" data-mystery-options>
|
<div class="mystery-options" data-mystery-options>
|
||||||
@@ -106,6 +110,20 @@ const remainingFooterLinks = footerLinks.slice(1)
|
|||||||
<a href={link.href}>{link.label}</a>
|
<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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -130,6 +148,8 @@ const remainingFooterLinks = footerLinks.slice(1)
|
|||||||
<script>
|
<script>
|
||||||
import '../ui/terminal.ts'
|
import '../ui/terminal.ts'
|
||||||
import '../ui/theme.ts'
|
import '../ui/theme.ts'
|
||||||
|
import '../ui/error-tracking.ts'
|
||||||
|
import '../ui/bug-report.ts'
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mystery-footer a {
|
.mystery-footer a,
|
||||||
|
.mystery-footer-bug-report {
|
||||||
color: var(--m-fg);
|
color: var(--m-fg);
|
||||||
text-decoration: none;
|
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 {
|
.mystery-footer span {
|
||||||
margin: 0 0.5ch;
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
+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: 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.
|
- [ ] 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.
|
- [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.
|
- [ ] 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: 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.
|
- [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()
|
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', () => {
|
describe('lightMechanicFrontmatterSchema', () => {
|
||||||
|
|||||||
@@ -71,6 +71,18 @@ export const uiFrontmatterSchema = z.object({
|
|||||||
typedEffect: true,
|
typedEffect: true,
|
||||||
roomScroll: 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>
|
export type UiFrontmatter = z.infer<typeof uiFrontmatterSchema>
|
||||||
|
|||||||
@@ -82,6 +82,18 @@ export interface UiConfig {
|
|||||||
typedEffect: boolean
|
typedEffect: boolean
|
||||||
roomScroll: boolean
|
roomScroll: boolean
|
||||||
}
|
}
|
||||||
|
bugReport?: {
|
||||||
|
enabled: boolean
|
||||||
|
label: string
|
||||||
|
bugpin?: {
|
||||||
|
serverUrl: string
|
||||||
|
apiKey: string
|
||||||
|
}
|
||||||
|
bugsink?: {
|
||||||
|
enabled: boolean
|
||||||
|
dsn: string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LightBurnTrigger = 'move' | 'wait'
|
export type LightBurnTrigger = 'move' | 'wait'
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ features:
|
|||||||
lightMeter: true
|
lightMeter: true
|
||||||
typedEffect: true
|
typedEffect: true
|
||||||
roomScroll: 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
|
# UI
|
||||||
|
|||||||
Reference in New Issue
Block a user