Commit Graph
435 Commits
Author SHA1 Message Date
SnapOtter 63a2d309ce test: honor SYNC_WAIT_MS in docker tests instead of clobbering it
tests/setup/per-fork-env.ts hardcoded SYNC_WAIT_MS=30000 on every fork, overriding whatever the container set, so the docker test image could never grant heavy ops a wider sync window. A 12MP stress-image enhance takes ~34s on the macOS Docker VM (Sharp runs 2-3x slower there), just past the 30s window, so the factory returned 202 and three sync-asserting image-enhancement tests failed.

Honor a higher SYNC_WAIT_MS when provided (30s floor preserved for host/CI), raise it to 120s in docker-compose.test.yml, and make the vitest test/hook timeouts env-overridable so a slow-but-correct job returns 200 rather than tripping a framework timeout. Host and CI behavior is unchanged.
2026-06-17 14:28:41 +08:00
SnapOtter 1f5b222267 test: fix docker test-image env and container-specific test guards
Make the full pnpm test:docker suite pass the env-dependent tests (~85 failures):
- Dockerfile.test: ENV LD_LIBRARY_PATH=/usr/local/lib so the built libheif 1.21 is not shadowed by the base image's older system libheif (heif-dec failed with an undefined-symbol error -> 'No HEIF decoder found' on 72 HEIF tests); add libjxl-tools (JXL) and ghostscript + the ImageMagick policy.xml EPS allow-edit.
- docker-compose.test.yml: SYNC_WAIT_MS=30000 so sync-wait image tools do not fall back to 202 under single-container contention (10 tests).
- install_feature.py: guard tarfile.extractall(filter='data') behind Python>=3.12 (bookworm ships 3.11); the manual entry guards already protect.
- feature-status.test.ts / docker-file-secrets.test.ts: skip the two cases that cannot hold inside the container (/.dockerenv always present; root bypasses chmod). Verified on host: all still pass.
2026-06-17 14:28:41 +08:00
SnapOtter c483897452 fix(api): drop app-logger import from media-input; update stale errors mock
The full unit suite surfaced two issues from earlier commits on this branch. (1) Importing the app logger into media-input.ts pulled logger.ts -- which builds its pino file transport at module load via join(env.LOG_DIR, ...) -- into the unit-test import graph, throwing at collection time wherever LOG_DIR is unset (integration tests set it; unit tests do not). A low-level modality handler should not depend on the app logger, and a corrupt upload is an expected user error, so drop the import and keep the clean validation message. (2) tool-factory-route.test.ts mocked errors.js without the new friendlyError export; add it.
2026-06-17 14:28:41 +08:00
SnapOtter 9c250d244f test: add regression coverage for the video QA fixes
friendlyError unit test (incl. the false-positive guard); gated integration tests for multi-file video batch and a multi-step video pipeline (regression for the modality-aware batch/pipeline fix). All pass locally; existing image batch (36) and pipeline (37) suites remain green, and the existing gif-to-video webm test now passes with the pix_fmt fix.
2026-06-17 14:28:41 +08:00
SnapOtter 3120e6708d fix(editor): apply layer effects + object flip, add Beta badge, repair e2e specs
While getting the editor e2e suite green, three "stale test" failures turned
out to be real bugs (per the reporter's hunch that tests might be catching
real issues):

- Layer effects (drop shadow, glows) never applied. The panel wrote effects
  into `attrs.effects` through updateObject, but the panel and renderer both
  read the object's top-level `effects`, so the toggle never persisted. Add a
  dedicated `setObjectEffects` store action and route the panel through it.
- Object flip (transform tool) did nothing. No object renderer applied
  `scaleX`/`scaleY`, and the flip negated scale without compensating position.
  Apply scale in the renderers and flip in place: mirror points for stroke
  objects, negate scale + shift position for sized objects.

