From e1448d8ea2b275d5d3c52f5d7bd36f13447187d2 Mon Sep 17 00:00:00 2001
From: Jared Vititoe
Date: Fri, 11 Sep 2026 15:08:05 -0400
Subject: [PATCH 1/4] Document intentional empty catches, fix one real gap,
allow == null in eqeqeq (#44)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
ESLint flagged ~20 empty catch blocks and 4 loose-equality comparisons
in assets/js/. Auditing each: all ~19 remaining empty catches are
localStorage/sessionStorage access (persisted tab/theme/column-
visibility state, recent command-palette entries) or the terminal
beep's AudioContext calls — genuinely intentional best-effort UX
affordances that must silently no-op if storage is disabled/full or
audio is blocked, not oversights. One (a viewport-change listener
callback) was a real gap: swallowing an arbitrary caller-supplied
callback's exception could hide a genuine bug, so that one now logs
via console.error instead.
Documented the storage/audio convention once in a file-level comment
rather than repeating the same explanation on ~19 near-identical
one-line try/catches. All 4 flagged loose-equality comparisons turned
out to be `== null`/`!= null` checks — the one loose-equality idiom
that's deliberately safe (catches both null and undefined in one
comparison; ESLint's own eqeqeq rule has a "smart" mode specifically
for this). Converting them to strict equality would have been a
behavior change (no longer catching undefined), not a fix, so switched
.eslintrc.json's eqeqeq rule to "smart" instead — flags every other
loose comparison as before, correctly stops flagging this one safe
idiom.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
---
.eslintrc.json | 2 +-
assets/js/base.js | 12 +++++++++++-
2 files changed, 12 insertions(+), 2 deletions(-)
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/assets/js/base.js b/assets/js/base.js
index 397c58f..9778205 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);
}
}
From 0e163f66073adc53c073627eaae1f46503797e59 Mon Sep 17 00:00:00 2001
From: Jared Vititoe
Date: Fri, 11 Sep 2026 15:10:47 -0400
Subject: [PATCH 2/4] Remove unused lt.markdown module (#43)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
lt.markdown was confirmed dead code app-wide (zero callers outside
base.js itself — markdown.js's parseMarkdown() is what's actually
wired up everywhere). The issue flagged its link handler as lacking a
URL-protocol allowlist unlike markdown.js's equivalent; checking the
current code, that link handler already restricts to http(s)/relative/
hash URLs (blocking javascript:/data: URIs) — the allowlist claim
didn't match what's actually there. Since the module is unused either
way, and its own doc comment invites exactly the kind of future
misuse the issue warned about ("For full GFM, swap in marked.js"),
deleted it outright rather than hardening dead code, removing the
landmine permanently instead of leaving an unused copy that could
still drift out of sync with markdown.js in some other way later.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
---
assets/js/base.js | 68 -----------------------------------------------
1 file changed, 68 deletions(-)
diff --git a/assets/js/base.js b/assets/js/base.js
index 9778205..329f022 100644
--- a/assets/js/base.js
+++ b/assets/js/base.js
@@ -2930,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) => `
(<(?: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)
@@ -3159,7 +3092,6 @@
timer,
lightbox,
auth,
- markdown,
ticketStatus,
pagination,
sidebarSubmenus: { init: initSidebarSubmenus },
From 803c65616b165c3a664196c125347b7b6334078c Mon Sep 17 00:00:00 2001
From: Jared Vititoe
Date: Fri, 11 Sep 2026 15:10:56 -0400
Subject: [PATCH 3/4] Update stale README Project Structure tree and migrations
docs (#45)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Cross-checking README.md against the actual file tree found three gaps:
views/error_403.php and error_404.php (plus error_500.php, added since
the issue was filed) weren't listed under views/; config/requirements.php
wasn't listed under config/ (confirmed it's not a duplicate of
scripts/check_requirements.php — it's the shared data source both that
script and api/health.php read from); and the Database Schema section
only described 000_baseline.sql and migrate.php generically, with no
mention that four numbered migrations now exist on top of the baseline.
Updated the Project Structure tree and the Database Schema/Migrations
prose to list all of these, noting that 000_baseline.sql already
includes every numbered migration's changes for a fresh install (they
only matter when upgrading an existing database).
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
---
README.md | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
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
From 1600412a6db6446a4d4f6ce4ec6a6eb228274f67 Mon Sep 17 00:00:00 2001
From: Jared Vititoe
Date: Fri, 11 Sep 2026 15:11:03 -0400
Subject: [PATCH 4/4] Add a table-insert toolbar button to the markdown editor
(#109)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The markdown toolbar offered bold/italic/code/heading/list/quote/link
but no table option, despite README describing table rendering as a
supported feature — the parser already renders manually-typed table
syntax correctly, this was purely a discoverability gap for a user who
wouldn't otherwise know the exact `| Header | Header |` / `|---|---|`
syntax to type from scratch.
Added toolbarTable(), which inserts a 2-column starter template (with
a leading newline only when needed, matching the table syntax's
requirement of a full line to itself) matching exactly what
parseMarkdownTables()'s detection regex expects, wired into the
toolbar's existing data-toolbar-action dispatch. Verified via jsdom
that the inserted template parses into a real HTML table.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_01Lhz7pGMaoTfL5sdYS5XiKv
---
assets/js/markdown.js | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
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;