Put the bug report behind an always-available button.
The debug report was only reachable in DEBUG mode, which is the one mode
a player hitting a real bug won't be running. Move it to a "🐛 Report a
bug" item in the game menu: a note field, a download, and a copy-as-text,
on any server.
That knowingly shows the reporter their opponents' hands and the shop
deck order. Credentials are still required, so a report only ever reaches
someone seated at that table. The buy-any-card panel stays DEBUG-only —
it changes the game; a report only reads it.
This commit is contained in:
@@ -43,14 +43,23 @@ Set via environment or a `.env` file (see `.env.example`):
|
|||||||
| `DATA_DIR` | `data` | Directory holding the SQLite DB |
|
| `DATA_DIR` | `data` | Directory holding the SQLite DB |
|
||||||
| `PORT` | `8080` | HTTP port |
|
| `PORT` | `8080` | HTTP port |
|
||||||
| `STATIC_DIR` | `web/dist` | Built frontend to serve |
|
| `STATIC_DIR` | `web/dist` | Built frontend to serve |
|
||||||
| `DEBUG` | off | Unlocks the in-game debug panel (see below) |
|
| `DEBUG` | off | Unlocks the in-game buy-any-card panel |
|
||||||
|
|
||||||
## Debugging a game that went wrong
|
## Debugging a game that went wrong
|
||||||
|
|
||||||
Any saved game can be dumped as a **debug report**: the full state (every deck
|
Any game can be dumped as a **debug report**: the full state (every deck card by
|
||||||
card by card, the shop row and the order of the remaining tier decks, discards,
|
card, the shop row and the order of the remaining tier decks, discards, pending
|
||||||
pending choices, each player's banked Mana/Trumpets/apples), the round's battles
|
choices, each player's banked Mana/Trumpets/apples), the round's battles with
|
||||||
with their lineups and *the dice they rolled*, and the entire event log.
|
their lineups and *the dice they rolled*, and the entire event log.
|
||||||
|
|
||||||
|
**In game**, the `⋯` menu has **🐛 Report a bug**: a note field, a
|
||||||
|
**Download report** button, and **Copy as text**. It works on any server, DEBUG
|
||||||
|
or not, so a bug hit in a real game can actually be reported. That does hand the
|
||||||
|
player their opponents' hands and the shop deck order — an accepted trade, since
|
||||||
|
a report only ever goes to someone seated at that table (`GET
|
||||||
|
/api/debug/report`, same credentials as the WebSocket).
|
||||||
|
|
||||||
|
**From the command line**, against the server's database:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
mise run report # list recent games
|
mise run report # list recent games
|
||||||
@@ -58,11 +67,6 @@ mise run report -- QWERT # the report, as text, for reading
|
|||||||
mise run report -- -json -out internal/game/testdata/bug.json QWERT
|
mise run report -- -json -out internal/game/testdata/bug.json QWERT
|
||||||
```
|
```
|
||||||
|
|
||||||
With `DEBUG=1` the in-game 🐛 panel grows two buttons — **Download JSON** and
|
|
||||||
**Copy as text** — that pull the same report over `GET /api/debug/report`. A
|
|
||||||
report exposes both players' hands and the shop deck order, so that endpoint is
|
|
||||||
DEBUG-only; on a live server use `cmd/report`, which reads the database.
|
|
||||||
|
|
||||||
The JSON form is the useful one, because it replays. Drop it in
|
The JSON form is the useful one, because it replays. Drop it in
|
||||||
`internal/game/testdata/` and the battle re-runs exactly — same lineups, same
|
`internal/game/testdata/` and the battle re-runs exactly — same lineups, same
|
||||||
dice, same events, right down to the log text:
|
dice, same events, right down to the log text:
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import (
|
|||||||
// work out which effect misfired. Report.Text() ends with a paste-ready test.
|
// work out which effect misfired. Report.Text() ends with a paste-ready test.
|
||||||
//
|
//
|
||||||
// Both forms contain hidden information — opponents' decks, the order of the
|
// Both forms contain hidden information — opponents' decks, the order of the
|
||||||
// shop decks — so a report is an operator's artifact, not a player's. The
|
// shop decks — and that is an accepted trade: the in-game "Report a bug" button
|
||||||
// server only serves one in DEBUG mode; `go run ./cmd/report` pulls one out of
|
// hands a player their own report on any server, DEBUG or not, because a bug
|
||||||
// the database with no such gate, because by then you are the operator.
|
// nobody can reproduce costs more than the little a cheat would gain.
|
||||||
|
// `go run ./cmd/report` reads the same report out of the database.
|
||||||
|
|
||||||
// DebugReportVersion is the report format's version, so an old report found on
|
// DebugReportVersion is the report format's version, so an old report found on
|
||||||
// disk can be recognized for what it is.
|
// disk can be recognized for what it is.
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"github.com/greyson/super-auto-pets-board-game/internal/store"
|
"github.com/greyson/super-auto-pets-board-game/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// reportServer stands up a server holding one started two-player game, and
|
// reportServer stands up an ordinary server — DEBUG off, the way it runs in
|
||||||
// returns it with the credentials for seat 0.
|
// production — holding one started two-player game, and returns it with the
|
||||||
func reportServer(t *testing.T, debug bool) (*httptest.Server, *game.Game, *game.Player) {
|
// credentials for seat 0.
|
||||||
|
func reportServer(t *testing.T) (*httptest.Server, *game.Game, *game.Player) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
st, err := store.Open(t.TempDir())
|
st, err := store.Open(t.TempDir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -31,8 +32,8 @@ func reportServer(t *testing.T, debug bool) (*httptest.Server, *game.Game, *game
|
|||||||
if err := st.Save(g); err != nil {
|
if err := st.Save(g); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
srv := New(st, "", debug)
|
srv := New(st, "", false)
|
||||||
srv.rooms[g.ID] = &room{game: g, conns: map[*client]struct{}{}, debug: debug}
|
srv.rooms[g.ID] = &room{game: g, conns: map[*client]struct{}{}}
|
||||||
ts := httptest.NewServer(srv.Handler())
|
ts := httptest.NewServer(srv.Handler())
|
||||||
t.Cleanup(ts.Close)
|
t.Cleanup(ts.Close)
|
||||||
return ts, g, p1
|
return ts, g, p1
|
||||||
@@ -52,10 +53,10 @@ func get(t *testing.T, url string) (int, string) {
|
|||||||
return res.StatusCode, string(body)
|
return res.StatusCode, string(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In DEBUG mode a seated player can pull the report, in both forms, and the
|
// A seated player can pull the report in both forms, and the JSON one restores
|
||||||
// JSON one restores to the game the server is actually holding.
|
// to the game the server is actually holding.
|
||||||
func TestDebugReportEndpoint(t *testing.T) {
|
func TestDebugReportEndpoint(t *testing.T) {
|
||||||
ts, g, p1 := reportServer(t, true)
|
ts, g, p1 := reportServer(t)
|
||||||
url := ts.URL + "/api/debug/report?game=" + g.ID + "&player=" + p1.ID + "&token=" + p1.Token
|
url := ts.URL + "/api/debug/report?game=" + g.ID + "&player=" + p1.ID + "&token=" + p1.Token
|
||||||
|
|
||||||
status, body := get(t, url+"¬e=shop+row+looked+wrong")
|
status, body := get(t, url+"¬e=shop+row+looked+wrong")
|
||||||
@@ -90,16 +91,16 @@ func TestDebugReportEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A report exposes both players' decks and the shop deck order, so it must not
|
// The in-game bug report button has to work on a normal server, so the endpoint
|
||||||
// be reachable without DEBUG, and never on someone else's credentials.
|
// is not DEBUG-gated — but it is still a seated player's own artifact, and never
|
||||||
func TestDebugReportIsGated(t *testing.T) {
|
// reachable on someone else's credentials.
|
||||||
ts, g, p1 := reportServer(t, false)
|
func TestDebugReportNeedsCredentialsNotDebugMode(t *testing.T) {
|
||||||
|
ts, g, p1 := reportServer(t)
|
||||||
status, _ := get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player="+p1.ID+"&token="+p1.Token)
|
status, _ := get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player="+p1.ID+"&token="+p1.Token)
|
||||||
if status != http.StatusNotFound {
|
if status != http.StatusOK {
|
||||||
t.Fatalf("without DEBUG the endpoint should 404, got %d", status)
|
t.Fatalf("a player should get their report without DEBUG, got %d", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
ts, g, p1 = reportServer(t, true)
|
|
||||||
status, _ = get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player="+p1.ID+"&token=wrong")
|
status, _ = get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player="+p1.ID+"&token=wrong")
|
||||||
if status != http.StatusForbidden {
|
if status != http.StatusForbidden {
|
||||||
t.Fatalf("a bad token should 403, got %d", status)
|
t.Fatalf("a bad token should 403, got %d", status)
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ func (s *Server) Handler() http.Handler {
|
|||||||
// `format=text` renders it for reading instead of as JSON, and `note=` records
|
// `format=text` renders it for reading instead of as JSON, and `note=` records
|
||||||
// what looked wrong.
|
// what looked wrong.
|
||||||
//
|
//
|
||||||
// A report holds hidden information — the opponents' decks, the order of the
|
// This backs the in-game "Report a bug" button, so it is deliberately not
|
||||||
// shop decks — so a seated player must not be able to pull one mid-game. It is
|
// DEBUG-gated: a reproducible bug report is worth more than the hidden
|
||||||
// DEBUG-only for that reason; on a live server, `go run ./cmd/report` reads the
|
// information a report gives away. It does hand a seated player their
|
||||||
// same report straight out of the database instead.
|
// opponents' hands and the order of the shop decks, which a determined one
|
||||||
|
// could read mid-game — the trade accepted here is that a player who wants to
|
||||||
|
// cheat gains little and a player who hits a bug can actually report it.
|
||||||
|
// Credentials are still required, so a report only ever goes to someone at that
|
||||||
|
// table.
|
||||||
func (s *Server) handleDebugReport(w http.ResponseWriter, req *http.Request) {
|
func (s *Server) handleDebugReport(w http.ResponseWriter, req *http.Request) {
|
||||||
if !s.debug {
|
|
||||||
httpError(w, http.StatusNotFound, "debug reports are only served in DEBUG mode")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
r, err := s.getRoom(q.Get("game"))
|
r, err := s.getRoom(q.Get("game"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import type { Session } from '../types'
|
||||||
|
import { debugReportURL, fetchDebugReportText } from '../api'
|
||||||
|
|
||||||
|
// BugReport is the dialog behind the game menu's "Report a bug": it hands the
|
||||||
|
// player the game's debug report — the whole state, both hands, every battle
|
||||||
|
// with the dice it rolled, and the event log — to attach to a bug. Downloading
|
||||||
|
// the JSON is the useful one, since that form replays in a test.
|
||||||
|
//
|
||||||
|
// Unlike the debug card panel this is always available, not DEBUG-gated: a bug
|
||||||
|
// is worth more than the hidden information the report gives away, and it only
|
||||||
|
// ever goes to the player who asked for it.
|
||||||
|
export function BugReport({ session, onClose }: { session: Session; onClose: () => void }) {
|
||||||
|
const [note, setNote] = useState('')
|
||||||
|
const [status, setStatus] = useState('')
|
||||||
|
|
||||||
|
const copyText = async () => {
|
||||||
|
setStatus('Fetching…')
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(await fetchDebugReportText(session, note))
|
||||||
|
setStatus('Copied to the clipboard.')
|
||||||
|
} catch {
|
||||||
|
setStatus('Could not copy it — try the download instead.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="modal-backdrop" onClick={onClose}>
|
||||||
|
<div className="modal bug-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2 className="bug-title">🐛 Report a bug</h2>
|
||||||
|
<p className="bug-blurb">
|
||||||
|
This saves everything about the game as it stands — both decks, the shop, every
|
||||||
|
battle with the dice it rolled, and the full event log — so the situation can be
|
||||||
|
replayed exactly and fixed.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="bug-note"
|
||||||
|
value={note}
|
||||||
|
onChange={(e) => setNote(e.target.value)}
|
||||||
|
placeholder="What went wrong? (optional, but it helps a lot)"
|
||||||
|
rows={3}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className="bug-actions">
|
||||||
|
<a
|
||||||
|
className="btn"
|
||||||
|
href={debugReportURL(session, 'json', note)}
|
||||||
|
onClick={() => setStatus('Downloaded — attach that file to the bug.')}
|
||||||
|
download
|
||||||
|
>
|
||||||
|
⬇ Download report
|
||||||
|
</a>
|
||||||
|
<button className="btn btn-ghost" onClick={copyText}>
|
||||||
|
📋 Copy as text
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{status && <div className="muted">{status}</div>}
|
||||||
|
<button className="btn btn-ghost btn-sm" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,28 +1,20 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { Card, ClientMessage, Session } from '../types'
|
import type { Card, ClientMessage } from '../types'
|
||||||
import { debugReportURL, fetchCatalog, fetchDebugReportText } from '../api'
|
import { fetchCatalog } from '../api'
|
||||||
import { CardView } from './CardView'
|
import { CardView } from './CardView'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canGrant: boolean // shop phase — grants only land then
|
canGrant: boolean // shop phase — grants only land then
|
||||||
packs: string[] // packs in play, so the catalog matches the game
|
packs: string[] // packs in play, so the catalog matches the game
|
||||||
session: Session // credentials for the debug report endpoint
|
|
||||||
send: (msg: ClientMessage) => void
|
send: (msg: ClientMessage) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugPanel is a testing aid (server DEBUG mode only). Two things live here:
|
// DebugPanel is a testing aid (server DEBUG mode only): a collapsible drawer
|
||||||
// a grab-any-card list of every card in the packs in play, tier by tier, and
|
// listing every card in the packs in play, tier by tier. Clicking one drops it
|
||||||
// the debug report — a dump of the whole game (both decks, the battles and their
|
// into your deck for free, off-turn.
|
||||||
// dice, the event log) that can be replayed in a test.
|
export function DebugPanel({ canGrant, packs, send }: Props) {
|
||||||
export function DebugPanel({ canGrant, packs, session, send }: Props) {
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [catalog, setCatalog] = useState<Card[]>([])
|
const [catalog, setCatalog] = useState<Card[]>([])
|
||||||
// What the report button last did, shown next to it and cleared on the next
|
|
||||||
// click — a report is silent otherwise, and copying especially so.
|
|
||||||
const [reportMsg, setReportMsg] = useState('')
|
|
||||||
// Travels with the report, so the dump itself says what looked wrong. Shared
|
|
||||||
// by both buttons: the download is a plain link and can't ask for it later.
|
|
||||||
const [note, setNote] = useState('')
|
|
||||||
|
|
||||||
// Joined into a stable key so a fresh array identity each render doesn't refetch.
|
// Joined into a stable key so a fresh array identity each render doesn't refetch.
|
||||||
const packKey = packs.join(',')
|
const packKey = packs.join(',')
|
||||||
@@ -34,17 +26,6 @@ export function DebugPanel({ canGrant, packs, session, send }: Props) {
|
|||||||
|
|
||||||
const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b)
|
const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b)
|
||||||
|
|
||||||
const copyReport = async () => {
|
|
||||||
setReportMsg('…')
|
|
||||||
try {
|
|
||||||
const text = await fetchDebugReportText(session, note)
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setReportMsg('copied to the clipboard')
|
|
||||||
} catch {
|
|
||||||
setReportMsg('failed — is the server in DEBUG mode?')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`debug-panel ${open ? 'is-open' : ''}`}>
|
<div className={`debug-panel ${open ? 'is-open' : ''}`}>
|
||||||
<button className="debug-toggle" onClick={() => setOpen((o) => !o)}>
|
<button className="debug-toggle" onClick={() => setOpen((o) => !o)}>
|
||||||
@@ -52,27 +33,6 @@ export function DebugPanel({ canGrant, packs, session, send }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="debug-body">
|
<div className="debug-body">
|
||||||
<div className="debug-head">Debug report</div>
|
|
||||||
<div className="debug-report">
|
|
||||||
<input
|
|
||||||
className="debug-note"
|
|
||||||
value={note}
|
|
||||||
onChange={(e) => setNote(e.target.value)}
|
|
||||||
placeholder="What went wrong? (saved in the report)"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
className="debug-report-btn"
|
|
||||||
href={debugReportURL(session, 'json', note)}
|
|
||||||
onClick={() => setReportMsg('downloaded — replay it with rep.ReplayBattle(0)')}
|
|
||||||
download
|
|
||||||
>
|
|
||||||
⬇ Download JSON
|
|
||||||
</a>
|
|
||||||
<button className="debug-report-btn" onClick={copyReport}>
|
|
||||||
📋 Copy as text
|
|
||||||
</button>
|
|
||||||
{reportMsg && <div className="muted">{reportMsg}</div>}
|
|
||||||
</div>
|
|
||||||
<div className="debug-head">
|
<div className="debug-head">
|
||||||
Buy any card
|
Buy any card
|
||||||
{!canGrant && <span className="muted"> · only in the shop</span>}
|
{!canGrant && <span className="muted"> · only in the shop</span>}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ArrangePhase } from './ArrangePhase'
|
|||||||
import { BattlePhase } from './BattlePhase'
|
import { BattlePhase } from './BattlePhase'
|
||||||
import { GameOver } from './GameOver'
|
import { GameOver } from './GameOver'
|
||||||
import { DebugPanel } from './DebugPanel'
|
import { DebugPanel } from './DebugPanel'
|
||||||
|
import { BugReport } from './BugReport'
|
||||||
import { EventLog, battleLogLines } from './EventLog'
|
import { EventLog, battleLogLines } from './EventLog'
|
||||||
|
|
||||||
// Table connects to the game and routes to the right phase screen.
|
// Table connects to the game and routes to the right phase screen.
|
||||||
@@ -61,9 +62,10 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
|||||||
// this open flag is simply ignored.
|
// this open flag is simply ignored.
|
||||||
const [logOpen, setLogOpen] = useState(false)
|
const [logOpen, setLogOpen] = useState(false)
|
||||||
|
|
||||||
// The room code and Leave button live behind a "⋯" menu in the topbar rail,
|
// The room code, the bug reporter and Leave live behind a "⋯" menu in the
|
||||||
// keeping the rail short enough for one row on a phone.
|
// topbar rail, keeping the rail short enough for one row on a phone.
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
|
const [bugOpen, setBugOpen] = useState(false)
|
||||||
|
|
||||||
// The battle outcome is known before the replay plays out, so we hold its
|
// The battle outcome is known before the replay plays out, so we hold its
|
||||||
// "result" log entry back: it's dropped from the persistent list during the
|
// "result" log entry back: it's dropped from the persistent list during the
|
||||||
@@ -257,6 +259,16 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
|||||||
{view.code}
|
{view.code}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => {
|
||||||
|
setMenuOpen(false)
|
||||||
|
setBugOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🐛 Report a bug
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-ghost btn-sm"
|
className="btn btn-ghost btn-sm"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
@@ -323,14 +335,11 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
|||||||
)}
|
)}
|
||||||
{error && <div className="toast">{error}</div>}
|
{error && <div className="toast">{error}</div>}
|
||||||
{view.debug && (
|
{view.debug && (
|
||||||
<DebugPanel
|
<DebugPanel canGrant={view.phase === 'shop'} packs={view.packs} send={send} />
|
||||||
canGrant={view.phase === 'shop'}
|
|
||||||
packs={view.packs}
|
|
||||||
session={session}
|
|
||||||
send={send}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{bugOpen && <BugReport session={session} onClose={() => setBugOpen(false)} />}
|
||||||
|
|
||||||
{deckPeek &&
|
{deckPeek &&
|
||||||
view.phase === 'shop' &&
|
view.phase === 'shop' &&
|
||||||
(() => {
|
(() => {
|
||||||
|
|||||||
+35
-32
@@ -3536,47 +3536,50 @@ h3 {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The report buttons sit above the card list, small and out of the way — they
|
/* --- bug report dialog (always available, not a DEBUG affordance) --- */
|
||||||
are used once a session, when something has already gone wrong. */
|
|
||||||
.debug-report {
|
.bug-modal {
|
||||||
display: flex;
|
width: min(460px, 92vw);
|
||||||
flex-wrap: wrap;
|
gap: 14px;
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.debug-report-btn {
|
.bug-title {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
font-family: var(--font-display);
|
||||||
color: var(--cream);
|
font-size: 1.3rem;
|
||||||
border: 1px solid var(--cocoa);
|
margin: 0;
|
||||||
border-radius: 6px;
|
|
||||||
padding: 5px 8px;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.debug-report-btn:hover {
|
.bug-blurb {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.debug-report .muted {
|
.bug-note {
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug-note {
|
|
||||||
flex-basis: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
color: var(--cream);
|
color: var(--cream);
|
||||||
border: 1px solid var(--cocoa);
|
border: 1px solid rgba(253, 243, 220, 0.25);
|
||||||
border-radius: 6px;
|
border-radius: 10px;
|
||||||
padding: 5px 8px;
|
padding: 8px 10px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bug-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The download is an <a> so the browser saves the file; make it sit level with
|
||||||
|
the button beside it. */
|
||||||
|
.bug-actions a.btn {
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.debug-tier-label {
|
.debug-tier-label {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/anim.ts","./src/api.ts","./src/main.tsx","./src/petArt.ts","./src/types.ts","./src/useBattleSpeed.ts","./src/useCatalog.ts","./src/useGame.ts","./src/useMediaQuery.ts","./src/vite-env.d.ts","./src/components/ArrangePhase.tsx","./src/components/BattlePhase.tsx","./src/components/CardView.tsx","./src/components/DebugPanel.tsx","./src/components/DiceRoll.tsx","./src/components/EventLog.tsx","./src/components/GameOver.tsx","./src/components/Home.tsx","./src/components/Lobby.tsx","./src/components/ShopPhase.tsx","./src/components/Table.tsx"],"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/anim.ts","./src/api.ts","./src/main.tsx","./src/petArt.ts","./src/types.ts","./src/useBattleSpeed.ts","./src/useCatalog.ts","./src/useGame.ts","./src/useMediaQuery.ts","./src/vite-env.d.ts","./src/components/ArrangePhase.tsx","./src/components/BattlePhase.tsx","./src/components/BugReport.tsx","./src/components/CardView.tsx","./src/components/DebugPanel.tsx","./src/components/DiceRoll.tsx","./src/components/EventLog.tsx","./src/components/GameOver.tsx","./src/components/Home.tsx","./src/components/Lobby.tsx","./src/components/ShopPhase.tsx","./src/components/Table.tsx"],"version":"5.9.3"}
|
||||||
Reference in New Issue
Block a user