Migrates the existing upload/format-select/quality/convert logic from
the old single-file App.jsx into a router shell (/ -> /fr//en/) with a
shared Layout (header, language switcher, dark-mode toggle, footer).
Tool logic itself is unchanged, only restyled into cards.
Part of the Ombrora Convert redesign (Task 2/5).
Foundation sub-project: merged homepage+tool page, FR/EN i18n via URL
prefix, dark mode, SEO basics, and reassurance elements backed by
actual backend guarantees (1h auto-delete, 100MB max, no account).
Also fixes a real bug found via the fb2 API test: file-type sniffs a
real fb2 file's XML declaration as generic "xml", not "fb2" and not
undetected, so it never reached the undetectable-format fallback added
in the previous commit. Added an fb2->xml alias in normalizeFormat,
same pattern as the existing azw3->mobi one.
Verified empirically: fontkit has no default ESM export in this project's
"type": "module" setup (import fontkit from 'fontkit' throws). The working
form is import * as fontkit from 'fontkit'.
Empirically verified library choices (fonteditor-core, opentype.js, fontkit)
against real fonts before committing to the scope, and root-caused a
file-type/dfont MIME collision that would otherwise silently break uploads.
9 tasks covering mime.js, the ico/heic converter modules, a new
iconSize column on ConversionJob, app.js/worker.js wiring, and the
frontend size picker. Every code snippet (icojs encode/decode, the
heic-convert ESM import, prisma db execute/migrate status flags) was
verified against the actual installed/resolved packages rather than
guessed.
Reuses libheif's own LGPL/MIT-licensed with-alpha-512x512.heic sample
under both a .heic and .heif filename, verified to decode correctly
via heic-convert (the library the image format work will use). No
suitably-licensed HEVC/mif1-branded .heif-distinct sample exists that
is compatible with the pinned libheif-js version, and file-type detects
both extensions identically regardless of container brand, so the
duplicate content is sufficient to exercise the heif source-format
code path.
The documented shadow-database-free fallback for generating migration SQL
(--from-migrations ... --to-schema-datamodel ...) was never actually tested
and fails for the same P3014 reason `migrate dev` does, since
--from-migrations also requires --shadow-database-url internally. Verified
against the local dev DB that --from-schema-datasource (live introspection,
no shadow DB) diffed against --to-schema-datamodel (static file read) works
in both the no-op case and a real ADD COLUMN case, and documented that
instead. Also corrected the inaccurate claim that this matched Task 3's
approach (Task 3 used --from-empty) and moved the suggested SQL output path
out of the repo root into the OS temp directory.
Also brings markFailed's errorMessage/errorLog params in line with markDone's
existing ?? null guard, and adds orderBy to findExpiredJobs to match
findPendingJobs, for consistency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CLAUDE.md:
- Add the one-time `prisma migrate resolve --applied 0_init` baseline
step that must run before the first `migrate deploy` against any
environment (o2switch production included) whose conversion_jobs
table predates Prisma. Without it, migrate deploy tries to
CREATE TABLE against a table that already exists and fails, leaving
migration history stuck.
- Document the actual, tested result of `prisma migrate dev` against
the local dev DB: it fails with P3014 because convert_user lacks
CREATE DATABASE/DROP DATABASE privileges needed for the shadow
database. Document the verified fallback (`migrate diff
--from-migrations ... --to-schema-datamodel ...` + manual migration
folder + `migrate resolve --applied`) as the supported way to create
new migrations here.
README.md:
- Local development step 2 referenced the deleted db/schema.sql;
replaced with the real `prisma migrate deploy` invocation, which
creates conversion_jobs fresh on an empty local database. Reordered
so `npm install` runs first, since the migrate command needs
node_modules/@prisma/client.
- Deployment on o2switch: added the same one-time baseline step
(clearly marked, not to be repeated) before the first migrate
deploy on that server, and added `prisma migrate deploy` to the
"every subsequent deployment" checklist, after npm install and
before restarting the app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
runCleanup previously had no try/catch around each expired job, so one
job's markCleaned/delete failure would abort the whole pass, leaving
later expired jobs' files undeleted for that run. Each iteration is now
wrapped in a try/catch that logs and continues; the returned count only
reflects jobs that actually completed the delete+markCleaned sequence.
markDone now guards outputPath/outputMimeType/outputSizeBytes/
conversionDurationSeconds with `?? null`, matching the guard createJob
already has on `quality` — Prisma treats `undefined` in a data object as
"leave the column alone" rather than binding NULL like the old raw SQL
did. Currently unreachable in practice since the worker always passes
real values, but keeps the repository defensive and consistent.
Added a cleanup.test.js case that monkey-patches
prisma.conversionJob.update to reject for one job's cleanedAt update,
asserting a later expired job in the same batch still gets cleaned.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
src/db.js imports @prisma/client at runtime, but it was listed under
devDependencies (likely merged with the `prisma` devDependency during
Task 1). A production install that omits dev dependencies would crash
on startup with a missing-module error. `prisma` (the CLI) stays a
devDependency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 4 rewrote src/db.js and src/jobs/jobRepository.js to use Prisma
instead of the hand-rolled mariadb pool. This updates every remaining
call site (app.js, server.js, worker.js, cleanup.js) and the 5 test
files that still referenced getPool/closePool/pool.query, so the app
and full test suite compile and run against Prisma Client.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Marks 0_init as applied via prisma migrate resolve so Prisma's
migration history is in sync with the pre-existing conversion_jobs
table, without running any SQL against the table itself.
Task 3 found Prisma's generated migration SQL never emits an
explicit ENGINE=... clause, same root cause as the already-accepted
collation exception (no schema-level attribute exists). No practical
impact: the baseline never executes this SQL against the real DB,
and any fresh deploy still gets InnoDB from the server default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of Task 1 (d86245a) found two issues:
1. prisma/schema.prisma didn't reproduce db/schema.sql's DDL exactly.
Add @db.DateTime(0) to createdAt/updatedAt/expiresAt/cleanedAt (Prisma
otherwise defaults to DATETIME(3)), and explicit map: names on the
uuid unique constraint and status/expiresAt indexes so they match the
real table's uniq_uuid/idx_status/idx_expires_at. Verified via
`prisma migrate diff --from-empty --to-schema-datamodel`, which now
shows DATETIME(0)/CURRENT_TIMESTAMP(0) and the correct names. Table
collation and updated_at's ON UPDATE CURRENT_TIMESTAMP remain
documented, accepted gaps with no schema-level fix in Prisma's mysql
provider.
2. The original @prisma/client@6/prisma@6 pin used --force without
diagnosing why. Reinstalled with `npm install @prisma/client@6
prisma@6 --save-dev` (no --force) directly against this project's
node_modules: it completed cleanly with no conflicts, confirming
--force was unnecessary.
`prisma validate`, `prisma generate`, and the full test suite
(69 passed, 4 pre-existing failures unrelated to this change) all pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 1's reviewer found the original schema.prisma didn't reproduce
db/schema.sql's DDL (DATETIME(3) vs DATETIME, auto-generated index
names, no explicit precision on defaults). Verified the real DDL via
SHOW CREATE TABLE and empirically confirmed @db.DateTime(0) plus
explicit map: names close the gap. Two remaining differences (table
collation, no DB-level ON UPDATE for updatedAt) are Prisma/MySQL
provider limitations with no schema-level fix; documented as
accepted, per user decision, since the baseline never executes this
SQL against the real database and all writes go through Prisma.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task-by-task plan to replace the mariadb pool/repository with
Prisma Client and baseline Prisma Migrate against the existing
conversion_jobs table without data loss.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plans the replacement of the raw mariadb pool and hand-written
repository with Prisma Client, plus a no-data-loss Prisma Migrate
baseline against the existing conversion_jobs table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers data model, backend threading through registry/image/imageToPdf/worker, frontend controls, validation, and testing for a new quality/compressionLevel option on image conversions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename the old CHAR(36) id to uuid (still used for public URLs and file
naming) and add a real auto-increment id as the primary key. Track
input/output file size and conversion duration per job. Cleanup no
longer deletes rows; it marks cleaned_at and skips already-cleaned
expired jobs on later runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also fixes a Windows-specific bug in worker.js and cleanup.js: the
"run only if executed directly" guard compared import.meta.url against
`file://${process.argv[1]}`, which never matches on Windows (backslash
path separators, missing extra slash before the drive letter). main()
silently never ran, so the worker process started and exited
immediately without ever polling. Caught during this task's manual
verification of the full upload-convert-download flow. Fixed with
node:url's pathToFileURL, which builds a correct file:// URL cross-platform.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also adds vitest.config.js with fileParallelism: false. Every DB-backed
test file wipes and reseeds the shared conversion_jobs table in
beforeEach; running test files in parallel (Vitest's default) let one
file's DELETE race another file's just-inserted row against the same
live MariaDB instance, causing intermittent cross-file failures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Regenerates the sample.png test fixture via sharp itself: the
hand-encoded base64 PNG from the plan had valid magic bytes (enough to
fool file-type's signature check) but was malformed past the header,
which libpng rejected as soon as sharp actually tried to decode it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also configures the MariaDB pool with timezone: 'auto', since the
default 'local' mode sends dates without timezone conversion and
silently broke expires_at comparisons whenever the app host and DB
server clocks differ (caught by the findExpiredJobs test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
18 bite-sized TDD tasks from project scaffolding through the React
frontend and an end-to-end pipeline test, following the approved design
spec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>