Merge branch 'main' into UIRedesignStaging
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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();
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user