(The paint-bucket / pixel-tool coordinate bug and the broken-at-non-100%-zoom
export were fixed in the preceding #259 change.)

Also adds a small "Beta" badge to the editor (welcome heading + nav link) and
repairs ~18 stale editor e2e specs whose selectors/assertions had drifted from
the current UI: the options bar is `h-9` not `h-10` (added a stable
`data-testid`), the menu bar is `h-8`/`bg-background`, the flip button
aria-labels are lowercase, the welcome "Image Editor" heading collides with an
sr-only `<h1>`, the color-picker tabs need a role-scoped selector, and the
magic-wand / flip tests now use deterministic setup and assert the actual
effect instead of fragile screenshot diffs.
2026-06-17 14:21:35 +08:00
SnapOtter 81e16d7ce6 fix(editor): capture document pixels without the zoom/pan transform (#259)
Every editor tool that reads or writes raster pixels exported the stage with `stage.toCanvas({ x: 0, y: 0, width, height })`, which bakes in the stage's zoom/pan transform. The captured buffer was the *viewport* (the document scaled and offset by the current zoom/pan), not the document in its own coordinate space, so tools sampled and wrote the wrong pixels: the paint bucket produced a misplaced black rectangle instead of flood-filling the click, the eyedropper read the wrong colour, the magic wand selected the wrong region, and PNG/clipboard export silently produced a scaled/offset image at any zoom other than 100%.

Add `captureDocumentCanvas()`, which normalizes the stage to the document size with an identity transform, renders, captures, and restores -- all synchronously, so there is no visible flicker. Route every pixel capture through it: fill, magic wand, clone stamp, eyedropper, dodge/burn, blur/sharpen/smudge, the adjustments histogram, and the exporter.

The 'rulers render as black bars' part of #259 was fixed in the preceding editor-layout change (#258).

Adds editor-tool-coordinates.spec.ts asserting the paint bucket fills at the clicked location.
2026-06-17 14:21:35 +08:00
SnapOtter 063a2e47e2 fix(editor): fill canvas viewport, fix black rulers, add resizable panel (#258)
The image editor canvas only used part of the viewport, and the right sidebar was a fixed width that could clip its controls on shorter screens.

- Canvas: the canvas container used `flex-1`, but its parent wrapper in editor-page.tsx was not a flex container, so it collapsed to the Konva Stage's content height (~600px), leaving a large inert region below. Make the wrapper a flex container so the canvas fills the available area.

- Rulers: ruler background/ticks were set via `ctx.fillStyle = "var(--color-card)"`, which canvas 2D cannot parse, so the default black fill remained and painted the rulers as solid black bars. Resolve the theme tokens to concrete colors from computed style at draw time (theme-aware).

- Right panel: add a left-edge drag handle to resize the panel (240-480px, persisted to localStorage) and `min-h-0` so the tab content scrolls internally instead of pushing the color controls off-screen.

Adds editor-layout.spec.ts (canvas-fill + resize) and a ruler-not-black regression test. All 7 targeted editor e2e tests pass.
2026-06-17 14:21:35 +08:00
SnapOtterandGitHub 17726ae59d docs: multi-modality rebrand, 2.0 architecture accuracy, and full OpenAPI coverage (#254)
* docs: rebrand from image-only to multi-modality across docs and metadata

SnapOtter expanded from image-only to 157 tools across 5 modalities
(image, video, audio, document/PDF, data). Update all product-level
copy, metadata, and i18n that still framed it as an image-only tool.

- README, package.json, root llms.txt: multi-modality framing, 157 tools
- OpenAPI info + tags, generated /llms.txt tagline (docs.ts)
- VitePress docs site: hero, getting-started, architecture, security,
  deployment, configuration, developer, supported-formats
- i18n: 10 product keys across all 21 locales (hero, app description,
  privacy notes, AI features, progress messages, getting-started)
- web/demo/landing meta + privacy copy, COMMUNITY_GUIDE, .env.example

Stale tool counts (53/50+/52/70+/35) corrected to 157 throughout.
Database/container deployment claims left unchanged (out of scope).

* docs: fix stale post-rebrand test assertions and README language list

- tests/e2e-docs/homepage.spec.ts: assert the current docs homepage (file toolkit, 157 tools, 5 modalities) instead of the old image-only strings
- tests/unit/api/docs-route.test.ts: sync the reproduced llms.txt tagline with docs.ts
- README.md: 21 languages with the correct list (add Swedish and Chinese Traditional, drop Czech which is not supported)

* docs: correct 2.0 architecture references (Postgres 17 + Redis 8, 3-container stack)

The docs and metadata still described the 1.x stack (SQLite, single container, p-queue). Update them to the current 2.0 reality.

- README: replace the broken single-container `docker run` quick-start with the real Docker Compose stack (app + Postgres 17 + Redis 8); fix the "no Redis, no Postgres" feature bullet
- package.json: description no longer claims a single container
- apps/docs: rewrite database.md for Postgres; configuration.md DB_PATH -> DATABASE_URL + REDIS_URL; architecture.md SQLite/p-queue/better-sqlite3 -> Postgres/BullMQ/pg and add media-engine + doc-engine; developer/security/deployment/docker-tags/getting-started/contributing compose examples now include postgres + redis; index.md + api/ai.md AI count 16 -> 19
- SECURITY.md: Drizzle (SQLite) -> (PostgreSQL)
- landing: enterprise/FeatureHighlights single-container wording; TrustSignals/ToolGrid 150+ -> 157 (dynamic); Pricing/FAQ 15 -> 19 AI tools

* docs(api): document all video, audio, document, and data tool endpoints in OpenAPI

The spec covered only image tools; the Scalar UI and the generated /llms.txt and /llms-full.txt inherited that gap. Add the 104 missing tool endpoints so the API docs match the code.

- Video: 29 endpoints (most long/async; auto-subtitles is AI)
- Audio: 17 (transcribe-audio is AI)
- Document/PDF: 36 (ocr-pdf is AI; conversions are long/async)
- Data: 10
- Image: 12 newer tools (background-replace, blur-background AI; histogram/lqip-placeholder/sprite-sheet custom responses; barcode-generate uses a JSON body)

Each schema is derived from the tool's Zod validator and executionHint (fast -> 200, long -> 202+SSE, AI adds 501 FeatureNotInstalledError, multi-file inputs as arrays), referencing the existing shared schemas. Tool path entries: 64 -> 168. Spec parses as valid YAML with no duplicate paths and only known $refs.
2026-06-16 18:04:52 +08:00
SnapOtterandGitHub 8eee17aeea fix: handle non-image modalities across uploads, previews, and filenames (#255)
SnapOtter spans five modalities now, but several code paths still assumed image input.

- dropzone: default to accept-all when no fileFilter is given (image tools still pass one); neutral "supported file types" error text instead of "image files"
- automate (pipelines): accept any modality in the file pickers and dropzones; render modality-aware previews (video player, audio waveform, document/data card) instead of always using ImageViewer/BeforeAfterSlider
- filename sanitizer: extend the double-extension allowlist beyond image extensions to video/audio/document/data so e.g. "report.csv.php" becomes "report.csv"; add tests
- thumbnail route: return 422 for non-rasterisable files (audio, data, non-PDF docs) instead of attempting a doomed Sharp decode
- pool: unknown tools fall back to the "system" pool, not the image pool
- a11y labels: "Previous/Next image", "Image viewer/area/controls/drop zone" are now modality-neutral, across all 21 locales
- copy: bulk-rename default, find-duplicates ZIP name, SSRF user-agent, fetch-urls fallback name, file-details MIME label, URL-import placeholder, help dialog
2026-06-16 18:04:48 +08:00
SnapOtterandGitHub 622c9f98a5 fix(ci): resolve pre-existing failures making main red (#250)
After #249 merged, main's CI is red on failures that predate this work (they
were cache-masked on the old branch). This applies only the missing fixes onto
current main (reverts nothing from #246/#248; saml/user-files are already fixed
on main):

- remove 8 dead landing unit tests importing @landing/app and @landing/components
  React paths deleted in the Astro migration
- format tool-factory.ts and json-xml.ts to clear the api lint errors
- accept a 202 async fallback in the image-enhancement large-image test (it
  exceeds the sync window on slower CI runners)
- relax the pipeline no-file assertion to match 'No image file provided' or
  'No file provided'
2026-06-16 16:09:45 +08:00
SnapOtterandGitHub d50e8e42a7 QA + image-tool depth pass: codec/eraser/PDF fixes, modality renames, 13 image tools deepened (#249)
* fix(media): mux container-correct codecs in video tools

Video tools hardcoded H.264 (and AAC) while keeping the input's container extension, so a .webm input produced an invalid file (ffmpeg exit 234: H.264 cannot be muxed into WebM). Add shared videoEncodeArgsForContainer/audioEncodeArgsForContainer helpers (vp9+opus for webm, theora+vorbis for ogv, h264+aac otherwise) and apply them across 14 tools; re-encode audio to AAC in burn-subtitles (forced mp4). Adds a webm regression test for change-fps.

* fix(eraser): recover Object Eraser when its progress SSE drops

The eraser used a bespoke EventSource with no recovery, so a dropped SSE left the UI stuck at ~25% forever even though the backend job had finished and saved its result. Add a resilient subscription (reconnect on tab refocus, which replays the cached terminal frame; 5-minute stall timeout) mirroring the standard processor's PR #203/#204 recovery.

* feat(ui): rename the Documents modality to PDF and Data to Files

Updates modality display names, the home-page tabs, the tool-page breadcrumb, and the homePage.documents/data + modalities labels across all 21 locales. URL slugs are unchanged for link stability.

* feat(compress-pdf): add quality and target-size compression modes

Mirror the image Compress tool: a quality slider (1-100) and a target file size, replacing the screen/ebook/printer preset. Adds gsCompressPdfQuality to doc-engine (quality maps to image downsample DPI, the dominant size lever for PDFs); target-size binary-searches the DPI for the highest quality under the target. The frontend reuses the shared CompressControls component, so no new translation strings are needed.

* feat(ocr-pdf): show the PDF preview and extracted text side by side

ocr-pdf fell back to the image viewer, which cannot render a PDF, so the right pane showed 'Preview not available' and the extracted text was only a download. It now uses a custom results view (custom-results display mode) rendering the input PDF via pdf.js (DocumentView gains an inputOnly prop, since the tool's output is a .txt) next to the extracted OCR text, with a copy button.

* feat(ui): link the modality breadcrumb to its tools tab

The modality segment of the tool breadcrumb (PDF, Image, Video, Audio, Files) is now a link to /?modality=<tab>. The home page reads the param, activates the matching tab, and cleans the URL, so it returns to the existing Tools page filtered to that modality without a new page. Handles the file modality whose tab key is 'data'.

* feat(circle-crop): add zoom/offset framing, border, background, and output size

Upgrade the circle-crop tool from a bare centered crop into a framing and
styling tool. New settings (all backward-compatible with the old empty
payload):

- zoom (1-5x) plus offsetX/offsetY (0-1) to control how tight the circle is
  and where it sits in the source image
- borderWidth (0-200px) plus borderColor for an optional ring
- background: transparent (clear corners) or a hex fill
- outputSize for a square output; omitted keeps native size

The settings panel gains an inline draggable circular preview that mirrors
the framing live, a zoom slider, a border slider with color, a
transparent/color background toggle, and an output-size field. Adds an
integration test covering output size, border, and a solid background.

* feat(image-tools): flesh out five thin tools (gif-webp, histogram, favicon, color-palette, lqip)

Tier A of the image-tool depth pass. Each of these was as bare as the old
circle-crop (empty settings, opaque or invisible output). Now:

- gif-webp: quality, lossless, and resize-percent controls; shows before/after size
- histogram: returns full per-channel bins + stats; the settings panel renders an
  inline interactive histogram with R/G/B/Luma toggles, linear/log scale, and a
  mean/median/stdev readout (server PNG still downloadable)
- favicon: background fill, padding, corner-radius, theme color, and a per-size
  checklist, with a live preview grid; the route applies the styling and honors
  the size filter
- color-palette: count (2-16) and hex/rgb/hsl format controls, median-cut
  extraction, a palette strip, and CSS/JSON export
- lqip-placeholder: blur/pixelate/solid strategies, format and quality; the
  output panel now surfaces the data URI with copy plus HTML/CSS snippets and a
  preview (previously the deliverable was never shown)

Also expose resultPayload from useToolProcessor so a tool can render the route's
extra result fields (histogram bins, lqip data URI) in its own panel. Updates the
five integration tests to cover the new settings.

* feat(image-tools): deepen five thin tools (duotone, vignette, pixelate, background-replace, blur-background)

Tier B of the image-tool depth pass.

- duotone: preset palettes, an intensity slider that blends the duotone with
  the original, and a true live duotone preview (a self-contained grayscale +
  lighten/darken overlay so the pane filter cannot wash it out)
- vignette: radius, softness, roundness, and center-x/y controls driving a
  rebuilt radial gradient, with a matching live overlay
- pixelate: a selection mode that exposes the route's region support via a
  draggable box over the image plus width/height sliders, so a face or plate
  can be pixelated in isolation
- background-replace: gradient backgrounds, edge feather, and webp output on top
  of the existing solid color; now shown before/after
- blur-background: edge feather and webp output; now shown before/after

The live previews for duotone and vignette needed onImageStyle to mount the
overlay branch in image-viewer. The duotone intensity blend and both AI tools'
edge feather were rewritten to splice the alpha channel through raw buffers;
joinChannel did not reliably re-tag the merged channel as alpha and a
raw-without-encoder buffer broke the next decode. Updates the five integration
tests.

* fix(data): rename Files modality to Data + 20 Data-tool bug fixes (#247)

* fix(ui): restore the Data modality name (revert Files rename)

The 'file' modality reverts to the 'Data' label in modality.ts, the home-page tab, and the tools.data + documentsAndFiles i18n keys across all 21 locales. The separate Documents to PDF rename is kept. The URL slug was already /data, so name and slug realign; the tool breadcrumb follows modality.ts automatically.

* fix(create-zip): require at least two files before enabling submit

create-zip enabled its submit button with a single file, but the backend rejects fewer than two files ('Zipping needs at least two files'), producing a 422 error. Gate the button on files.length >= 2 to match the sibling merge-csvs tool. Found during the Data-modality QA sweep.

* fix(data): resolve 17 bugs found in a deeper Data-tool review

Crashes (threw an internal error on otherwise-valid input):
- csv-json: a primitive JSON array like [1,2,3] threw "Unable to serialize"; now a clear error.
- json-xml: a null or primitive JSON root crashed the XML builder; now a clear 4xx.
- yaml-json: an empty or comment-only YAML returned undefined and threw on Buffer.from; now emits null.

Data loss / wrong output:
- csv-json: nested objects rendered as "[object Object]" (now serialized to JSON); heterogeneous objects dropped columns (now the union of all keys).
- xml-to-csv: leaked fast-xml-parser markers ("@_" on attributes, "#text") into CSV headers (now cleaned); a single-record XML failed to tabulate (now a 1-row table); heterogeneous records dropped columns (now the union of all keys).
- csv-excel: xlsx date cells were rendered in the server timezone via Date.toString (now ISO 8601, round-trippable).
- create-zip and extract-zip: filename/basename collisions overwrote zip entries and silently lost a file; dedup now checks generated names and guarantees uniqueness.
- chart-maker: negative values produced invalid/degenerate SVG that Sharp silently dropped; now rejected with a clear message.

Empty output / validation:
- split-csv: a header-only CSV produced an empty zip; now errors with "No data rows to split".
- extract-zip: a directory-only zip produced an empty zip; now errors with "No extractable files found".
- create-zip and merge-csvs: a single-file request fell through to the worker and returned 422; the factory now supports minInputs and returns 400 pre-enqueue.

UI:
- review-panel: the result card showed "Saved +X%" when the output grew; the savings row now appears only when the file is actually smaller (Original/Processed sizes always shown).

Found via two adversarial code-review passes over the 10 Data routes. All 24 fix + regression checks pass against a fresh Docker stack on :1359.

* fix(data): clean 400 for unsafe-zip entries; drop header on split keepHeader=false

- tool-factory: add an opt-in preValidate hook that runs after input prep and
  before enqueue. Throwing InputValidationError there returns its statusCode
  (400) instead of the worker's generic 422. BullMQ loses the error class across
  the job boundary, so InputValidationErrors thrown in the worker cannot be
  mapped to their status; pre-enqueue validation can.
- extract-zip: validate entry paths via preValidate, rejecting path-traversal
  and absolute-path archives (and unreadable/corrupt zips) with a clear 400. The
  processV2 guards remain as defense-in-depth for the pipeline/batch path.
- split-csv: keepHeader=false now drops the header (parts contain only data
  rows) instead of keeping it as the first data row of part-1.

Verified against a fresh Docker stack: unsafe / absolute / corrupt zips -> 400,
normal zip still 200; split keepHeader=false drops the header while true repeats
it in each part. No regressions across 51 fix + scenario checks.

* feat(image-tools): deepen image-pad and sprite-sheet, fix sprite-sheet multi-file submit

Tier C of the image-tool depth pass.

- image-pad: a custom W:H ratio alongside the presets, a background mode
  (solid color, transparent, or an Instagram-style blurred cover fill), and an
  extra padding margin. The settings panel gains a real live preview of the
  padded canvas (it previously declared live-preview but rendered nothing) via
  onImageStyle + onImageOverlay.
- sprite-sheet: PNG/WebP/JPEG output with a quality control, and the coordinate
  map it already computes is now returned and surfaced as Copy CSS (per-frame
  background-position rules) and Copy JSON exports.

Also fix a pre-existing sprite-sheet bug: with more than one image the panel
called processAllFiles, fanning out to the per-file batch route (422). It now
calls processFiles, which packs all images into a single sheet request (it is a
MULTI_FILE tool). Updates both integration tests.

* fix(media): preserve source sample rate after loudnorm (#243)

ffmpeg's loudnorm filter runs internally at 192 kHz and emits 192 kHz
unless the chain resamples back. normalize-audio and video-loudnorm
therefore produced 192 kHz output (4.3x larger files) regardless of the
input rate. Append aresample to restore the input's sample rate.
runMediaTool now exposes the input audio sample rate to its args callback.

* fix(color-palette): collapse solid-color images to one swatch

The median-cut bucket selector started bestRange at -1, so a uniform bucket
(range 0) still satisfied the > comparison and kept splitting, yielding N
identical swatches for a solid-color image. Start at 0 so only buckets with
real color spread are split.

* fix(lint): annotate implicit-any lets in saml and user-files

biome noImplicitAnyLet flagged the bare let in saml.ts (profile) and user-files.ts (stream); add derived type annotations (type-only, no behavior change). Latent on main via the turbo lint cache; surfaced when the Data changes busted the apps/api lint cache.
2026-06-16 15:16:13 +08:00
SnapOtterandGitHub d8cf979d4b fix: resolve 18 QA-discovered bugs across tools, previews, and the AI pipeline (#242)
Exhaustive QA sweep of all 157 tools. Fixes: CSP blob media, csv-excel ExcelJS interop, ocr-pdf segfault, chart-maker upload, non-PDF doc preview, RAW decode, merge-tool multi-file path, html-to-image chromium, ogv/wma/amr/ac3 preview fallbacks, meme/gif/stabilize codecs, nav+home a11y. Plus orphan-format and test-debt cleanup, the AI bundle build script, and a reusable Playwright QA harness under tests/qa/.
2026-06-15 22:26:24 +08:00
SnapOtterandGitHub 25babfae15 test(e2e-landing): match enterprise redesign and fix Astro dev-server config (#241) 2026-06-15 22:14:50 +08:00
SnapOtter 507fc1c356 Merge branch 'feat/tool-first-workflow'
Resolved conflict in worker.ts: kept remote refactored worker
(v2 process, scratch paths, extra outputs, metrics) and re-applied
the auto-save removal from the feature branch.
2026-06-15 14:55:58 +08:00
SnapOtterandGitHub 3fb8164fa5 feat: add OpenTelemetry distributed tracing (enterprise) (#232)
* feat(tracing): add OpenTelemetry dependencies and --import preload flag

* feat(enterprise): add distributed_tracing feature gate

* feat(tracing): add SDK bootstrap with enterprise gating

* fix(tracing): correct test coverage for enterprise-unavailable path and prevent double-init

Test 2 now mocks @snapotter/enterprise to throw an import error, exercising
the catch block in the preload. Test 3 imports with no endpoint so the preload
is a no-op, avoiding leaked SDK from double-initialization. Added idempotency
guard to initTracing() as a safety net.

* feat(tracing): add Pino trace mixin and shared logger

When OTel tracing is active, every Pino log line now includes traceId,
spanId, and traceFlags fields for log-to-trace correlation. The mixin
is a no-op when no SDK is registered (community users).

* feat(tracing): add _otel to ToolJobData and inject trace context at enqueue

Add optional _otel carrier field to ToolJobData for W3C trace context
propagation across BullMQ job boundaries. When an active OTel span exists,
propagation.inject() writes traceparent/tracestate into the job data before
queue.add(). When no SDK is registered (community edition), the carrier
stays empty and _otel remains undefined -- zero overhead.

* feat(tracing): extract trace context and create spans in BullMQ worker

* feat(tracing): inject trace context into Python sidecar calls

* feat(tracing): add trace context extraction to Python sidecar

* feat(tracing): add shutdownTracing to graceful shutdown sequence

* feat(tracing): enrich HTTP spans with tool_id and user_id attributes

* docs: add OpenTelemetry env var documentation to .env.example

* test(tracing): add lifecycle integration tests for trace propagation

* fix(tracing): inject trace context into pipeline and batch flow jobs

* fix(tracing): add sidecar.execute Node-side span and remove unnecessary comment

Wraps PythonDispatcher.run() with a sidecar.execute span on the Node
side so traces show the full round-trip (Node span -> Python span).
Also removes an obvious comment from logger.ts.
2026-06-15 12:53:06 +08:00
SnapOtter f387e98fff feat: tool-first workflow polish -- fix tests, lint cleanup
- Remove obsolete fullscreen redirect test (route deleted in Task 3)
- Update dropzone test strings to match new i18n values
- Fix categories count in i18n locale test (12 -> 24)
- Add chart-maker to no-dropzone exempt list in tool-registry test
- Auto-fix import ordering (Biome) in popular tools route and landing config
2026-06-14 19:44:58 +08:00
SnapOtter 112df13957 test: add enterprise feature test coverage
Tests for SCIM, GDPR lifecycle, legal hold, audit export/archival,
encryption, SIEM forwarding, MFA endpoints, IP allowlist, config
export/import, webhook management, and license validation.
2026-06-14 16:50:35 +08:00
SnapOtter 8403222d08 feat(landing): migrate from Next.js to Astro 5
Zero client JS, 165 static pages, self-hosted fonts, Otter Orange
design system. Includes tool SEO data for all 157 tools, enterprise
page, and updated e2e landing tests.
2026-06-14 16:50:20 +08:00
SnapOtter f9ed5d125a Merge branch 'feat/enterprise-on-prem' 2026-06-14 16:10:15 +08:00
SnapOtter e7bef34918 style: apply Biome formatting to enterprise files 2026-06-14 14:53:44 +08:00
SnapOtter 954cfb01a6 fix: test server registration gaps, Redis subscriber cleanup, atomic settings upsert 2026-06-14 14:30:35 +08:00
SnapOtter db6f7bf38a feat(enterprise): add IP allowlisting with CIDR matching and Redis cache
Enterprise-gated onRequest hook that restricts API access to
configured CIDR ranges using Node 22's native BlockList.

- Plugin (ip-allowlist.ts): builds a BlockList from the ipAllowlist
  setting, caches in-process, syncs across instances via Redis pub/sub.
  Exempt paths for health probes, SCIM, SAML/OIDC callbacks.
  Handles IPv4-mapped IPv6 (::ffff:x.x.x.x) transparently.
- Admin API (enterprise/ip-allowlist.ts): GET/PUT endpoints gated by
  security:manage permission and ip_allowlist feature flag.  Validates
  CIDRs, prevents self-lockout, emits IP_ALLOWLIST_UPDATED audit event.
- 32 unit tests covering CIDR matching, validation, exempt paths, IPv6,
  and edge cases (/0, /32, mapped addresses).
2026-06-13 22:54:06 +08:00
SnapOtter 1787be35fe feat(enterprise): add TOTP MFA with enrollment, verification, and recovery codes 2026-06-13 22:49:00 +08:00
SnapOtter a1b5c6d2c3 feat(enterprise): add SCIM 2.0 provisioning (Users + Groups) 2026-06-13 22:42:55 +08:00
SnapOtter 6920035f5a refactor: extract external auth resolver from OIDC for SAML reuse
Move user resolution logic (match by externalId, auto-link by email,
auto-create with user limit check) into a shared module that both
OIDC and SAML callbacks can use. Includes sanitizeUsername and
findUniqueUsername helpers. Preserves all existing OIDC behavior
and audit events.
2026-06-13 22:20:22 +08:00
SnapOtter dd2a50799a feat(enterprise): add GDPR user/team data purge with audit redaction 2026-06-13 21:13:22 +08:00
SnapOtter b6a8226837 feat(enterprise): add GDPR user data export (async) 2026-06-13 21:07:28 +08:00
SnapOtter fa7da7ce0c feat(enterprise): add legal hold with cleanup bypass 2026-06-13 21:01:04 +08:00
SnapOtter b1e94bd988 fix: build script venv handling and lint fixes
- Use /opt/venv directly when --entrypoint bash bypasses entrypoint.sh
- Use sys.executable for all pip calls (not bare pip)
- Override entrypoint in CI workflow to avoid startup banner
- Fix Biome formatting (template literals, try/catch blocks)
2026-06-13 20:48:27 +08:00
SnapOtter de7bbcc18d test: update permission count tests for new enterprise permissions 2026-06-13 17:07:39 +08:00
SnapOtter c1dc27f248 feat(enterprise): add audit log archival with crash-safe state machine 2026-06-13 16:57:20 +08:00
SnapOtter d3f30a2f5d feat(enterprise): add SIEM webhook forwarding with circuit breaker 2026-06-13 16:53:32 +08:00
SnapOtter ab88b9ad0d feat: add webhook delivery module with retry and backoff 2026-06-13 16:49:47 +08:00
SnapOtter 895e29e93f feat(enterprise): add tamper-resistant audit mode with HMAC integrity 2026-06-13 16:48:09 +08:00
SnapOtter 913dd6bbe1 feat(enterprise): add audit log export endpoint (CSV/JSON) 2026-06-13 16:45:11 +08:00
SnapOtter a4fa3ce2a7 feat: rewrite install_feature.py for pre-built tar bundles 2026-06-13 16:37:00 +08:00
SnapOtter 2520cdd556 feat: add AES-256-GCM encryption at rest for sensitive settings 2026-06-13 16:35:28 +08:00
SnapOtter 5d5240e481 feat: extend importBundleArchive for site-packages and fixups 2026-06-13 16:32:45 +08:00
SnapOtter 36f083ba64 feat(audit): add TOOL_EXECUTED logging with opt-in setting
Add isToolAuditEnabled() helper that checks the auditToolOperations
DB setting (off by default) or falls back to the enterprise
audit_export feature flag. The createToolRoute factory now emits a
TOOL_EXECUTED audit entry on successful tool execution when enabled,
using a fire-and-forget pattern so a failed audit write never blocks
the tool response.
2026-06-13 16:31:49 +08:00
SnapOtter d1e9536cff feat: extend crash recovery for pre-built bundle artifacts 2026-06-13 16:29:50 +08:00
SnapOtter a4ac7cf7d6 feat: bump feature manifest to v2 with archive metadata 2026-06-13 16:27:24 +08:00
SnapOtter f6d5479334 test: add enterprise feature mock utilities 2026-06-13 16:22:01 +08:00
SnapOtter 86d6f50ea6 feat: add enterprise Phase 1-4 feature flags and new permissions 2026-06-13 16:20:24 +08:00
SnapOtter 492da820f8 fix(web): resolve feature install status sync and mutual exclusivity (#214) 2026-06-13 13:44:37 +08:00
SnapOtter 4e14e00a4c test(matrix): multi-modality format matrix covering video, audio, document, and data tools 2026-06-13 12:59:45 +08:00
SnapOtter e6e4ff921d test(matrix): settings variation matrix for tool-specific option combinations
Static data-driven test covering 90 tools with 551 variations. For each
tool that has configurable settings, every enum value, boundary number,
and boolean flag is exercised through the HTTP endpoint. The test asserts
no 500s occur and validates response shape for 200/202 results.

Coverage spans image (29 tools), video (20), audio (15), PDF/document
(21), and data (5) modalities.
2026-06-13 12:01:01 +08:00
SnapOtter 92c9bea3c7 test(fixtures): add sample files for all 37 missing accepted formats
Video (13): mov, webm, avi, m4v, mts, m2ts, 3gp, flv, wmv, mpg, mpeg, ts, ogv
Audio (9): flac, aac, m4a, ogg, opus, wma, aiff, amr, ac3
Subtitle (1): ass
Image RAW (5): cr2 (Canon), nef (Nikon), arw (Sony), orf (Olympus), rw2 (Panasonic)
Document (7): doc, ppt, odp, xls, ods, htm, markdown
Data/archive (2): yml, zip

Media files generated locally with ffmpeg from existing tiny.mp4/wav.
RAW files downloaded from rawsamples.ch and filesamples.com.
Office files downloaded from filesamples.com.
All validated with ffprobe/file magic bytes.
2026-06-13 11:17:20 +08:00
SnapOtter 5397f9b21c fix(ai): broaden SSRF pre-scan regex to cover srcset, poster, formaction, @import 2026-06-13 10:36:12 +08:00
SnapOtter fc718c1684 fix(jobs): strip internal paths from all worker SSE error frames
Closes #71. Several error paths in the worker could leak internal
filesystem paths (/tmp/workspace, /data/ai/venv, /app) through SSE
frames, resultPayload objects, and Redis batch-error lists. The
existing stripInternalPaths call at worker.ts line 345 only covered
the single-file processToolJob catch block.

Wrapped 6 additional call sites:
- processPipelineStep: prevError from DB and catch errorMsg
- processPipelineFinalize: composed errorMsg reaching SSE,
  recordChildOutcome, and resultPayload
- processBatchChild: catch error reaching recordChildOutcome
  and resultPayload
- processBatchFinalize: manifest errorMsg from DB rows
- recordChildOutcome (batch-progress.ts): defense-in-depth
  strip before Redis rpush

Added 10 unit tests for stripInternalPaths covering /tmp, /data,
/app, /opt, /home, /workspace, multi-path messages, safe passthrough,
and pipeline-step wrapping.
2026-06-13 10:36:01 +08:00
SnapOtter 39543d6512 fix(docker): make venv bootstrap upgrade-aware with pip-freeze stamp
On 1.x-to-2.0 upgrade the /data/ai/venv already existed with stale
packages, so the entrypoint skipped the cp from /opt/venv and 2.0
Python tools failed with ImportError (#85).

Dockerfile now writes a SHA-256 of `pip freeze` to /opt/venv/.venv-version
at build time. The entrypoint compares that stamp against the copy on the
volume; on mismatch it does a clean nuke + re-copy and resets
installed.json so the UI correctly shows bundles as needing reinstall.
Models in /data/ai/models survive the refresh, so bundle reinstall only
reruns pip (model downloads are idempotent).

Backward compatible: if the image has no stamp (old build), the
entrypoint falls through to the existing skip-if-exists behavior.
2026-06-13 10:31:22 +08:00