Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions apps/web/src/features/reader/ReaderPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useParams, useNavigate } from 'react-router-dom';
import { createSpanId, createTraceId } from '@do-epub-studio/shared';
import { useTranslation } from '../../hooks/useTranslation';
import { apiRequest } from '../../lib/api';
import { logClientEvent } from '../../lib/client-logger';
import { fetchHighlights, fetchComments } from '../../lib/api/annotations';
import { useAuthStore, useReaderStore, usePreferencesStore } from '../../stores';
import { setupOnlineListener } from '../../lib/offline';
import { setupZombieDetection } from '../../lib/offline/permissions';
import { AnnotationToolbar, extractSelectionData, CommentsPanel } from './components/annotations';
import { useReaderUI, useReaderEpub, useAnnotationHandlers, useBookmarkHandlers, useExportNotes } from './hooks';
import {
useReaderUI,
useReaderEpub,
useAnnotationHandlers,
useBookmarkHandlers,
useExportNotes,
} from './hooks';
import { AnimatePresence } from 'framer-motion';
import {
ReaderToolbar,
Expand Down Expand Up @@ -126,7 +134,15 @@ export function ReaderPage() {
setHighlights(hl);
setComments(cm);
} catch (err) {
console.warn('Failed to fetch annotations', err);
const error = err instanceof Error ? err : new Error(String(err));
logClientEvent({
level: 'warn',
event: 'reader.annotations_fetch_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: { name: error.name, message: error.message, stack: error.stack },
metadata: { bookId },
});
}
};
void load();
Expand Down Expand Up @@ -199,7 +215,14 @@ export function ReaderPage() {
try {
await apiRequest('/api/access/logout', { method: 'POST', token: sessionToken ?? undefined });
} catch (err) {
console.error('Logout failed', err);
const error = err instanceof Error ? err : new Error(String(err));
logClientEvent({
level: 'error',
event: 'reader.logout_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: { name: error.name, message: error.message, stack: error.stack },
});
} finally {
logout();
void navigate('/login');
Expand Down
72 changes: 60 additions & 12 deletions apps/web/src/features/reader/hooks/useReaderEpub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import ePub from '@intity/epub-js';
import type { Book, Rendition, NavItem, Contents } from '@intity/epub-js';
import type { PageDirection, WritingMode } from '../../../stores';
import { parseAccessibilityFromOpf, parseFixedLayoutFromOpf } from '@do-epub-studio/reader-core';
import { createSpanId, createTraceId } from '@do-epub-studio/shared';
import { logClientEvent } from '../../../lib/client-logger';
import {
useAuthStore,
useReaderStore,
Expand Down Expand Up @@ -159,11 +161,12 @@ export function useReaderEpub(
setToc(tocItems);
tocRef.current = tocItems;

const bookDirection: PageDirection = book.packaging?.direction === 'rtl'
? 'rtl'
: book.packaging?.direction === 'ltr'
? 'ltr'
: 'default';
const bookDirection: PageDirection =
book.packaging?.direction === 'rtl'
? 'rtl'
: book.packaging?.direction === 'ltr'
? 'ltr'
: 'default';
directionRef.current = bookDirection;
setBookDirection(bookDirection);

Expand Down Expand Up @@ -270,7 +273,11 @@ export function useReaderEpub(
const adapter = createEpubAnnotationAdapter(rendition);
adapterRef.current = adapter;

applyDirectionAndWritingMode(rendition, readerDirection !== 'default' ? readerDirection : bookDirection, readerWritingMode);
applyDirectionAndWritingMode(
rendition,
readerDirection !== 'default' ? readerDirection : bookDirection,
readerWritingMode,
);
await rendition.display();
if (!active) return;

Expand Down Expand Up @@ -305,12 +312,32 @@ export function useReaderEpub(

if (cfi) await rendition.display(cfi);
} catch (e) {
console.warn('Failed to load progress from API', e);
const apiError = e instanceof Error ? e : new Error(String(e));
logClientEvent({
level: 'warn',
event: 'reader.progress_load_api_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: { name: apiError.name, message: apiError.message, stack: apiError.stack },
metadata: { bookId },
});
try {
const cached = await getProgress(bookId);
if (cached?.cfi) await rendition.display(cached.cfi);
} catch (dbErr) {
console.warn('Failed to load progress from cache', dbErr);
const cacheError = dbErr instanceof Error ? dbErr : new Error(String(dbErr));
logClientEvent({
level: 'warn',
event: 'reader.progress_load_cache_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: {
name: cacheError.name,
message: cacheError.message,
stack: cacheError.stack,
},
metadata: { bookId },
});
}
}
}
Expand Down Expand Up @@ -360,7 +387,19 @@ export function useReaderEpub(
body: JSON.stringify({ locator: { cfi }, progressPercent, mutationId }),
});
} catch (e) {
console.warn('Failed to save progress online, queuing offline', e);
const saveError = e instanceof Error ? e : new Error(String(e));
logClientEvent({
level: 'warn',
event: 'reader.progress_save_online_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: {
name: saveError.name,
message: saveError.message,
stack: saveError.stack,
},
metadata: { bookId },
});
await queueOffline();
}
} else {
Expand All @@ -379,7 +418,15 @@ export function useReaderEpub(
);
});
} catch (err) {
console.error('EPUB init error:', err);
const error = err instanceof Error ? err : new Error(String(err));
logClientEvent({
level: 'error',
event: 'reader.epub_init_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: { name: error.name, message: error.message, stack: error.stack },
metadata: { bookId },
});
setError(t('reader.loadError'));
}
};
Expand Down Expand Up @@ -437,8 +484,9 @@ export function useReaderEpub(
const rendition = renditionRef.current;
if (!rendition) return;

const isRtl = directionRef.current === 'rtl'
|| (directionRef.current === 'default' && document.documentElement.dir === 'rtl');
const isRtl =
directionRef.current === 'rtl' ||
(directionRef.current === 'default' && document.documentElement.dir === 'rtl');
const nextPage = isRtl ? 'ArrowLeft' : 'ArrowRight';
const prevPage = isRtl ? 'ArrowRight' : 'ArrowLeft';

Expand Down
18 changes: 16 additions & 2 deletions apps/web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,27 @@ if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
};
if (syncReg.sync) {
void syncReg.sync.register('sync-reader-state').catch((err: unknown) => {
console.error('Failed to register background sync:', err);
const error = err instanceof Error ? err : new Error(String(err));
logClientEvent({
level: 'error',
event: 'sw.background_sync_register_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: { name: error.name, message: error.message, stack: error.stack },
});
});
}
}
},
onRegisterError(error) {
console.error('Service worker registration failed:', error);
const err = error instanceof Error ? error : new Error(String(error));
logClientEvent({
level: 'error',
event: 'sw.registration_failed',
traceId: createTraceId(),
spanId: createSpanId(),
error: { name: err.name, message: err.message, stack: err.stack },
});
},
});
}
Expand Down
93 changes: 93 additions & 0 deletions plans/066-goap-comprehensive-analysis-2026-06-05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# GOAP Plan: Comprehensive Codebase Analysis & Modernization

