refactor: remove dead code across server, templ pages, and client modules

Deadcode-verified removals (RTA reachability from main, cross-checked
with -test and textual xrefs):

Go server:
- whole files: middleware/transaction.go, middleware/error_handler.go,
  middleware/password_validator.go (live registration machinery kept),
  handlers/sync.go, sync/offline.go
- unrouted endpoints: auth UpdateUsername/UpdateEmail, timezone
  settings handler, OPDS RegisterOPDS, Kobo LibrarySync handler,
  ReaderHandler.ShowReader, JSON-data twins on collections/library
- duplicate book-matching handlers (book_matching.go Handler twins,
  matching.go MatchingHandler twins), superseded helpers
  (router.parseUUID, handlers.ValidateDeviceToken, normalizePath,
  userExists, generateLockID)

Templ/client:
- never-routed pages: api_explorer.templ, queue.templ and their
  client modules api-explorer.ts, api-explorer-docs.ts, queue.ts
- fully orphaned wrapper modules events.ts, dom.ts
- broken clearSyncQueue (called a nonexistent endpoint) and the
  hidden, never-populated sync-queue section on the devices page
- 166+ unused exports demoted to private (knip-verified); ~85 truly
  dead declarations removed via tsc noUnusedLocals fixpoint
- stale 'interactive API Explorer' claims dropped from docs

The docs drift-gate package (internal/docs), templ-bound modules
(dashboard, stats, header, index, login, profile*, bookshelf,
collections, toast-error), sqlc-generated code, and web/static are
intentionally untouched.
This commit is contained in:
John O'Keefe committed 2026-10-25 11:06:08 -04:00
1 parent 8733e83f1f
commit 8631aa00bf
107 files changed
+689 -3443

No files matched your search

