Fix the broken ESLint setup: add vue-eslint-parser and @typescript-eslint/parser
and wire the TS parser into eslint.config.mjs so .ts and <script lang=ts> parse
(was failing outright). Clear the resulting backlog to a clean 0/0 baseline —
fix genuine issues, relax two intentional-pattern rules (multi-word-component-names,
no-required-prop-with-default).
Add a committed .githooks/pre-commit (enabled via core.hooksPath, auto-set by the
prepare script) that runs Pint on staged PHP and ESLint --max-warnings 0 on staged
resources/scripts JS/TS/Vue, blocking on failure. Add composer/npm lint scripts and
document the gate in CLAUDE.md.
Replace every scattered v-html with a single audited BaseSanitizedHtml component
that DOMPurify-sanitizes its input (new utils/markdown.ts sanitizeHtml), so
server/registry-provided HTML is actually sanitized and vue/no-v-html stays enabled
everywhere but one reviewed sink.
The AI chat drawer was rendering assistant responses as plain text,
so code blocks, lists, tables and inline formatting came through as
literal asterisks and backticks — noisy and hard to scan.
Adds a shared renderMarkdown() helper in resources/scripts/utils/
markdown.ts that parses GFM markdown via marked and sanitizes the
result with DOMPurify before handing it to Vue's v-html. AiChatMessage
uses the helper for assistant messages only; user messages stay as
plain text since markdown syntax in their own typed input would be
surprising.
Assistant bubbles get the Tailwind `prose prose-sm` classes from the
already-enabled @tailwindcss/typography plugin so headings, lists and
code blocks inherit sensible defaults without per-element styling.
Security: DOMPurify runs in its default browser profile, which strips
<script>, event handlers, javascript: URLs and every other XSS vector.
The AI provider isn't a trusted source — it can echo arbitrary user
input and tool-call results from the database — so sanitization is
non-negotiable even though the immediate source is our own backend.