**Date**: 2026-06-05
**Orchestrator**: goap-agent
**Objective**: Analyze the codebase across implementation gaps, features, UI/UX, security,
logging, performance, deployment, and the AGENTS.md coding workflow; then execute the
high-value, evidence-backed changes. Companion policy: ADR-067.

## 1. Analysis

- **Primary Goal**: Identify and close real gaps (not speculative ones) and keep `main` green.
- **Constraints**: Quality gates (`./scripts/quality_gate.sh`), atomic commits, feature
branch + PR, AGENTS.md TIER 1–3 rules.
- **Complexity**: Medium. The codebase is mature (prior plans 020–065) and largely clean.
- **Method**: Evidence-first survey. Every finding below is backed by a concrete observation,
not assumption. Tooling note: `rg` is unavailable in this environment; `grep` was used.

### Baseline (evidence)

| Area | Observation |
|------|-------------|
| Stack currency | React 19, Vite 8, TS 6, Zod 4, Zustand 5, Tailwind 4, Vitest 4, Hono 4, Wrangler 4 — all current. |
| First-party TODO/FIXME | 0 (all matches were under `node_modules`). |
| Tests | 84 test files across apps/packages. |
| Worker observability | Structured JSON logging with traceId/spanId in `apps/worker/src/lib/observability.ts`. |
| Web observability | `apps/web/src/lib/client-logger.ts` exists, used in only ~5 sites. |
| Security | Argon2id, CSP middleware, signed URLs, rate-limit, security-headers all present. |
| Open issues | 1: #439 CI failure on `main`. Open PRs: 0. |

## 2. Findings & Decomposition

### Batch 1 — CI / Deployment (P0, BLOCKING) ✅ executed
- **#439**: `main` CI red. Root cause: `scripts/validate-workflows.sh` line 115 —
`zizmor: Permission denied`. The pip/curl-installed launcher in `~/.local/bin`
intermittently loses its exec bit, failing mid-run on one workflow file.
- **Action**: Guarantee exec bit on resolved `actionlint`/`zizmor` binaries before the
validation loop (`chmod u+x "$(command -v …)"`).
- **Quality Gate**: `bash scripts/validate-workflows.sh` → exit 0 (verified locally).

### Batch 2 — Logging / Observability (P1) ✅ executed
- **Gap**: AGENTS.md TIER 1 requires "emit traceId on every critical UI action," but
raw `console.warn/error` calls in `apps/web/src` critical paths bypassed `logClientEvent`
and carried no traceId.
- **Action (done)**: Converted the critical-path failures to `logClientEvent` with a traceId:
- `main.tsx`: service-worker registration + background-sync registration failures.
- `ReaderPage.tsx`: annotations fetch failure, logout failure.
- `useReaderEpub.ts`: EPUB init failure, progress load (API + cache), progress save.
- **Quality Gate (verified)**: web typecheck ✓, lint ✓, 264 unit tests ✓. The conversions
reuse the already-tested `logClientEvent` contract (covered by api/sync/conflict tests), so
no brittle epub.js/SW mocks were added — avoiding over-engineering per the pragmatism guard.