-8
View File
@@ -526,14 +526,6 @@ document.querySelectorAll<HTMLElement>("[data-merge-source]").forEach((el) => {
);
});
export {
checkActiveScans,
hideScanProgress,
loadWatchStatus,
scanAllLibraries,
stopScanStatusPolling,
};
// Library admin page helpers (rendering-ownership: these orchestrate htmx
// swaps and modal visibility on server-rendered DOM; they render nothing).
function toggleLibraryPanel(btn: HTMLElement): void {
-1
View File
@@ -256,5 +256,4 @@ export {
triggerDownload,
bannerBookTitle,
type AnnotationRow,
type AnnotationSource,
};
-16
View File
@@ -90,18 +90,6 @@ interface ReadingProgress {
percentage?: number;
last_read_at: string;
}
async function getReaderMetadata(mediaItemId: string): Promise<ReaderMetadata> {
const response = await apiGet(`/media-items/${mediaItemId}`);
return handleResponse<ReaderMetadata>(response);
}
async function updateReadingProgress(
mediaItemId: string,
progress: ReadingProgress,
): Promise<void> {
const response = await apiPut(`/media-items/${mediaItemId}/progress`, progress);
await handleVoidResponse(response);
}
async function getReadingProgress(mediaItemId: string): Promise<ReadingProgress | null> {
const response = await apiGet(`/media-items/${mediaItemId}/progress`);
@@ -117,14 +105,10 @@ export {
apiPost,
apiPut,
apiDelete,
apiPatch,
handleResponse,
handleVoidResponse,
handleError,
getReaderMetadata,
getReadingProgress, // ADD THIS
updateReadingProgress,
ReadingProgress,
};
Alpine.data("api", () => ({
+1 -1
View File
@@ -32,7 +32,7 @@ export type KeyAction =
| "media-next"
| "media-prev";
export interface KeySpec {
interface KeySpec {
// e.key value to match. Single characters are matched
// case-insensitively by resolveShortcut; multi-character key names
// ("Enter", "ArrowLeft", "MediaPlayPause") must match verbatim.
+1 -1
View File
@@ -4,7 +4,7 @@
// (the Android counterpart's persisted sleep setting is exactly the
// mistake plan §7 removes).
export type SleepMode = "off" | "minutes" | "end-of-chapter" | "fading";
type SleepMode = "off" | "minutes" | "end-of-chapter" | "fading";
export interface SleepState {
mode: SleepMode;
+4 -12
View File
@@ -60,12 +60,12 @@ type AudioChapter = { index: number; title: string; start: number; end: number }
// layouts) carry several; single-file books carry exactly one spanning
// the whole file. start/end are GLOBAL seconds — the same coordinate
// system as chapters, bookmarks and audio_offset_seconds.
export type AudioTrack = { url: string; start: number; end: number; title?: string };
type AudioTrack = { url: string; start: number; end: number; title?: string };
// trackIndexFor finds the track holding a global position: the last one
// whose start is at or before it (zero-duration/unknown-end tracks keep
// working because only start matters for membership).
export function trackIndexFor(tracks: AudioTrack[], globalSeconds: number): number {
function trackIndexFor(tracks: AudioTrack[], globalSeconds: number): number {
for (let i = tracks.length - 1; i > 0; i -= 1) {
if (globalSeconds >= tracks[i].start) return i;
}
@@ -73,7 +73,7 @@ export function trackIndexFor(tracks: AudioTrack[], globalSeconds: number): numb
}
// localOffsetFor converts a global position to seconds within track i.
export function localOffsetFor(tracks: AudioTrack[], index: number, globalSeconds: number): number {
function localOffsetFor(tracks: AudioTrack[], index: number, globalSeconds: number): number {
const track = tracks[index];
if (!track) return Math.max(0, globalSeconds);
return Math.max(0, globalSeconds - track.start);
@@ -81,11 +81,7 @@ export function localOffsetFor(tracks: AudioTrack[], index: number, globalSecond
// globalSecondsFor converts (track i, local seconds) back to the global
// position — the inverse of localOffsetFor.
export function globalSecondsFor(
tracks: AudioTrack[],
index: number,
localSeconds: number,
): number {
function globalSecondsFor(tracks: AudioTrack[], index: number, localSeconds: number): number {
const track = tracks[index];
return (track ? track.start : 0) + Math.max(0, localSeconds);
}
@@ -1434,8 +1430,4 @@ Alpine.data("audioReader", () => ({
this.pushProgress();
}, PROGRESS_INTERVAL_MS);
},
fmtTime,
}));
export { fmtTime };
-9
View File
@@ -114,15 +114,6 @@ function collectFormData(editorTags: string[]): Record<string, unknown> {
return data;
}
export {
showReaderPlaceholder,
showMetadataEditor,
showProgressSyncModal,
showNotesModal,
hideProgressSyncModal,
hideNotesModal,
};
interface MetadataEditorState {
openSections: Record<string, boolean>;
hasExistingCover: boolean;
-16
View File
@@ -375,22 +375,6 @@ function logout(): void {
// Exports
// ============================================================
export {
backToCollection,
clearForm,
deleteRule,
getFieldLabel,
getOperatorLabel,
handleCreateRule,
initCollectionRules,
loadRules,
logout,
renderRules,
setupEventDelegation,
testRule,
toggleRule,
};
Alpine.data("collectionRules", () => ({
backToCollection,
clearForm,
-16
View File
@@ -216,22 +216,6 @@ function handleResolveSubmit(event: Event): void {
hideResolveModal();
}
export {
bulkDismiss,
bulkResolve,
dismissAllResolved,
handleResolveSubmit,
hideResolveModal,
refreshConflicts,
resolveConflict,
showResolveModal,
filterConflicts,
toggleAllConflicts,
updateBulkActions,
resolveSelected,
dismissSelected,
};
Alpine.data("conflicts", () => ({
init() {
filterConflicts();
-49
View File
@@ -533,29 +533,6 @@ async function rejectDevice(registrationId: string): Promise<void> {
}
}
async function clearSyncQueue(): Promise<void> {
if (!confirm("Are you sure you want to clear all sync queue items?")) return;
const token = getToken();
if (!token) return;
try {
const response = await fetch("/api/queue/clear", {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
showToast("Sync queue cleared", "success");
} else {
showToast("Failed to clear queue", "error");
}
} catch (error) {
console.error("Failed to clear queue", error);
showToast("Failed to clear queue", "error");
}
}
function setupEventDelegation(): void {
document.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
@@ -578,34 +555,8 @@ function setupEventDelegation(): void {
});
}
export {
approveDevice,
clearSyncQueue,
copyToClipboard,
deleteMapping,
editMapping,
getDeviceIcon,
handleAddDevice,
handleRevokeDevice,
handleSaveDeviceSettings,
handleSaveMapping,
hideAddDeviceModal,
hideAddMappingModal,
hideDeviceSettingsModal,
hideShelfMappingsModal,
loadCollections,
loadShelfMappings,
rejectDevice,
setupEventDelegation,
showAddDeviceModal,
showAddMappingModal,
showDeviceSettings,
showShelfMappings,
};
Alpine.data("devices", () => ({
approveDevice,
clearSyncQueue,
copyToClipboard,
deleteMapping,
editMapping,
+7 -7
View File
@@ -16,7 +16,7 @@ import { Alpine } from "./alpine";
// ─── operator + field metadata (client mirror of
// internal/services/filter_registry.go — labels drive the menus) ────
export interface OpMeta {
interface OpMeta {
key: string;
label: string;
negated: boolean;
@@ -34,7 +34,7 @@ const op = (key: string, label: string, negated: boolean, arity: OpMeta["arity"]
// label cannot drift apart again (a mis-keyed entry threw every
// non-self-named operator out of the allow-list: "doesn't contain" was
// being sent literally, and the server rightly refused it).
export const OPS: Record<string, OpMeta> = Object.fromEntries(
const OPS: Record<string, OpMeta> = Object.fromEntries(
[
op("contains", "contains", false, "many"),
op("not_contains", "doesn't contain", true, "many"),
@@ -57,7 +57,7 @@ export const OPS: Record<string, OpMeta> = Object.fromEntries(
].map((m) => [m.key, m]),
);
export interface FieldMeta {
interface FieldMeta {
key: string;
label: string;
group: string;
@@ -86,7 +86,7 @@ const TEXT_OPS = ["contains", "not_contains", "is", "is_not", "empty"];
const NUM_OPS = ["between", "at_least", "at_most", "empty"];
const DATE_OPS = ["within_days", "between", "empty"];
export const FIELD_GROUPS: { group: string; fields: FieldMeta[] }[] = [
const FIELD_GROUPS: { group: string; fields: FieldMeta[] }[] = [
{
group: "People",
fields: [
@@ -335,7 +335,7 @@ export const isRule = (n: FilterRuleState | FilterGroupState): n is FilterRuleSt
// flatParamNames are the wire params the flat deep-link contract pins
// (plus the two flat extras the builder owns).
export const FLAT_WIRE_NAMES = [
const FLAT_WIRE_NAMES = [
...Object.values(FLAT_PARAMS),
"explicit_filter",
"abridged_filter",
@@ -432,7 +432,7 @@ export function pruneIncomplete(
// ─── row projection (flat rendering of the nested tree) ─────────────
export interface BuilderRow {
interface BuilderRow {
key: string;
path: number[];
type: "rule" | "groupOpen" | "groupClose";
@@ -528,7 +528,7 @@ interface Suggestion {
display?: string;
}
export function filterBuilderData() {
function filterBuilderData() {
return {
// Rule tree (source of truth; hidden wire inputs are synced after
// every mutation).
+1 -5
View File
@@ -1,11 +1,7 @@
import { Alpine } from "./alpine";
import { ALL_LIBRARIES, getSelectedLibrary, setSelectedLibrary } from "./storage";
import { ALL_LIBRARIES, setSelectedLibrary } from "./storage";
import { showToast } from "./toast";
export function getCurrentLibraryId(): string {
return getSelectedLibrary();
}
export async function switchWithTransition(
containerId: string,
fetchFn: () => Promise<void>,
-9
View File
@@ -186,15 +186,6 @@ function hideMatchModal(): void {
}
}
export {
autoLinkBooks,
getSuggestions,
hideMatchModal,
linkBook,
loadUnlinkedBooks,
showSuggestionsModal,
};
Alpine.data("linking", () => ({
autoLinkBooks,
getSuggestions,
-5
View File
@@ -4,8 +4,6 @@ import { Alpine } from "./alpine";
import "./admin";
import "./audio";
import "./api";
import "./api-explorer";
import "./api-explorer-docs";
import "./bookPicker";
import "./bookshelf";
import "./filter_builder";
@@ -17,8 +15,6 @@ import "./custom-section-builder";
import "./dashboard";
import "./device-management";
import "./docs";
import "./dom";
import "./events";
import "./header";
import "./index";
import "./library-switcher";
@@ -27,7 +23,6 @@ import "./login";
import "./password_validation";
import "./profile";
import "./profile-modal";
import "./queue";
import "./register";
import "./search";
import "./series";
+2 -2
View File
@@ -6,7 +6,7 @@
// are positional. Pages are therefore joined gap-aware, with a char→item
// map so each match can be covered by the rects of the items it spans.
export interface PdfSearchItem {
interface PdfSearchItem {
/** page-fraction rect of this text item */
x: number;
y: number;
@@ -41,7 +41,7 @@ interface RawItem {
}
/** Join one page's text items into a searchable string + item map. */
export function buildPageText(
function buildPageText(
rawItems: RawItem[],
viewportWidth: number,
viewportHeight: number,
+1 -21
View File
@@ -43,27 +43,7 @@ export async function saveSettings(settings: Partial<ReaderSettings>): Promise<v
localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(updated));
}
}
export async function syncSettings(): Promise<void> {
const local = localStorage.getItem(LOCALSTORAGE_KEY);
if (!local) return;
const settings = JSON.parse(local);
const token = getToken();
if (token) {
try {
await fetch("/readers/settings", {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(settings),
});
} catch (error) {
console.error("Failed to sync settings:", error);
}
}
}
export function getDefaultSettings(): ReaderSettings {
function getDefaultSettings(): ReaderSettings {
return {
chrome_behavior: "auto-hide",
progress_mode: "pages",
-2
View File
@@ -10,8 +10,6 @@ function initRegisterTheme(): void {
}
}
export { initRegisterTheme };
Alpine.data("register", () => ({
init() {
initRegisterTheme();
-7
View File
@@ -288,11 +288,8 @@ Alpine.data("theme", () => ({
changeTheme,
changeWoodPaneling,
initializeTheme,
loadTheme,
loadUserTheme,
loadWoodPaneling,
revealCurrentTheme,
updateThemeIndicators,
updateWoodPanelingIndicators,
}));
@@ -301,11 +298,7 @@ export {
changeTheme,
changeWoodPaneling,
initializeTheme,
loadTheme,
loadUserTheme,
loadWoodPaneling,
revealCurrentTheme,
updateThemeIndicators,
updateWoodPanelingIndicators,
};
export type { ThemeType };
-15
View File
@@ -387,21 +387,6 @@ function setupEventDelegation(): void {
});
}
export {
bulkAutoLink,
bulkGetSuggestions,
confirmManualLink,
displaySuggestions,
hideManualLinkModal,
searchBooksForLink,
searchMatches,
selectBookForLink,
setupEventDelegation,
showBulkManualLink,
showManualLinkModal,
toggleAllUnlinked,
};
Alpine.data("unlinkedBooks", () => ({
bulkAutoLink,
bulkGetSuggestions,
+1 -1
View File
@@ -120,4 +120,4 @@ function createWebSocket(config: {
return ws;
}
export { addListener, createWebSocket, disconnectWebSocket, removeListener };
export { addListener, createWebSocket, removeListener };