feat: AI-vs-reviewed match stats endpoints#3749
Closed
JanCizmar wants to merge 60 commits into
Closed
Conversation
Introduces the foundations of the Tolgee Apps plugin system: a third-party
app, described by a JSON manifest, can be registered at the organization
level, enabled per project, and surface its declared pages in the project
sidebar.
Scopes included
- Plugin registered for an organization: manifest-URL-based registration,
refresh, and removal under /v2/organizations/{orgId}/apps. New org
settings section "Custom apps" with a register dialog and per-app
refresh/remove actions.
- Plugin enabled per project: join entity AppEnabledForProject with
PUT/DELETE/GET endpoints under /v2/projects/{projectId}/apps. New
project settings "Apps" tab with a toggle per registered app. Removing
the org-level install also clears its enablements.
- Project sidebar entries: ProjectMenu now appends one item per enabled
app's project-dashboard-page module with the manifest's icon and title.
A new placeholder route /projects/{id}/apps/{installId}/{moduleKey}
shows the page metadata; the iframe + signed context token come in the
next scope.
Other artifacts
- dev-plugin/: a Vite + React + TS scaffold that serves a minimal manifest
at /manifest.json and a Hello-World page to drive the end-to-end flow
locally on http://localhost:5180.
- docs/dev-notes/TOLGEE_APPS_ARCHITECTURE.md: design memo covering the
manifest model, multi-tenancy, signed context tokens, postMessage
transport, iframe sandboxing, and the planned dev CLI.
- Backend tests: OrganizationAppsControllerTest (8) and
ProjectAppsControllerTest (7) — all green.
What's intentionally not included yet
- Sandboxed iframe rendering and the signed context token (next scope).
- REST API client + auth scopes, webhooks, inline annotations, dev CLI.
- Translation keys via the Tolgee API — UI uses defaultValue fallbacks.
Two follow-ups worth tracking outside of the PoC scopes file: - Incremental consent flow when a plugin tries to use a scope it wasn't granted at install time (ask for additional scopes). - Caching the install's granted scopes so the auth filter doesn't re-query on every request.
Wires Tolgee Apps into the existing permission model: - Manifest declares `scopes` (strings matching `Scope.value`). - Registration is now a 2-step flow: preview the manifest, show the requested scopes, then approve & install. - Refresh re-prompts for consent, highlighting added vs removed scopes. - A new app-issued JWT (audience `tg.app`) carries only identity claims (installId / projectId / userId). Permissions are resolved from the DB on every request, so revocation works the moment any of install, enablement, user, or `tokensValidNotBefore` changes. - `SecurityService.getCurrentPermittedScopes` intersects the install's granted scopes with the user's project scopes — same pattern as PAK. - `OrganizationAuthorizationInterceptor` rejects app tokens outright; apps never satisfy `@RequiresOrganizationRole`. - `canUseAdminPermissions` returns false for app auth so an admin using a plugin is still capped by the install's grant. Tests - OrganizationAppsControllerTest extended: preview returns scopes, register/refresh persist `grantedScopes`, unknown scope is rejected, empty scopes block stores no granted scopes. - New AppPermissionInterceptionTest: full pipeline coverage — happy-path intersection, install-lacks-scope deny, install-removed 401, enablement-removed 401, tokensValidNotBefore 401, org endpoint rejection, `@UseDefaultPermissions` accept, malformed token 401. Out of scope (deferred) - Install-context (server-to-server) tokens — ship with webhooks. - Refresh-token hook on the SDK side. - Separate signing key for app tokens. - Granular per-scope opt-out during consent.
Closes the loop on Tolgee Apps PoC: a user opens a plugin page in
their project, an app-context JWT is minted server-side, the iframe
loads at the plugin's entry URL with the token, and the plugin calls
Tolgee's REST API on the user's behalf using @tginternal/client. The
intersection of install.grantedScopes and the user's project role
gates every call.
Backend
- POST /v2/projects/{projectId}/apps/{installId}/token mints a
user-context app token. @UseDefaultPermissions — any project
member can mint; 404 if the install isn't enabled for this project.
Frontend
- ProjectAppPageView swaps the placeholder for a real sandboxed
iframe. Calls the new mint endpoint, then sets the iframe `src` to
`<baseUrl><entry>?token=<jwt>&projectId=<id>&apiUrl=<tolgee-api>`.
- Iframe sandbox is `allow-scripts allow-forms` (no same-origin) so
the plugin runs in an opaque-origin browser sandbox.
Dev plugin
- Depends on @tginternal/client@0.0.0-prerelease.ec3da97e7
- Manifest declares scopes `["translations.view", "keys.view"]`
- App.tsx reads token/projectId/apiUrl from the iframe URL,
constructs an ApiClient with `userToken: <jwt>`, and fetches the
current project's name + the first 20 keys with translations.
Renders a simple table so the end-to-end auth path is observable.
Verification path
- Run dev backend + webapp, register the dev-plugin against an org,
enable it for a project, click the "Hello World" sidebar entry.
The placeholder is gone; the iframe shows "Hello World from
<project-name>" + a table of keys/translations fetched via the
REST API using the minted token.
- Pass token/projectId/apiUrl to the iframe via a ready/init postMessage handshake instead of URL query string. Platform checks event.origin and event.source; iframe mounts only after the token mutation returns so the listener is in place before the plugin posts ready. - Render the iframe chrome-less (no border, transparent background) and drop the in-page title in favor of the breadcrumb. The dev-plugin's index.css is transparent so the host's background shows through. - Return 403 with required scopes (instead of 404) when an app token's install has no granted scopes intersecting the project — the user reached the page through the install, so hiding existence is the wrong threat model. - Show raw scope ids on consent / refresh / list chips instead of redundant friendly labels. - Use typed components from @tginternal/client (KeyWithTranslationsModel, LanguageModel) in the dev-plugin instead of guessed shapes.
… backend
- Manifest can declare webhooks: { events: ["translation.set"], url }. On
enable, AppManagedAutomationService auto-creates a hidden WebhookConfig +
Automation + AutomationTrigger(ACTIVITY, SET_TRANSLATIONS) + WEBHOOK action
per (install, project). Disable / refresh / uninstall cascade through. The
user-facing webhook list filters out app-managed configs.
- AppInstall now carries three credentials: clientId (tgapp_), clientSecret
(tgapps_, hashed at rest like PATs), webhookSecret (tgappw_, cleartext —
HMAC signing needs it). Registration response exposes the cleartext
clientSecret exactly once; subsequent reads show only the prefix.
- AuthenticationFilter accepts two header forms for app credentials:
X-API-Key: tgapps_... and Authorization: Basic base64(clientId:clientSecret).
Optional X-Tolgee-Act-As-User-Id header downgrades the install's scopes to
the intersection with that user's project scopes.
- New event-validation whitelist on manifest webhooks; new error
APP_ACTING_AS_USER_NOT_PROJECT_MEMBER.
- Webapp: register dialog reveals the credentials once after install;
registered-apps row shows clientId, masked clientSecret prefix, and a
reveal-toggle for webhookSecret; refresh dialog diffs requested events
alongside the existing scope diff.
- Dev plugin grows an Express backend at :5181 (proxied through Vite) that
receives webhooks (HMAC-verified when TOLGEE_WEBHOOK_SECRET is set),
persists emoji + updatedAt in server/data.json, and serves /api/state +
/api/emoji to the iframe. A new "Note" column with a 5-emoji palette
demonstrates the round-trip.
- Server logs an ISO timestamp per translation update into a 14-day retention buffer (`data.events[translationId][]`), exposed alongside emojis + updatedAt via /api/state. Webhook payload's `modifiedEntities` is parsed as `Record<entityClass, ModifiedEntityModel[]>` (it's a map, not a flat array). - Frontend gains an "Activity" column with an inline 14-bar SVG sparkline per key, aggregating events across every translation of the key. Bucket math uses calendar-day alignment so today's edits land in today's bar rather than getting dropped. - Webhook URL in manifest is now absolute `http://localhost:5181/webhook` so Tolgee's server-side dispatcher reaches Express directly without depending on Vite being up. Vite's `/webhook` proxy is dropped; `/api/*` stays for browser-originated calls. - `tsx watch` loads `.env.local` via `--env-file-if-exists` so the operator can paste `TOLGEE_CLIENT_ID` / `TOLGEE_CLIENT_SECRET` / `TOLGEE_WEBHOOK_SECRET` once and restart.
Adds a second module type so apps can contribute collapsible panels into the translations view's right-hand tools panel, alongside Comments, History, MT and TM. Each app panel mounts a sandboxed iframe with its own signed token; selection identifiers (keyId/languageId/translationId) flow via postMessage rather than the JWT and update while the panel is open. The iframe can self-size via tolgee-app:resize. Dev plugin ships an Activity sparkline tab that exercises the new flow.
Adds a new extension point for apps: small icon buttons rendered next to
the existing edit / screenshot / comments controls on each key row and
each translation cell. The icon is a navigation shortcut — heavier UI
lives in existing surfaces (the tools panel, the key-edit dialog).
Manifest additions:
- key-edit-tab module: tabs rendered inside the key-edit dialog (parallel
to translation-tools-panel)
- key-action / translation-action modules with type link | tab | panel.
Link opens a URL (manifest urlTemplate or per-key dynamic URL). Tab
references a key-edit-tab and opens the dialog with that tab focused.
Panel references a translation-tools-panel and opens the side panel
scrolled to it.
- decoratorsUrl top-level field for dynamic decorators
Dynamic decorators are fetched frontend-direct: the webapp POSTs to the
plugin's decoratorsUrl with the user-context JWT we already mint for the
iframe; no Tolgee-side proxy endpoint. The plugin must set CORS. Static
manifest actions always render with URL-template interpolation
({keyName}, {keyId}, {languageTag}, etc.); dynamic responses can override
URLs and gate visibility per (keyId, languageTag).
Permissions: the key dialog opens via plugin tabs even without keys.edit.
In that mode native tabs and Save are hidden, leaving only the plugin
tabs as a read-only key-info view.
Shared iframe handshake (useAppIframeMessaging) replaces the per-component
postMessage wiring so the tools-panel iframe, the key-edit-tab iframe,
and any future iframe surface share one implementation.
Dev plugin demonstrates every shape: a static link key-action, a dynamic
tab key-action pointing at a new Audit tab, and a dynamic panel
translation-action pointing at the existing Activity side panel.
Note: backend test suite was not re-run as part of this commit because
the local Docker daemon is currently unresponsive; gradle compileKotlin
succeeded. Webapp eslint + tsc and dev-plugin tsc all pass. The schema
file (apiSchema.generated.ts) was patched manually for the same reason;
a real regen via scripts/regenerate-api-schema.sh will replace it
cleanly when docker recovers.
AppRegistrationResponseModel didn't pass decoratorsUrl to its parent AppInstallModel constructor, so newly-registered apps reported null even when the manifest declared the URL. Threads decoratorsUrl through the response model and its controller call site. Schema regenerated from the rebuilt backend so the generated TS types now match what Spring actually emits.
The decorators hook was bypassing the webapp's HTTP client to mint the install-context token, so the request hit /v2/projects/.../token without the Authorization: Bearer <user-jwt> header and the backend rejected it with 403. Adds the header from tokenService.getToken() before the fetch.
Clicking a translation-action of type 'panel' previously did nothing
useful: the side panel didn't open, and even if it did the panels
inside don't render until a cursor is set (ToolsPanel renders the
placeholder when no key+language is selected).
The click now also calls setEdit({keyId, language: languageTag}) to
focus the cell — which sets the cursor, opens the side panel, and lets
ToolsPanel render the panels for that translation. The existing
panelRevealEvent flow then ensures the plugin's panel is the one
expanded and scrolled into view.
A pending-reveal slot in panelRevealEvent handles the mount race: if
the side panel was closed at click time, FloatingToolsPanel (which
hosts the listener) hadn't mounted yet, so the live event would be
lost. ToolsPanel now consumes any pending request on first mount.
Verified end-to-end via Playwright against the running dev webapp:
clicking the 📈 decorator opens the side panel with the Activity panel
expanded and showing the sparkline for the focused translation.
Adds two refinements to the action-decorator system: - visibility: 'always' | 'on-hover' on key-action and translation-action manifest entries. Default is 'on-hover' (matches the native edit-pencil pattern). The dynamic decorators response can override visibility per (keyId, languageTag) instance — precedence is dynamic > manifest > default 'on-hover'. Lets a plugin keep an icon hover-only by default but elevate it for items needing attention. - count?: number on the dynamic response. When present and > 0 the icon is wrapped in a small MUI Badge mirroring the native unresolved- comments style. count === 0 hides the badge but keeps the icon. Backend: new AppActionVisibility enum, visibility field on both action modules, test asserts visibility round-trips through the response. Dev-plugin demos the feature: static view-source link is on-hover, show-activity is manifest-always but the /decorators handler emits a varying count per (keyId, languageTag) and dynamically downgrades to on-hover when the count is 0. Architecture doc updated. Verified: webapp eslint + tsc clean, dev-plugin tsc clean, gradle OrganizationAppsControllerTest passes. Browser e2e to follow after the IDE-launched backend is restarted (currently unresponsive on 8824).
Bumps @tginternal/client to 0.0.0-prerelease.c8329b552 which now ships typed webhook payloads — each activity type has its own AppWebhookPayload_<TYPE> schema covering eventType, activityData (with the discriminating `type` literal), modifiedEntities (typed per activity variant), webhookConfigId, timestamps, etc. Replaces the dev-plugin's hand-rolled payload type with a generated discriminated union assembled from all AppWebhookPayload_* schemas. The handler can now narrow by `payload.activityData?.type` if it later wants per-activity behavior; the current Translation-only logic stays as it was but with proper ModifiedEntityModel typing on the iterated entries.
Adds dev-plugin/server/webhookHandler.ts exporting: - WebhookType — keyof the schema's `webhooks` interface - WebhookPayloadFor<T> — concrete payload for a given event - AppWebhookPayload — discriminated union of every typed payload - onWebhook(payload, types, handler) — invokes handler with a payload narrowed to the supplied type(s). Accepts a single event name or an array; returns true on match so handlers can be chained. Refactors the /webhook route to use it. Activity-specific code now runs inside a lambda where TypeScript knows the exact payload shape (modifiedEntities.Translation typed straight from the schema, no manual casts).
Splits the monolithic server/index.ts into focused modules:
server/
├── index.ts — assembly + listen (~25 lines)
├── config.ts — env + paths + retention
├── store.ts — in-memory state + JSON persistence
├── cors.ts — CORS middleware
├── signature.ts — HMAC verification + parsing helpers
├── webhookHandler.ts — (existing) typed onWebhook utility
└── routes/
├── webhook.ts — POST /webhook
├── decorators.ts — POST /decorators
├── state.ts — GET /api/state
└── emoji.ts — PUT /api/emoji
Each route module exports a single register…(app) function and otherwise
contains small handler + helper functions (parse, build, persist). The
store now owns persistence; routes no longer touch the filesystem
directly. The webhook route delegates activity recording to the typed
onWebhook utility.
Also moves the local-only state file from server/data.json into
server/.data/data.json so the leading-dot directory makes it obvious
it's not part of the source tree. .gitignore updated accordingly; the
store mkdirs the directory on first persist.
Two related extensions to the apps SDK.
1. Webhook subscription opened up
AppManifestFetcher.SUPPORTED_APP_EVENTS now resolves to every
ActivityType enum name; AppManagedAutomationService maps the event
string straight via ActivityType.valueOf. Plugins can subscribe to
any activity (SET_TRANSLATIONS, CREATE_KEY, BATCH_KEY_RESTORE, ...)
instead of just the hardcoded translation.set. Dev plugin now
subscribes to four events; existing webhook payload typing via the
client schema's `webhooks` interface continues to narrow per event.
2. Modal as a fourth action behavior + five new trigger anchors
- New `modal` module declares iframe content (entry/width/height).
- New `key-action`/`translation-action`/etc. type `modal` plus the
existing `link` keep referencing it by `modalKey`.
- Five new trigger module types — `bulk-action`,
`translations-toolbar-action`, `project-menu-action`,
`key-edit-footer-action`, `shortcut` — each attaches plugin
items to a specific Tolgee surface and dispatches link or modal
on click. Shortcuts use mousetrap-style combinations (Mod+...).
- Cross-ref validation in AppManifestFetcher rejects dangling
modalKey, blank shortcut combinations, and link types missing
urlTemplate. Tests cover happy-path and three rejection cases.
- useAppIframeMessaging gains onClose and extraInitPayload so each
trigger anchor can pass its own selection context (bulk-action
forwards selectedKeyIds, key-edit-footer forwards keyId).
- AppModalProvider hosts a single dialog slot mounted at
ProjectRouter level. Generic useAppTriggers + useAppTriggerDispatch
reused by all five anchors.
End-to-end verified via Playwright against the dev-plugin: every
trigger surface is visible at the right place, click opens the modal
with the correct init payload, and the iframe's tolgee-app:close
closes the dialog.
Plugin `icon` strings are now resolved against a merged registry of
@untitled-ui/icons-react and tg.component/CustomIcons before falling
back to literal text. This lets plugin authors pick "Settings01" and
get the same outline icon as the native sidebar, while keeping the
existing emoji path working for `"⚙️"`.
The AppIcon component centralises the lookup. All nine plugin-icon
render sites (decorators, modal title, tools panel, key-edit tabs,
sidebar entries, project-menu actions, toolbar/bulk/key-edit-footer
trigger actions) now route through it.
The dev-plugin manifest mixes native names with one emoji-only entry
(Hello World 👋) and one unknown name ("Robot") to exercise both
fallback paths.
The footer-action trigger rendered after the Cancel/Save buttons in the key-edit dialog, which read as misplaced — Cancel/Save are the dialog's terminal actions, not a leading-edge toolbar. Plugins that want to act on the open key still have key-edit-tab, key-action decorators, and the modal trigger surfaces. Removes the trigger end-to-end: webapp UI, TriggerModuleKey, manifest DTO + validation, generated API schema, controller-test fixtures, dev plugin manifest, and docs.
Batch activity webhooks carry only a revisionId; the actual modified entities are fetched separately via the modified-entities-by-revision endpoint (paginated, filterable by entity class). Without this note plugin authors hit BATCH_* events and assume the payload is incomplete or malformed.
Add `PATCH /v2/organizations/{orgId}/apps/{installId}/manifest-url` so
an existing install can be re-pointed at a new manifest URL without
delete + re-register. The new manifest must still declare the same
`appId`. This unlocks the local-dev workflow where a Cloudflare quick
tunnel hands out a different URL every restart.
Wire the dev-plugin to use it:
- `manifest.json` is now served by Express from a template, with the
base URL read on every request from `.tolgee-dev/tunnel.json`.
Falls back to `http://localhost:5180` when no tunnel is active so
local-only iframe development keeps working unchanged.
- `scripts/dev.ts` boots a Cloudflare quick tunnel via the
`cloudflared` npm package, writes the assigned URL into the tunnel
state file, and PATCHes the install (when one is registered) so the
Tolgee snapshot points at the live tunnel URL.
- `scripts/register.ts` handles the one-time bootstrap: prompts for a
PAT + org, registers the plugin with a fresh tunnel URL, and stores
the install id + PAT in `.tolgee-dev/install.json` (gitignored).
- Vite proxies `/manifest.json`, `/webhook`, `/decorators`, `/state`,
`/emoji` to Express so a single quick-tunnel hostname exposes
everything Tolgee needs to reach.
The webapp's generated API schema isn't regenerated here — only dev
scripts call the new endpoint, and the running backend needs a restart
before regen can pick it up.
Top-level layout for the Tolgee Apps work:
apps/
tolgee-apps-sdk/ — @tolgee/apps-sdk skeleton package
example-apps/
dev-plugin/ — moved from the repo root
The SDK package is a scaffold (package.json + README + empty index.ts)
that captures what'll be published: manifest schema, postMessage
handshake, JWT decoder, webhook signature verifier, typed event
dispatcher. Implementations land as the contract stabilises; the
reference code currently lives inside example-apps/dev-plugin.
No behavioural change — git mv preserves the dev-plugin tree as-is.
No external references to the old path exist (checked: docs only mention
"the dev plugin" conceptually; no gradle/build scripts touch it).
Lift webhook + JWT + postMessage helpers out of dev-plugin into a
shared SDK so create-tolgee-app's template (and external plugin
authors) can depend on a typed surface instead of copy-pasting.
Three sub-entry points keep environment-specific code out of the
wrong bundle:
@tolgee/apps-sdk shared types only
@tolgee/apps-sdk/server WebCrypto webhook signature verifier,
typed onWebhook dispatcher, JWT decoder
@tolgee/apps-sdk/browser createTolgeeApp() postMessage handle +
createTolgeeAppClient() REST wrapper
WebCrypto for HMAC means the server helpers run unchanged on Node 20+,
Cloudflare Workers, Bun, and Deno.
dev-plugin now imports from the SDK: server/signature.ts and
server/webhookHandler.ts deleted (lifted verbatim), all four iframe
files replace their inline postMessage handshake with createTolgeeApp()
and the inline createApiClient with createTolgeeAppClient(ctx).
Wired in via npm workspaces at the platform root so dev-plugin
resolves @tolgee/apps-sdk to the source tree without a publish step.
tsconfig.node.json broadened to include server/** and scripts/** so
tsc catches the same diagnostics IDEA already shows.
Pinned @tginternal/client to the exact prerelease version: caret
semver lets npm pick a later tag that ships an empty `webhooks`
interface, silently breaking WebhookPayloadFor<T>.
Scaffold a working Tolgee Apps plugin in one prompt-driven flow.
Wizard (via @clack/prompts) collects:
- App identifier (kebab-case slug → manifest.id + dir name)
- Display name
- Modules to scaffold (multi-select from all 10 module types)
- Webhook events to subscribe to
- Tolgee URL
- Whether to install deps + git init
Generated project lifts dev-plugin's working dev environment:
- Vite + React + TypeScript + Express
- scripts/dev.ts boots a Cloudflare quick tunnel and PATCHes the
install's manifest-url on every restart
- scripts/register.ts handles one-time install
- server/manifest.ts substitutes the live tunnel URL into the
manifest at request time
Uses @tolgee/apps-sdk for the postMessage handshake, typed webhook
dispatcher, signature verifier, and REST client wrapper — no glue
copy-pasted into the generated project.
Template files use `_X` → `.X` renames for dotfiles and
`_package.json` → `package.json` so the template itself doesn't
register as an npm package. Two placeholders (`{{routes}}` and
`{{webhookHandlers}}`) are populated by targeted edits after the
mustache-style substitution pass, since their bodies are computed
from the selected modules / events.
For now the SDK is private, so generated projects must live inside
the platform npm workspace (apps/example-apps/) to resolve
`@tolgee/apps-sdk`. Real npm publishing is a later milestone.
`npm run dev` was unconditionally booting a Cloudflare quick tunnel, even when the registered Tolgee instance lives on localhost — where Tolgee can reach the plugin directly and the tunnel is pure overhead. The dev orchestrator now skips cloudflared when any of these holds: - the install record's `tolgeeUrl` is localhost / 127.0.0.1 / ::1 - `TOLGEE_URL` env var (loaded from .env.local) is localhost - `TOLGEE_DEV_TUNNEL=none` is set (explicit override) In the skip path the tunnel state file is written with `http://localhost:5180` and the manifest URL is PATCHed to the same, so the rest of the dev flow (manifest re-pointing, hot reload of manifest.template.json) keeps working identically. `dev:tunnel` now loads `.env.local` via tsx's --env-file-if-exists flag so a `TOLGEE_URL` set there actually reaches the orchestrator. Same fix applied to the create-tolgee-app template; future generated projects get the behaviour out of the box, and the .env.example calls it out.
Mirror the dev-orchestrator's local-Tolgee detection in register: when the target Tolgee URL is localhost / 127.0.0.1 / ::1, skip cloudflared entirely and use http://localhost:5181/manifest.json (Express direct) as the registered manifest URL. The bug surfaced because Cloudflare quick tunnels go stale — the cloudflared daemon stays alive locally but Cloudflare's edge starts returning 530 after some time. Tolgee then can't fetch the manifest during install, and the POST fails with app_manifest_fetch_failed. For local Tolgee the tunnel was always pure overhead since Tolgee can reach localhost directly. For remote Tolgee the tunnel path is unchanged. Also added an early `fetch(manifestUrl)` sanity check before posting, so the script fails with a clear "is npm run dev running?" message instead of a server-side fetch-failed error. Reproduced + verified against a local Tolgee at localhost:8824: POST with the tunnel URL → 530-wrapped app_manifest_fetch_failed; POST with http://localhost:5181/manifest.json → 200.
VITE_PORT and SERVER_PORT are now read from .env.local (default 5180/5181). Both templates and the dev-plugin reference plugin honour the override across vite.config.ts (via loadEnv), server/config.ts, server/manifest.ts, scripts/dev.ts and scripts/register.ts. Drop a `VITE_PORT=5280` / `SERVER_PORT=5281` pair into a second plugin's .env.local and the two run alongside without colliding on ports — and Tolgee no longer serves whichever vite happened to bind 5180 first when rendering iframes for the other install. Also fix: when the tunnel orchestrator skips cloudflared (local Tolgee), it now parks on a never-firing interval instead of exiting, so `concurrently -k` keeps vite and the server alive. .env.example documents the two new vars and the package.json register script now loads .env.local so the same TOLGEE_URL signal that drives the dev-time tunnel skip also reaches register.
The translation-tools-panel iframe watches the focused cell, fetches the base + focused texts via Tolgee's REST API, then POSTs the pair to the plugin's own /translate-back. The server calls Anthropic to back-translate and classify the result as match / close / mismatch, with a one-line explanation. Results are cached per (baseLang, baseText, sourceLang, text) so re-focusing a cell hits memory instead of the API. Skips on the base language and on missing texts. Returns 503 with a friendly error when ANTHROPIC_API_KEY is unset. ANTHROPIC_MODEL is overridable; defaults to claude-haiku-4-5 for speed, with claude-sonnet-4-6 documented as the quality knob. Routes/glue: server/routes/translateBack.ts, server/config.ts (key + model env), server/index.ts (mount + startup warning), vite.config.ts (proxy /translate-back to Express). Iframe rewritten end-to-end; CSS added; .env.example documents the new vars. To use: drop ANTHROPIC_API_KEY=sk-ant-… into .env.local and restart.
…richer template @tolgee/apps-sdk: - typed AppManifest + decorator types (single source of truth for the builder, template, and hand-written apps) - framework-agnostic server helpers: receiveWebhook, tolgeeAppCorsHeaders, renderManifest, loadTolgeeAppConfig (no Express dependency) @tolgee/apps-dev (new): the `tolgee-app` bin (dev tunnel + manifest repoint, browser register), extracted from the generated template's scripts/ so apps no longer copy ~490 LOC of toolchain. create-tolgee-app: - scope selection, a type:link action example, and a real /decorators demo with a dynamic:true action (closes manifest-capability gaps) - headless mode: `--yes` plus --modules/--scopes/--webhooks/--tolgee-url/ --vite-port/--server-port flags - writes a ready-to-run .env.local with an auto-detected free port pair; ports also promptable in the wizard; URL input rejects duplicate/invalid schemes - template consumes the SDK helpers and reads the webhook secret from the install record instead of requiring it in env
- @tolgee/apps-sdk gains a real tsup build (ESM + d.ts for ., /server, /browser); exports/main/types point at dist, ships dist only, un-private. - @tolgee/apps-dev + create-tolgee-app un-private with publishConfig. - create-tolgee-app injects the published @tolgee/* version into generated projects (its own version, or `*` when run unpublished from the repo). - Lerna 6 (fixed versioning, npm workspaces) for lockstep alpha publishing; example-apps stay private so they're never published. `npm run build:apps` builds all in topo order. Lerna 8's nx-based discovery finds 0 packages in this git-worktree layout, so we pin v6.
- apps-sdk / apps-dev / create-tolgee-app published to npm under the `alpha` dist-tag at 0.0.1-alpha.0; lerna.json + package.json versions reflect it so the next publish increments to alpha.1. - create-tolgee-app now decides the generated app's @tolgee/* dep version by workspace-sibling presence (in-repo → `*`, published → own version) instead of the version number, so it keeps linking the workspace after release bumps.
- create-tolgee-app now scaffolds a CLAUDE.md that guides AI-assisted development: typesafety workflow (verify with `npm run typecheck` after every change, prefer the typed SDK), and where to find docs, SDK types, and the example apps. - add a `typecheck` script (tsc -b) and a `pull-context` script that snapshots the SDK source, example apps, and docs into a gitignored .context/ (shallow + sparse, ~1 MB). - default the scaffolder's Tolgee URL to https://apps.preview.tolgee.io.
…eachability `tolgee-app register` against a remote Tolgee always opened a second quick tunnel and fetched it once before it was edge-routable, failing even though `tolgee-app dev` had a live tunnel serving the manifest. register now reuses dev's tunnel (read from .tolgee-dev/tunnel.json, gated by a manifest reachability check so stale URLs fall back), and verifyManifestReachable retries up to 5×/1s for the cold-start (no dev) case. - lib.ts: add readTunnelState() (read counterpart to write/clearTunnelState). - register.ts: reusableTunnel() helper; remote branch reuses-or-starts; retry/backoff.
The project-dashboard-page app iframe was capped at maxWidth 1200 with a fixed min-height of 600px. Make it full-bleed: maxWidth="max" + the BaseView `stretch` row so the content fills the viewport height, and the iframe is width/height 100%.
…oject menu The tools-panel header and project side-menu rendered whatever icon each panel / menu item passed, so Tolgee app icons (18-20px) came out smaller than the native icons (24px) next to them. Give PanelHeader and SideMenuItem a single fixed icon slot (24px) that normalizes every icon — native and app alike — and drop the now redundant per-call sizes from the app-icon usages.
The AuthenticationFilter constructor gained main's appTokenService/keyGenerator/ permissionService alongside our appInstallService/appEnablementService, so the two security filter tests no longer matched it positionally. Pass arguments by name (and add the missing service mocks) so the tests are robust to ordering.
Picks up main's new endpoints (per-language QA on/off, announcement banner, language selector) alongside the apps endpoints, so the webapp type-checks.
…t format After the rebase the filter's JWT path consults appTokenService; the unstubbed mock returned null and NPEd on claims.installId. Stub validateToken to throw for non-app tokens (matching production, so the filter falls back to user JWT auth). Also apply ktlint formatting to the apps controller tests.
diffChangeLog drift after rebase: app_install.manifest_json is nullable in the entity (installs are created before the manifest is fetched), app_install_webhook _subscription.event is a string, and the two collection tables have no surrogate PK. Add the generated changesets so the changelog matches the entity model.
…languages Adds a new manifest module shown in the translations tools panel when no cell is selected; it receives the view's currently selected language tags (selection.selectedLanguages, updated as the user changes the language selector). - backend: new module in AppManifestModules (reuses TranslationToolsPanelModule) - sdk: manifest type + selectedLanguages on TolgeeAppSelection, wired through the init / selection-changed messages - webapp: render app empty panels in the no-selection tools panel, open the side panel by default when an app contributes one, expand plugin panels by default, drop the hint title, plugins before shortcuts, keep the close button visible - create-tolgee-app: scaffold the new module (registry + manifest + template stub) - dev-plugin example: panel that fetches and lists the selected languages
Hands every app iframe the host's resolved theme (mode + a curated color palette) at init and on every light/dark toggle, so plugins can match Tolgee. - sdk: TolgeeAppTheme on context, onThemeChanged(), and applyTolgeeTheme() which exposes the palette as --tg-color-* CSS vars + sets data-tg-theme/color-scheme; init now also delivers the initial selection/theme to handlers registered before init (so onThemeChanged fires on first load, not only on change) - webapp: useAppIframeMessaging sends theme in init + a theme-changed message; ProjectAppPageView folded onto the shared hook so the dashboard page is themed too - create-tolgee-app: module stubs apply the theme; base CSS keys off --tg-color-* - examples: dev-plugin + back-translate fully themed via the SDK vars
… URLs Mirrors tolgee.webhook.allow-local-addresses: a config flag that lets app manifest URLs target otherwise-blocked local/private addresses (off by default). AppManifestFetcher passes it to UrlSecurity.validateUrl.
Adds read-only endpoints under /v2/projects/{projectId}/ai-match-stats that
report how much reviewers changed AI/MT output before approving it
(word-level edit-distance similarity, 0-100; 100 = approved verbatim),
broken down into a project summary, per language, and per prompt + prompt
version.
The score per reviewed translation is reconstructed from the activity log
(the AI output the reviewer saw vs. the final reviewed text) and stored in a
materialized translation_ai_match table. The table is maintained on demand:
each read advances a per-project revision watermark and recomputes only the
translations touched since, set-based (bulk reads + bulk upsert/delete). The
backfill is bounded and resumable, so a first read on a very large project
never runs unbounded; the summary exposes an upToDate flag for that case.
Per-prompt rows attribute each translation to the prompt version that was
live when it was produced (derived from the prompt's own activity history),
so a prompt's accuracy before vs. after an edit can be compared.
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…jancizmar/ai-match-stats # Conflicts: # backend/data/src/main/resources/db/changelog/schema.xml
The app iframe sandbox omitted allow-popups, so target="_blank" links and window.open from inside an app were silently blocked. Add allow-popups (so the popup opens) and allow-popups-to-escape-sandbox (so the opened tab is a normal, non-sandboxed page) to all app iframe surfaces: project dashboard page, modal, tools panel, and key-edit tab.
# Conflicts: # backend/data/src/main/resources/db/changelog/schema.xml
Sandboxed app iframes couldn't navigate the top-level window, so target="_top" links (and breaking out to a Tolgee route) were blocked. Add allow-top-navigation-by-user-activation to all app iframe surfaces — permits navigation on a real user gesture while still blocking silent programmatic redirects of the host page.
…ancizmar/ai-match-stats
Contributor
|
This PR is stale because it has been open for 30 days with no activity. |
Contributor
|
This PR was closed because it has been inactive for 14 days since being marked as stale. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Read-only endpoints under
GET /v2/projects/{projectId}/ai-match-statsthat answer: when a reviewer approved an AI/MT translation, how much did they change the AI's output? (word-level edit-distance similarity, 0–100; 100 = approved verbatim). Three views:…/ai-match-stats— project summary (score buckets b100/b9990/b8980/b7970/bno, reviewed/not-reviewed totals,avgMatchScore,reviewedPct)…/ai-match-stats/languages— per-language breakdown…/ai-match-stats/prompts— per prompt and prompt versionScope:
translations.view, API-access enabled, default branch.How it works
auto/mtProvider/promptId), compared to the final reviewed text, and scored word-level (Levenshtein over tokens; an exact match is the only 100). Everything is word-weighted.translation_ai_matchtable, maintained on demand, watermark-gated: each read advances a per-projectactivity_revisionwatermark and recomputes only the translations touched since — set-based (batched reads + bulkINSERT … ON CONFLICT/ bulk delete), so no N+1 and no per-row writes.upToDateto signal catch-up.Tests
Notes / open question
notReviewedper prompt is reported at the prompt level (on the newest version row), since an unreviewed cell has no review timestamp to pin a version to.DISABLEDAI cells are currently excluded from the not-reviewed count.