### Batch 3 — Security (P2, audit-only)
- No defects found in survey. Schedule a `security-code-auditor` pass on auth + signed-URL +
EPUB-parsing paths to confirm; file follow-ups only if real issues surface.

### Batch 4 — Performance (P2, monitor)
- Reader virtualization, turbo input/output tuning, and route-aware bundle budgets already
landed (plans 046/065). No new action; keep budgets enforced in CI.

### Batch 5 — UI/UX (P3, monitor)
- 2026 design system (OKLCH tokens, View Transitions, panel mutual-exclusivity) delivered in
plans 031/032/063. No regression observed. No new action.

### Batch 6 — AGENTS.md Workflow (P3)
- Deprecation warning: `actions/upload-artifact@v4.6.2` is forced onto Node 24 (Node 20
sunset). Non-blocking; Dependabot owns the bump. Track only.

## 3. Strategy

- **Hybrid**: Batch 1 executed immediately (unblocks `main`). Batch 2 next (small, testable).
Batches 3–6 are audit/monitor — documented, not force-changed, to avoid over-engineering.

## 4. Agent Assignment

- goap-agent: orchestration + synthesis
- shell-script-quality: Batch 1
- reader-ui-ux / cloudflare-worker-api: Batch 2
- security-code-auditor: Batch 3 (read-only)

## 5. Execution Plan

1. ✅ ADR-067 (policy) authored.
2. ✅ Batch 1 — fix zizmor/actionlint exec-bit race; verify locally.
3. ✅ Batch 2 — client traceId adoption on critical UI actions.
4. ⏳ Batches 3–6 — scheduled audit/monitor; promote to action only on real findings.
5. Quality gate + atomic commits + PR.

## 6. Synthesis

- [x] CI blocker #439 root-caused and fixed (verified `exit 0`).
- [x] Comprehensive evidence-based gap map recorded.
- [x] Client-side traceId adoption (Batch 2) — typecheck/lint/264 tests green.
- [x] No speculative refactors introduced (pragmatism guard).
46 changes: 46 additions & 0 deletions plans/067-adr-observability-and-ci-resilience.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ADR-067: Observability Adoption & CI Tooling Resilience

**Status**: Accepted
**Date**: 2026-06-05
**Context plan**: 066-goap-comprehensive-analysis-2026-06-05.md
**Supersedes/relates**: ADR-035 (CSP), ADR-034 (ReDoS), TIER 1 "emit traceId" rule

## Context

A comprehensive analysis (plan 066) found the codebase mature and clean, with two real,
evidence-backed gaps rather than broad deficiencies:

1. **CI fragility** — `scripts/validate-workflows.sh` shells out to `actionlint` and `zizmor`
binaries installed into `$HOME/.local/bin`. These launchers can intermittently lose their
exec bit, producing `Permission denied` and a red `main` (issue #439). The failure is
non-deterministic, so a one-off rerun masks it without fixing the cause.

2. **Partial client observability** — Worker requests are fully traced
(`apps/worker/src/lib/observability.ts`), and a client logger exists
(`apps/web/src/lib/client-logger.ts`), but most web error paths still call raw `console.*`
without a traceId, violating the TIER 1 rule "emit traceId on every critical UI action."

## Decision

1. **CI tooling must be defensively executable.** Any script that depends on a downloaded or
pip-installed CLI MUST guarantee the binary's exec bit (`chmod u+x "$(command -v <tool>)"`)
after detection and before use. Transient permission/exec failures in vendored tooling are
treated as bugs to fix at the source, never as "rerun the job."

2. **Critical UI actions must be traced.** Failures on auth, EPUB load, progress sync, and
service-worker registration MUST go through `logClientEvent` with a `traceId`. Raw
`console.*` is permitted only for development-only diagnostics that carry no operational
signal.

3. **No speculative work.** Audit/monitor-tier areas (security, performance, UI/UX) found
clean in plan 066 are NOT refactored preemptively. They are promoted to action only when a
concrete defect is observed, keeping changes minimal and reviewable (pragmatism guard).

## Consequences

- **Positive**: `main` stops flaking on the workflow-validation hook; client errors become
correlatable with server traces; scope stays tight and auditable.
- **Negative / cost**: A small ongoing discipline cost — new CLI-invoking scripts must add the
exec-bit guard, and new critical UI handlers must thread a traceId.
- **Follow-ups**: Schedule a read-only `security-code-auditor` pass; keep bundle budgets and
Lighthouse thresholds enforced; let Dependabot retire the Node 20 `upload-artifact` warning.
Loading
Loading