Merge branch 'main' into UIRedesignStaging

This commit is contained in:
EthanHealy01
2026-04-23 13:05:14 +01:00
committed by GitHub
12 changed files with 314 additions and 206 deletions
+18 -56
View File
@@ -1,8 +1,8 @@
name: Update Package Manager Manifests
on:
# release:
# types: [released]
release:
types: [released]
workflow_dispatch:
inputs:
version:
@@ -75,7 +75,7 @@ jobs:
echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT"
echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT"
update-homebrew:
update-homebrew-and-scoop:
needs: get-release-info
runs-on: ubuntu-latest
permissions:
@@ -86,22 +86,21 @@ jobs:
with:
egress-policy: audit
- name: Checkout homebrew tap
- name: Checkout homebrew-stirling-pdf tap (also hosts Scoop bucket)
uses: actions/checkout@v4
with:
repository: Stirling-Tools/homebrew-stirling-pdf
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap
path: tap
- name: Update cask (stirling-pdf.rb)
- name: Update Homebrew cask (Casks/stirling-pdf.rb)
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
ARM64_SHA: ${{ needs.get-release-info.outputs.dmg_arm64_sha256 }}
X64_SHA: ${{ needs.get-release-info.outputs.dmg_x86_64_sha256 }}
run: |
CASK="homebrew-tap/Casks/stirling-pdf.rb"
CASK="tap/Casks/stirling-pdf.rb"
sed -i "s/version \".*\"/version \"${VERSION}\"/" "$CASK"
# Update ARM64 sha256 (line following on_arm block)
awk -v arm="$ARM64_SHA" -v x64="$X64_SHA" '
/on_arm/ { in_arm=1 }
/on_intel/ { in_arm=0; in_intel=1 }
@@ -111,87 +110,50 @@ jobs:
{ print }
' "$CASK" > tmp && mv tmp "$CASK"
- name: Update formula (stirling-pdf-server.rb)
- name: Update Homebrew formula (Formula/stirling-pdf-server.rb)
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
run: |
FORMULA="homebrew-tap/Formula/stirling-pdf-server.rb"
FORMULA="tap/Formula/stirling-pdf-server.rb"
sed -i "s/version \".*\"/version \"${VERSION}\"/" "$FORMULA"
sed -i "s/sha256 \".*\"/sha256 \"${JAR_SHA}\"/" "$FORMULA"
- name: Show homebrew tap diff (for dry-run visibility)
working-directory: homebrew-tap
run: |
echo "--- diff --stat ---"
git diff --stat
echo "--- full diff ---"
git diff
- name: Commit and push homebrew tap updates
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
working-directory: homebrew-tap
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/stirling-pdf.rb Formula/stirling-pdf-server.rb
git diff --cached --quiet && echo "No changes" && exit 0
git commit -m "chore: bump Stirling-PDF to v${{ needs.get-release-info.outputs.version }}"
git push
update-scoop:
needs: get-release-info
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@v2
with:
egress-policy: audit
- name: Checkout Scoop bucket (shared with Homebrew tap)
uses: actions/checkout@v4
with:
repository: Stirling-Tools/homebrew-stirling-pdf
token: ${{ secrets.SCOOP_BUCKET_TOKEN }}
path: scoop-bucket
- name: Update stirling-pdf.json
- name: Update Scoop stirling-pdf.json
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
MSI_SHA: ${{ needs.get-release-info.outputs.msi_sha256 }}
run: |
MANIFEST="scoop-bucket/scoop/stirling-pdf.json"
MANIFEST="tap/scoop/stirling-pdf.json"
jq --arg v "$VERSION" --arg h "$MSI_SHA" \
'.version = $v | .architecture["64bit"].url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-windows-x86_64.msi" | .architecture["64bit"].hash = $h' \
"$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST"
- name: Update stirling-pdf-server.json
- name: Update Scoop stirling-pdf-server.json
env:
VERSION: ${{ needs.get-release-info.outputs.version }}
JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }}
run: |
MANIFEST="scoop-bucket/scoop/stirling-pdf-server.json"
MANIFEST="tap/scoop/stirling-pdf-server.json"
jq --arg v "$VERSION" --arg h "$JAR_SHA" \
'.version = $v | .url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-with-login.jar" | .hash = $h' \
"$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST"
- name: Show Scoop bucket diff (for dry-run visibility)
working-directory: scoop-bucket
- name: Show tap diff (for dry-run visibility)
working-directory: tap
run: |
echo "--- diff --stat ---"
git diff --stat
echo "--- full diff ---"
git diff
- name: Commit and push Scoop bucket updates
- name: Commit and push all tap updates
if: ${{ github.event_name == 'release' || inputs.dry_run == false }}
working-directory: scoop-bucket
working-directory: tap
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add scoop/stirling-pdf.json scoop/stirling-pdf-server.json
git add Casks/stirling-pdf.rb Formula/stirling-pdf-server.rb scoop/stirling-pdf.json scoop/stirling-pdf-server.json
git diff --cached --quiet && echo "No changes" && exit 0
git commit -m "chore: bump Stirling-PDF to v${{ needs.get-release-info.outputs.version }}"
git push
+79 -8
View File
@@ -43,15 +43,86 @@ After modifying any files in the project, you must run the relevant `task check`
### Security Mode Development
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
### Python Development
Development for the AI engine happens in the `engine/` folder. The frontend calls the Python via Java as a proxy.
### Python Development (AI Engine)
- Follow the engine-specific guidance in [engine/AGENTS.md](engine/AGENTS.md) for Python architecture, code style, and AI usage.
- Use Task commands from the repo root:
- `task engine:check` — lint, type-check, test
- `task engine:fix` — auto-fix linting and formatting
- `task engine:install` — install dependencies
- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.
The engine is a Python reasoning service for Stirling: it plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. The frontend calls the Python engine via Java as a proxy.
#### Python Commands
All engine commands run from the repo root using Task:
- `task engine:check` — run all checks (typecheck + lint + format-check + test)
- `task engine:fix` — auto-fix lint + formatting
- `task engine:install` — install Python dependencies via uv
- `task engine:dev` — start FastAPI with hot reload (localhost:5001)
- `task engine:test` — run pytest
- `task engine:lint` — run ruff linting
- `task engine:typecheck` — run pyright
- `task engine:format` — format code with ruff
- `task engine:tool-models` — generate `tool_models.py` from the Java OpenAPI spec
The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`.
#### Python Code Style
- Keep `task engine:check` passing.
- Use modern Python when it improves clarity.
- Prefer explicit names to cleverness.
- Avoid nested functions and nested classes unless the language construct requires them.
- Prefer composition to inheritance when combining concepts.
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
- Add comments sparingly and only when they explain non-obvious intent.
#### Python Typing and Models
- Deserialize into Pydantic models as early as possible.
- Serialize from Pydantic models as late as possible.
- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.
- Avoid `Any` wherever possible.
- Avoid `cast()` wherever possible (reconsider the structure first).
- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.
- Do not use string literals for any type annotations, including `cast()`.
#### Python Configuration
- Keep application-owned configuration in `stirling.config`.
- Only add `STIRLING_*` environment variables that the engine itself truly owns.
- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.
- Let `pydantic-ai` own provider authentication configuration when possible.
#### Python Architecture
**Package roles:**
- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.
- `stirling.models`: shared model primitives and generated tool models.
- `stirling.agents`: reasoning modules for individual capabilities.
- `stirling.api`: HTTP layer, dependency access, and app startup wiring.
- `stirling.services`: shared runtime and non-AI infrastructure.
- `stirling.config`: application-owned settings.
**Source of truth:**
- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.
- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.
- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.
- If a tool ID must match a parameter model, validate that relationship explicitly in code.
**Boundaries:**
- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.
- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.
- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.
- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.
#### Python AI Usage
- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.
- Use AI for reasoning-heavy outputs, not deterministic glue.
- Do not ask the model to invent data that Python can derive safely.
- Do not fabricate fallback user-facing copy in code to hide incomplete model output.
- AI output schemas should be impossible to instantiate incorrectly.
- Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.
- Prefer Python to derive deterministic follow-up structure from a valid AI result.
- Use `NativeOutput(...)` for structured model outputs.
- Use `ToolOutput(...)` when the model should select and call delegate functions.
#### Python Testing
- Test contracts directly.
- Test agents directly where behaviour matters.
- Test API routes as thin integration points.
- Prefer dependency overrides or startup-state seams to monkeypatching random globals.
### Frontend Development
- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080
+1 -1
View File
@@ -78,7 +78,7 @@ springBoot {
allprojects {
group = 'stirling.software'
version = '2.9.2'
version = '2.10.0'
configurations.configureEach {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
-90
View File
@@ -1,90 +0,0 @@
# Stirling AI Engine Guide
This file is for AI agents working in `engine/`.
The engine is a Python reasoning service for Stirling. It plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value.
## Commands
All engine commands can be run from the repository root using Task:
- `task engine:check` — run all checks (typecheck + lint + format-check + test)
- `task engine:fix` — auto-fix lint + formatting
- `task engine:install` — install Python dependencies via uv
- `task engine:dev` — start FastAPI with hot reload (localhost:5001)
- `task engine:test` — run pytest
- `task engine:lint` — run ruff linting
- `task engine:typecheck` — run pyright
- `task engine:format` — format code with ruff
- `task engine:tool-models` — generate tool_models.py from Java OpenAPI spec
## Code Style
- Keep `task engine:check` passing.
- Use modern Python when it improves clarity.
- Prefer explicit names to cleverness.
- Avoid nested functions and nested classes unless the language construct requires them.
- Prefer composition to inheritance when combining concepts.
- Avoid speculative abstractions. Add a layer only when it removes real duplication or clarifies lifecycle.
- Add comments sparingly and only when they explain non-obvious intent.
### Typing and Models
- Deserialize into Pydantic models as early as possible.
- Serialize from Pydantic models as late as possible.
- Do not pass raw `dict[str, Any]` or `dict[str, object]` across important boundaries when a typed model can exist instead.
- Avoid `Any` wherever possible.
- Avoid `cast()` wherever possible (reconsider the structure first).
- All shared models should subclass `stirling.models.ApiModel` so the service behaves consistently.
- Do not use string literals for any type annotations, including `cast()`.
### Configuration
- Keep application-owned configuration in `stirling.config`.
- Only add `STIRLING_*` environment variables that the engine itself truly owns.
- Do not mirror third-party provider environment variables unless the engine is actually interpreting them.
- Let `pydantic-ai` own provider authentication configuration when possible.
## Architecture
### Package Roles
- `stirling.contracts`: request/response models and shared typed workflow contracts. If a shape crosses a module or service boundary, it probably belongs here.
- `stirling.models`: shared model primitives and generated tool models.
- `stirling.agents`: reasoning modules for individual capabilities.
- `stirling.api`: HTTP layer, dependency access, and app startup wiring.
- `stirling.services`: shared runtime and non-AI infrastructure.
- `stirling.config`: application-owned settings.
### Source Of Truth
- `stirling.models.tool_models` is the source of truth for operation IDs and parameter models.
- Do not duplicate operation lists if they can be derived from `tool_models.OPERATIONS`.
- Do not hand-maintain parallel parameter schemas when the generated tool models already define them.
- If a tool ID must match a parameter model, validate that relationship explicitly in code.
### Boundaries
- Keep the API layer thin. Route modules should bind requests, resolve dependencies, and call agents or services. They should not contain business logic.
- Keep agents focused on one reasoning domain. They should not own FastAPI routing, persistence, or execution of Stirling operations.
- Build long-lived runtime objects centrally at startup when possible rather than reconstructing heavy AI objects per request.
- If an agent delegates to another agent, the delegated agent should remain the source of truth for its own domain output.
## AI Usage
- The system must work with any AI, including self-hosted models. We require that the models support structured outputs, but should minimise model-specific code beyond that.
- Use AI for reasoning-heavy outputs, not deterministic glue.
- Do not ask the model to invent data that Python can derive safely.
- Do not fabricate fallback user-facing copy in code to hide incomplete model output.
- AI output schemas should be impossible to instantiate incorrectly.
- Do not require the model to keep separate structures in sync. For example, instead of generating two lists which must be the same length, generate one list of a model containing the same data.
- Prefer Python to derive deterministic follow-up structure from a valid AI result.
- Use `NativeOutput(...)` for structured model outputs.
- Use `ToolOutput(...)` when the model should select and call delegate functions.
## Testing
- Test contracts directly.
- Test agents directly where behaviour matters.
- Test API routes as thin integration points.
- Prefer dependency overrides or startup-state seams to monkeypatching random globals.
+1 -1
View File
@@ -80,7 +80,7 @@
"web-vitals": "^5.1.0"
},
"scripts": {
"update:minor": "npm outdated || npm update --before=$(date -v-7d +%Y-%m-%d) && (npm audit fix --before=$(date -v-7d +%Y-%m-%d) || true) && npm test",
"update:minor": "node scripts/update-minor.js",
"update:major": "npx npm-check-updates -u && npm install",
"update:interactive": "npx npm-check-updates -i",
"update:minor-strict": "npx npm-check-updates -u --target minor && npm install"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/**
* Cross-platform update:minor script
* Calculates date from 7 days ago and runs npm update/audit with that date
*/
const { spawn } = require("child_process");
// Calculate date from 7 days ago in YYYY-MM-DD format
const date = new Date();
date.setDate(date.getDate() - 7);
const beforeDate = date.toISOString().split("T")[0];
console.log(`Updating packages modified before: ${beforeDate}`);
let lastExitCode = 0;
// Run npm outdated first
const outdated = spawn("npm", ["outdated"], { stdio: "inherit", shell: true });
outdated.on("close", (_code) => {
// npm outdated returns exit code 1 if updates are available, so we ignore it
// Run npm update with before date
const update = spawn("npm", ["update", `--before=${beforeDate}`], {
stdio: "inherit",
shell: true,
});
update.on("close", (updateCode) => {
// Track update failures
if (updateCode !== 0) {
lastExitCode = updateCode;
}
// Run npm audit fix with before date
const audit = spawn("npm", ["audit", "fix", `--before=${beforeDate}`], {
stdio: "inherit",
shell: true,
});
audit.on("close", (auditCode) => {
// Track audit failures (but don't override critical update failures)
if (auditCode !== 0 && lastExitCode === 0) {
lastExitCode = auditCode;
}
// Update complete - report with tracked exit code
console.log("\nPackage update complete!");
process.exit(lastExitCode);
});
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Stirling-PDF",
"version": "2.9.2",
"version": "2.10.0",
"identifier": "stirling.pdf.dev",
"build": {
"frontendDist": "../dist",
@@ -3,6 +3,7 @@ import { StirlingFileStub } from "@app/types/fileContext";
import { useIndexedDB } from "@app/contexts/IndexedDBContext";
import { generateThumbnailForFile } from "@app/utils/thumbnailUtils";
import { FileId } from "@app/types/fileContext";
import { useFileManagement } from "@app/contexts/FileContext";
/**
* Hook for IndexedDB-aware thumbnail loading
@@ -17,6 +18,7 @@ export function useIndexedDBThumbnail(
const [thumb, setThumb] = useState<string | null>(null);
const [generating, setGenerating] = useState(false);
const indexedDB = useIndexedDB();
const { updateStirlingFileStub } = useFileManagement();
useEffect(() => {
let cancelled = false;
@@ -27,58 +29,59 @@ export function useIndexedDBThumbnail(
return;
}
// First priority: use stored thumbnail
// Tier 1: stored thumbnail on the stub.
if (file.thumbnailUrl) {
setThumb(file.thumbnailUrl);
return;
}
// Second priority: generate thumbnail for files under 100MB
if (file.size < 100 * 1024 * 1024 && !generating) {
setGenerating(true);
try {
let fileObject: File;
// Try to load file from IndexedDB using new context
if (file.id && indexedDB) {
const loadedFile = await indexedDB.loadFile(file.id as FileId);
if (!loadedFile) {
throw new Error("File not found in IndexedDB");
}
fileObject = loadedFile;
} else {
throw new Error(
"File ID not available or IndexedDB context not available",
);
}
// Use the universal thumbnail generator
const thumbnail = await generateThumbnailForFile(fileObject);
if (!cancelled) {
setThumb(thumbnail);
// Save thumbnail to IndexedDB for persistence
if (file.id && indexedDB && thumbnail) {
try {
await indexedDB.updateThumbnail(file.id as FileId, thumbnail);
} catch (error) {
console.warn("Failed to save thumbnail to IndexedDB:", error);
}
}
}
} catch (error) {
console.warn(
"Failed to generate thumbnail for file",
file.name,
error,
);
if (!cancelled) setThumb(null);
} finally {
if (!cancelled) setGenerating(false);
}
} else {
// Large files - no thumbnail
// >=100MB files are skipped entirely — no thumbnail.
if (file.size >= 100 * 1024 * 1024) {
setThumb(null);
return;
}
// Tier 2: generate on demand from the File bytes in IndexedDB.
// Re-entry guard is handled by the effect's cleanup/cancelled pattern —
// `generating` is NOT in the deps, so setGenerating() does not trigger
// the effect to re-run and cancel itself mid-flight.
setGenerating(true);
try {
if (!file.id || !indexedDB) {
throw new Error(
`missing prerequisite fileId=${file.id} indexedDB=${Boolean(indexedDB)}`,
);
}
const loadedFile = await indexedDB.loadFile(file.id as FileId);
if (!loadedFile) {
throw new Error("not in IndexedDB (likely remote-only stub)");
}
const thumbnail = await generateThumbnailForFile(loadedFile);
if (cancelled) return;
setThumb(thumbnail);
if (file.id && indexedDB && thumbnail) {
try {
await indexedDB.updateThumbnail(file.id as FileId, thumbnail);
// Also sync the in-memory stub so subsequent re-mounts hit tier 1
// instead of regenerating. IndexedDB persistence alone only helps
// the next page load; the current session reads file.thumbnailUrl
// from the FileContext stub.
updateStirlingFileStub(file.id as FileId, {
thumbnailUrl: thumbnail,
});
} catch (error) {
console.warn("Failed to persist thumbnail:", error);
}
}
} catch (error) {
console.warn("Failed to generate thumbnail for file", file.name, error);
if (!cancelled) setThumb(null);
} finally {
if (!cancelled) setGenerating(false);
}
}
@@ -86,7 +89,11 @@ export function useIndexedDBThumbnail(
return () => {
cancelled = true;
};
}, [file, file?.thumbnailUrl, file?.id, indexedDB, generating]);
// `generating` is intentionally NOT in the deps — it's an internal flag
// set by this effect, and including it caused the effect to cancel
// itself mid-flight (orphaning the render and leaving generating=true
// stuck forever).
}, [file, file?.thumbnailUrl, file?.id, indexedDB, updateStirlingFileStub]);
return { thumbnail: thumb, isGenerating: generating };
}
@@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
const BASE_NO_LOGIN_CONFIG: AppConfig = {
enableAnalytics: true,
appVersion: "2.9.2",
appVersion: "2.10.0",
serverCertificateEnabled: false,
enableAlphaFunctionality: false,
serverPort: 8080,
@@ -0,0 +1,99 @@
// Browser page translators (Edge, Chrome, extensions) wrap text nodes in
// injected <font> elements, reparenting nodes React is holding. React's commit
// phase then throws NotFoundError on removeChild/insertBefore and the
// ErrorBoundary unmounts the app. https://github.com/facebook/react/issues/11538
//
// We watch for translator fingerprints (Google Translate's translated-* class
// on <html>, or any injected <font>) and install guards on Node.prototype only
// once one appears, so native DOM semantics are preserved when no translator
// is active.
declare global {
interface Node {
__stirlingTranslatorPatched?: boolean;
}
}
let patchApplied = false;
function isGoogleTranslateActive(): boolean {
const cls = document.documentElement.classList;
return cls.contains("translated-ltr") || cls.contains("translated-rtl");
}
function applyDomPatch(trigger: string): void {
if (patchApplied) return;
if (typeof Node === "undefined" || !Node.prototype) return;
if (Node.prototype.__stirlingTranslatorPatched) return;
patchApplied = true;
Node.prototype.__stirlingTranslatorPatched = true;
console.warn(
`[dom-patch] Browser page translator detected (${trigger}). ` +
"Installing removeChild/insertBefore guards to prevent React crashes. " +
"The UI may show minor glitches while the translator is active.",
);
const originalRemoveChild = Node.prototype.removeChild;
Node.prototype.removeChild = function patchedRemoveChild<T extends Node>(
this: Node,
child: T,
): T {
if (child.parentNode !== this) return child;
return originalRemoveChild.call(this, child) as T;
} as typeof Node.prototype.removeChild;
const originalInsertBefore = Node.prototype.insertBefore;
Node.prototype.insertBefore = function patchedInsertBefore<T extends Node>(
this: Node,
newNode: T,
referenceNode: Node | null,
): T {
if (referenceNode && referenceNode.parentNode !== this) return newNode;
return originalInsertBefore.call(this, newNode, referenceNode) as T;
} as typeof Node.prototype.insertBefore;
}
export function armTranslatorDetector(): void {
if (typeof window === "undefined" || typeof MutationObserver === "undefined")
return;
if (typeof document === "undefined" || !document.documentElement) return;
// Edge case: class already set (e.g., bfcache restore).
if (isGoogleTranslateActive()) {
applyDomPatch("html class was already translated-* on arm");
return;
}
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (
m.type === "attributes" &&
m.target === document.documentElement &&
isGoogleTranslateActive()
) {
applyDomPatch("<html> translated-* class appeared");
observer.disconnect();
return;
}
if (m.type === "childList") {
for (const n of m.addedNodes) {
if (n.nodeName === "FONT") {
applyDomPatch("<font> element injected into DOM");
observer.disconnect();
return;
}
}
}
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
childList: true,
subtree: true,
});
}
armTranslatorDetector();
+5
View File
@@ -1,3 +1,8 @@
// Must be imported before React so the DOM-prototype patch is installed
// before React's commit phase runs. Prevents browser page translators
// (Edge / Google Translate / extensions) from crashing the app via
// parent-mismatch DOMExceptions. See the module for details.
import "@app/utils/patchDomForTranslators";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "../vite-env.d.ts"; // eslint-disable-line no-restricted-imports -- Outside app paths
@@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
const BASE_NO_LOGIN_CONFIG: AppConfig = {
enableAnalytics: true,
appVersion: "2.9.2",
appVersion: "2.10.0",
serverCertificateEnabled: false,
enableAlphaFunctionality: false,
enableDesktopInstallSlide: true,