diff --git a/.eslintrc.json b/.eslintrc.json index 3f36631..bf1e71d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,6 +20,6 @@ "no-useless-escape": "warn", "no-regex-spaces": "warn", "semi": ["error", "always"], - "eqeqeq": "warn" + "eqeqeq": ["warn", "smart"] } } diff --git a/README.md b/README.md index 7f0cabd..0177c49 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,7 @@ Content-Type: application/json - `migrations/000_baseline.sql` is the full schema baseline for the whole database. It is written to be safe to re-run (idempotent) and is the source of truth for a fresh install. - `php migrations/migrate.php` applies any pending migration files in `migrations/` in order, tracking applied files in the `migrations` table. Use `--status` to list state and `--dry-run` to preview without executing. +- Numbered migrations on top of the baseline (all idempotent, safe to re-run): `001_widen_bulk_operations_status.sql`, `002_fix_collation_consistency.sql`, `003_fk_on_delete_set_null.sql`, `004_fix_ticket_watchers_type.sql`. A fresh install via `000_baseline.sql` already includes all of these; they only matter for upgrading an existing database. ### API Endpoints @@ -348,7 +349,9 @@ tinker_tickets/ │ └── images/ │ └── favicon.png ├── config/ -│ └── config.php # Config + .env loading +│ ├── config.php # Config + .env loading +│ └── requirements.php # PHP version/extension requirements (single source of +│ # truth for scripts/check_requirements.php + api/health.php) ├── controllers/ │ ├── CommentController.php # Comment create/edit/delete + notifications │ ├── DashboardController.php # Dashboard with stats + filters @@ -389,6 +392,10 @@ tinker_tickets/ │ └── WorkflowModel.php # Status transition workflows ├── migrations/ │ ├── 000_baseline.sql # Full schema baseline (safe to re-run) +│ ├── 001_widen_bulk_operations_status.sql # Upgrade-only (already in baseline for fresh installs) +│ ├── 002_fix_collation_consistency.sql # Upgrade-only (already in baseline for fresh installs) +│ ├── 003_fk_on_delete_set_null.sql # Upgrade-only (already in baseline for fresh installs) +│ ├── 004_fix_ticket_watchers_type.sql # Upgrade-only (already in baseline for fresh installs) │ └── migrate.php # CLI migration runner (tracks applied migrations) ├── scripts/ │ ├── check_requirements.php # Verify PHP extensions/config prerequisites @@ -406,6 +413,9 @@ tinker_tickets/ │ │ └── WorkflowDesignerView.php # Workflow transition designer │ ├── CreateTicketView.php # Ticket creation with visibility │ ├── DashboardView.php # Dashboard with kanban + sidebar + charts +│ ├── error_403.php # Access-denied error page +│ ├── error_404.php # Not-found error page +│ ├── error_500.php # Fatal-error page (self-contained, no app-state deps) │ ├── layout_footer.php # Shared footer (notification polling, boot sequence) │ ├── layout_header.php # Shared header (nav, command palette, theme toggle) │ └── TicketView.php # Ticket view with timeline, SLA, watcher avatars diff --git a/assets/js/base.js b/assets/js/base.js index 397c58f..329f022 100644 --- a/assets/js/base.js +++ b/assets/js/base.js @@ -41,6 +41,16 @@ * 32. Drag & Drop Upload * 33. Intersection Observer * 34. Full Initialisation + * + * NOTE ON EMPTY CATCH BLOCKS: throughout this file, `try { ... } catch (_) {}` + * around localStorage/sessionStorage access (persisted tab/theme/column- + * visibility state, recent command-palette entries, etc.) and the terminal + * beep's AudioContext calls is intentional, not an oversight — these are + * best-effort UX affordances that must silently no-op rather than break the + * surrounding feature if storage is disabled/full (private browsing, quota) + * or audio is blocked (autoplay policy). Swallowing errors from arbitrary + * caller-supplied callbacks (e.g. viewport-change listeners) is handled + * separately with real logging, since those can hide genuine bugs. */ (function (global) { @@ -1398,7 +1408,7 @@ _vpCurrent = bp; if (bp !== prev) { const evt = { bp, w, h, prev }; - _vpListeners.forEach(cb => { try { cb(evt); } catch (_) {} }); + _vpListeners.forEach(cb => { try { cb(evt); } catch (e) { console.error('[lt.viewport] listener threw:', e); } }); bus.emit('viewport:change', evt); } } @@ -2920,73 +2930,6 @@ }, }; - /* ================================================================ - MODULE 54 — MARKDOWN RENDERER - lt.markdown.render(mdString) → HTML string (sanitized) - lt.markdown.init(selector) → renders all matching el's .textContent - Uses a built-in micro-renderer (no deps) for common syntax. - For full GFM, swap in marked.js: window.marked && marked.parse() - ================================================================ */ - const markdown = { - render(md) { - // Always use the built-in XSS-safe micro-renderer. Do NOT delegate to - // window.marked / window.markdownit: their raw HTML output is not sanitized - // here, so delegating would enable stored XSS if such a lib were ever loaded. - // Micro-renderer: covers headings, bold, italic, code, links, lists, blockquote, hr - let html = escHtml(md) - // Fenced code blocks - .replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => `
${code.trim()}`)
- // Inline code
- .replace(/`([^`]+)`/g, '$1')
- // Headings
- .replace(/^######\s(.+)$/gm, '$1') - // Horizontal rule - .replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '
')
- .replace(/\n/g, '
');
- return `
${html}
` - .replace(/(<(?:pre|ul|ol|h[1-6]|blockquote|hr)[^>]*>)/g, '$1') - .replace(/(<\/(?:pre|ul|ol|h[1-6]|blockquote|hr)>)<\/p>/g, '$1'); - }, - - init(selector) { - document.querySelectorAll(selector).forEach(el => { - const raw = el.getAttribute('data-markdown') || el.textContent; - el.innerHTML = markdown.render(raw); - el.classList.add('lt-markdown'); - }); - }, - }; - /* ================================================================ MODULE 55 — PAGINATION lt.pagination.init(navEl, opts) @@ -3149,7 +3092,6 @@ timer, lightbox, auth, - markdown, ticketStatus, pagination, sidebarSubmenus: { init: initSidebarSubmenus }, diff --git a/assets/js/markdown.js b/assets/js/markdown.js index b9653ce..172ff9a 100644 --- a/assets/js/markdown.js +++ b/assets/js/markdown.js @@ -506,6 +506,25 @@ function toolbarHeading(textareaId) { textarea.dispatchEvent(new Event('input', { bubbles: true })); } +function toolbarTable(textareaId) { + const textarea = document.getElementById(textareaId); + if (!textarea) return; + + const start = textarea.selectionStart; + const text = textarea.value; + + // Insert on its own line(s), matching the blank-line-before convention + // toolbarList/toolbarHeading rely on the surrounding text for — a table + // needs a full line to itself both before and after the separator row. + const needsLeadingNewline = start > 0 && text[start - 1] !== '\n'; + const template = (needsLeadingNewline ? '\n' : '') + + '| Header 1 | Header 2 |\n' + + '| --- | --- |\n' + + '| Cell 1 | Cell 2 |\n'; + + insertMarkdownText(textareaId, template); +} + function toolbarQuote(textareaId) { const textarea = document.getElementById(textareaId); if (!textarea) return; @@ -544,6 +563,7 @@ function createEditorToolbar(textareaId, containerId) { + `; @@ -563,6 +583,7 @@ function createEditorToolbar(textareaId, containerId) { case 'heading': toolbarHeading(targetId); break; case 'list': toolbarList(targetId); break; case 'quote': toolbarQuote(targetId); break; + case 'table': toolbarTable(targetId); break; case 'link': toolbarLink(targetId); break; } }); @@ -578,6 +599,7 @@ window.toolbarLink = toolbarLink; window.toolbarList = toolbarList; window.toolbarHeading = toolbarHeading; window.toolbarQuote = toolbarQuote; +window.toolbarTable = toolbarTable; window.createEditorToolbar = createEditorToolbar; window.insertMarkdownFormat = insertMarkdownFormat; window.insertMarkdownText = insertMarkdownText;