remote.origin.promisor=true causes git to fetch missing blobs from the
remote during the commit connectivity check, failing if the server is
slow or unavailable. --filter=blob:none on fetch works independently of
promisor config so trees-only fetching is preserved. Existing cached
repos have the config unset on next use.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getAllDocs reads from MongoDB (docstore) which may lag behind the
in-memory document-updater. Without flushing first, edits made in
the Overleaf editor are silently skipped — the pushed file content
matches the pre-edit version so git sees no change and doesn't update
the remote. This is the same flush the zip download uses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
git add -A with a pathspec can silently skip modified files when git's
cached-tree extension still holds a valid tree hash for the subPath
directory (e.g. mtime unchanged after a pull checkout). Replacing with
git rm --cached + git add forces a full rescan regardless of cache state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--depth=1 limits fetch to only the latest commit's tree objects instead
of the full history, dramatically reducing fetch time on repos with
many commits. A git ls-remote check before fetching skips the fetch
entirely when local HEAD already matches remote HEAD, which is the
common case after a push with no external changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
git read-tree only updated the index, leaving HEAD on our previous
local commit. The push commit's diff then included all remote changes
outside subPath as if we made them. reset --mixed also moves HEAD to
FETCH_HEAD, so the new commit's parent is the remote HEAD and the diff
shows only the managed area changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull now reuses the persistent repo cache with --filter=blob:none and
checks out only the configured subPath, so only relevant blobs are
downloaded. Push likewise fetches trees-only and uses git read-tree to
update the index without touching the working tree, preserving files
outside the managed area without downloading their content.
Upserts on pull are now parallelised (p-limit 5) to speed up import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of both issues:
- OOM on Gitea: fresh git init every push → all blobs are "new" →
100% of content sent in every packfile, even images that didn't change
- Slow repush with no changes: fetch+rewrite+add all files O(n) every time
Fix: persist the git working directory at
<data-root>/git-sync-cache/<projectId>/
between pushes. On subsequent pushes:
- git fetch only downloads new commits (delta, not the full tree)
- git add only stages files whose content changed
- git push only sends new/changed blob objects
The first push still sends all objects (unavoidable), but every
subsequent push is proportional to what actually changed.
Remote URL changes (new token) are handled by git remote set-url.
The cached repo is recreated from scratch if it becomes invalid.
Also reverts the binary file exclusion added as a workaround — all
project files (including images) are now included, as originally
intended.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Binary files (images, PDFs, etc.) from getAllFiles are now filtered
through isTextFile before being written to the git working tree.
Text-typed uploaded files (.bib, .cls, .sty) are still pushed; binary
assets are excluded.
Git packs binary files uncompressed and the resulting packfile is
processed fully in memory by the receiving server (git-receive-pack).
Even a handful of images is enough to OOM a memory-constrained Gitea
pod. Binary assets belong in Git LFS or object storage, not in a
regular git object store.
The widget description now explicitly states that binary files are
excluded from the push.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Branch:
- New "Branch" text field (default: main) stored in gitSyncBranch on
the project. Used for git fetch, symbolic-ref, and push target.
Validated server-side: alphanumeric + / _ . - only, no ..
First-push confirmation:
- When pushFiles is enabled and the user hasn't confirmed for the
current subPath, clicking "Push now" shows an inline warning:
"The <subPath> directory will be completely replaced …"
with "Yes, push and replace" / "Cancel" buttons.
- Confirmation is stored in localStorage keyed by projectId+subPath,
so it's shown again if the subPath is changed.
- Auto-push bypasses the dialog (user already opted in explicitly).
PDF-only replace:
- Already correct with the fetch-first approach: only the specific
file at pdfPath is written; other files in the same directory
(e.g. output/old.pdf) are preserved from the fetched remote state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the push did git-init from scratch and force-pushed, wiping
everything in the remote branch that wasn't part of the Verso project.
New approach:
1. git init + remote add origin + fetch --depth 1 origin/main
2. reset --hard FETCH_HEAD (skipped silently on first push to empty repo)
3. Clear only the managed area (subPath dir, or all non-.git files if
no subPath is configured) so Verso deletions are still reflected
4. Write Verso docs and binary files
5. Write PDF at repo-root-relative pdfPath
6. git add -A + commit + push --force origin HEAD:main
With a subPath configured, everything outside the subPath is fetched
from the remote and preserved in the new commit unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gitSyncEnabled is now false for non-owners, hiding the rail tab
- gitRemote (and all other git sync config) is served as empty string
to non-owners, preventing auth token leakage via meta tags to
collaborators and anonymous token users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pdfPath is now joined against tmpDir (repo root) instead of fileRoot
(subPath dir), so the PDF can be placed anywhere in the repo
independently of where the project files land.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zz.clsiServerId().optional() accepts undefined but rejects null.
?? null was coercing an absent clsiServerId into null, failing Zod
validation with "expected string, received null".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two new boolean fields on the project (gitSyncPushFiles, gitSyncPushPdf,
both default true) let users control what gets pushed independently:
- "Push project files" switch — skip all docs/binary files when off
- "Push compiled PDF" switch — grayed out when no pdfPath is set
The push button and auto-push are disabled when both switches would
result in nothing being pushed. Config is stored in MongoDB so settings
persist per-project.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In single-server CE deployments (Verso included) the CLSI does not
return a clsiServerId, so it was always undefined. The push condition
checked pdfPath && pdfBuildId && pdfClsiServerId && userId, meaning the
PDF was silently skipped every time in practice.
clsiServerId is optional in getOutputFileURL (single-server deployments
work without it), so only require pdfPath and pdfBuildId. Also remove
the inner try/catch so PDF fetch errors surface to the user instead of
being swallowed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
req.session.user is not the right path in Overleaf/Verso — the session
uses passport's structure. SessionManager.getLoggedInUserId(req.session)
is the standard way all other controllers access the user id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move userId + req.body reads inside try/catch in pushToGit and
pullFromGit so any synchronous throw returns JSON, not an HTML 500
(which made err.data undefined and showed only "Internal Server Error")
- Add extractError() helper in widget that tries err.data.error first,
then err.message, then String(err) — surfaces the actual git failure
message from the server log
- Change rail icon from 'merge' (not in unfilled-symbols list) to
'autorenew' (sync arrows, already in the list) — fixes text showing
when panel is closed and wrong icon when open
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /project/:id/git-sync/pull clones the configured remote at depth 1,
walks all files under the configured subPath, and upserts each into the
Verso project using upsertDocWithPath (text) or upsertFileWithPath
(binary), with full folder creation via mkdirp. The .git directory is
skipped. Pull is additive/update-only — no Verso entities are deleted.
Text vs binary classification uses Settings.textExtensions (same list
the editor uses for file uploads), so .typ, .tex, .md, .yml etc. all
become editable docs while images and PDFs stay as files.
Frontend: "Pull from git" button added alongside "Push now".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- getConfig was outside try/catch so errors returned as HTML 500 instead
of JSON, hiding the real message in the frontend
- git init -b main requires git ≥ 2.28; replaced with git init +
symbolic-ref to support older git versions in the base image
- apt-get update before git install to avoid stale package list failures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Icon: change rail tab from integration_instructions to merge
- Auto-push on compile: toggle stored in localStorage, watches compile
context; when compiling goes true→false with no error, triggers a push
automatically (including the latest build's PDF if configured)
- PDF destination path: new gitSyncPdfPath field; backend fetches the
compiled PDF from CLSI (buildId + clsiServerId passed from frontend)
and writes it at the configured path in the repo; silently skipped if
no recent compile or field is blank
- Push now always sends current buildId/clsiServerId so PDF is included
without needing a separate save step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Project files can now be pushed into a subfolder of the target repo
rather than always going to the root. The path is sanitised on the
backend (strips leading/trailing slashes, rejects traversal with ..).
An empty value (default) keeps the existing root behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Git Sync" section in the Integrations rail panel that lets
project owners configure an HTTPS remote URL (with embedded auth token)
and force-push all project files as a single commit.
Backend:
- GitSyncHandler: assembles project docs + binary files into a temp dir,
runs git init/commit/push --force, then cleans up
- GitSyncController: GET/POST /project/:id/git-sync (configure),
POST /project/:id/git-sync/push (trigger)
- Project model: gitRemote field
- Dockerfile: ensures git is present at runtime
- Env flag: OVERLEAF_ENABLE_GIT_SYNC=true (set in k8s manifest)
Frontend:
- GitSyncWidget: URL input + Save + Push Now buttons, success/error feedback
- Integrations panel: shows widget when gitSyncEnabled
- Rail: shows Integrations tab when gitSyncEnabled (was only gitBridgeEnabled)
- i18n: en + fr translations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebAssembly.instantiateStreaming() requires 'wasm-unsafe-eval' in the
script-src CSP directive. Unlike 'unsafe-eval', this only permits WASM
compilation and does not allow arbitrary eval() calls.
Needed for the typst.ts WASM preview (both compiler and renderer).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only collaborators and token-link users could publish, unpublish,
and rotate presentation share tokens. Change all three write endpoints
from ensureUserCanReadProject to ensureUserCanAdminProject so only the
project owner can perform these actions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sed substitution literal-escaped the backtick and \${ in the
groupSSOReauthenticate subject line, producing invalid JS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Email:
- Replace Overleaf green palette with Verso teal (#2a9d8f) for CTA
buttons, links, and logo text
- Rename force-overleaf-style CSS class to force-verso-style in all
email body templates and layout
- Replace all user-visible "Overleaf" strings with settings.appName
across email subjects, bodies, and sign-offs (EmailBuilder.mjs)
- Remove Overleaf CEO signature from onboarding email; substitute
"The Verso Team"
- Fix utm_source=overleaf → utm_source=verso in onboarding links
- Point git token docs URL to local siteUrl instead of docs.overleaf.com
- Fix hard-coded Overleaf green (#0F7A06) in SSO disabled email link
Mobile filter chips:
- Switch from overflow-x:auto (still leaks through ancestor flex chain)
to flex-wrap:wrap with flex:1 1 auto on each chip so all chips in a
row grow to equal width — no overflow, clean alignment
- Toolbar becomes flex-column: chips on top, zoom below-right
Misc:
- Remove import_typst_file option from new-project menu
- Add interface_language to extracted-translations.json whitelist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert: backend now returns parentFolderId+isNew; frontend calls
dispatchCreateDoc directly so the new file appears without a page refresh
- Set as main: infer compiler from file extension and POST both rootDocId
and compiler together; updateProject propagates the change to the
editor settings dropdown and project list immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set as main document (context menu):
- canSetRootDocId used selectedEntityIds so .typ/.qmd files couldn't be
set as main via right-click on an unselected file.
Now computed locally from contextMenuEntityId (same pattern as convert)
using isValidTeXFile which already covers .typ, .qmd, .tex etc.
Compiler filter (editor settings):
- docs?.find(id) could return undefined due to ID format mismatch,
causing all engines to show as available for non-LaTeX projects.
Added findInTree fallback so the root doc name is always resolved.
ZIP import compiler:
- Projects created from ZIP always got defaultLatexCompiler ('quarto')
regardless of content.
- findRootDocFileFromDirectory now also searches for .typ and .qmd root
files after finding no .tex file.
- ProjectUploadManager now infers the compiler from the root doc
extension and sets it on the project after import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. Converting when output already exists threw DuplicateNameError (400).
Now overwrites existing doc via setDocument instead of failing.
2. Right-clicking an unselected file left contextMenuEntityId null,
so the first click on Convert silently did nothing. Added
contextMenuEntityId to FileTreeMainContext, set it on right-click
and on the … button click; FileTreeItemMenuItems now uses it for
the convert hooks rather than relying on selectedEntityIds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "Convert to Typst (.typ)" in the file tree context menu for .tex
docs, and "Convert to LaTeX (.tex)" for .typ docs. Clicking runs pandoc
on the file content and creates the converted file in the same folder.
- New backend endpoint POST /project/:id/doc/:id/convert/:type that reads
the doc from document-updater, runs pandoc directly, and creates the
result via ProjectEntityUpdateHandler (file tree updates via socket).
- Rewrite the export success toast for typst and latex conversions: no
more link to /contact, replaced with a plain warning that errors are
expected (pandoc does not support all constructs).
- Add i18n keys: convert_to_typst, convert_to_latex,
typst_export_feedback_message, latex_export_feedback_message (EN + FR)
and all four to extracted-translations.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- clsi-nginx: allow hyphens in project-id regex — conversion IDs are UUIDs
which nginx was rejecting, causing 500 on file download after conversion
- CLSI ConversionController/Manager: add 'latex' export type (typst→latex via pandoc)
- Web: add 'latex' to SUPPORTED_CONVERSION_TYPES
- Frontend: add Export as LaTeX button (visible only for typst projects)
- Fix visibility logic: export-as-latex shows for typst, export-as-typst shows for latex
- Add export_as_latex translation key (en + fr)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typst → LaTeX import:
- CLSI ConversionManager: add 'typst' to CONVERSION_CONFIGS
(pandoc input.typ --from typst --to latex --standalone → zip archive)
- Web controller: allow 'typst' as a valid importDocument conversion type
- Frontend modal: add .typ file config to ImportDocumentModal
- New project button modal: add 'import_typst' variant + switch case
- New project button: show "Import Typst file" when enablePandocConversions
is true (no split test gate — Verso has no SaaS split test infra)
- Locales: add choose_typst_file and import_typst_file keys (18 locales)
Export button fix:
- Remove featureFlag="export-typst" from ExportProjectWithConversionButton
so the button shows whenever enablePandocConversions is true, without
needing an unconfigured split test to return 'enabled'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new "Export as Typst" option in the project title dropdown and
File menu, mirroring the existing docx/markdown/html export pipeline.
Changes:
- CLSI ConversionManager: add 'typst' to LATEX_EXPORT_CONFIGS
(compressOutput: false, pandoc --from latex --to typst)
- Web controller: register 'typst' → 'typ' in SUPPORTED_CONVERSION_TYPES
- Frontend: extend conversionType union and add ExportProjectWithConversionButton
- File menu: add 'export-as-typst' to the download group command structure
- Locales: add export_as_typst key to all 18 locale files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Footer: use translate('built_on') key instead of hardcoded 'Built on';
update fr.json 'built_on' → 'Basé sur Overleaf'
- Mobile project list: move search bar + button outside the bordered
TableContainer so its width is viewport-constrained, not affected by
the table's fixed layout
- Mobile table rows: use width:100% (not auto) on cells so they fill the
full tr width regardless of the higher-specificity column percentage
rules; add explicit width:100% on tr to anchor flex-column sizing;
keep width:auto on absolutely-positioned actions cell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Language picker:
- Add fallback href in Pug so language links navigate even if JS fails
- Anchor dropdown to right edge (right:0) so it stays on-screen when
the picker is near the right side of the footer on mobile
Editor layout:
- Read stored pdfLayout from localStorage on init so the last-used
layout is remembered across sessions
- Default to verticalSplit (top/bottom) on mobile when no preference
is stored, so the editor opens in a sensible layout on phones
Translations:
- Add top_bottom_split_view key to all 16 locales that were missing it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The manual JS click-handler approach (tried with stopPropagation,
containment check, and mousedown variants) never worked reliably on
login/password/settings pages. The browser's native <details>/<summary>
toggle behaviour requires no JavaScript and is immune to Bootstrap JS or
React event delegation interference.
The inline script now only builds the return_to hrefs and handles
outside-click-to-close (setting details.open=false). The CSS gains a
.language-picker-details rule that sets position:relative so the
absolutely-positioned dropdown-menu is positioned correctly, and
details[open] .dropdown-menu { display: block } to show the menu.
The #language-picker-toggle id remains on the <summary> so the existing
CSS (cursor, text-decoration, material-symbols alignment) continues to
apply. The React LanguagePicker is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Search bar overflow: min-width:0 on form prevents input min-content
from overflowing flex container on narrow screens
- Remove duplicate New Project button from header row (tableTopArea
already provides it for mobile)
- Simplify tableTopArea: single button+search layout regardless of
isLibraryEnabled flag
- 2-line project rows on mobile: merge dash-cell-date-owner +
dash-cell-tag into a single dash-cell-meta so date/owner/tags flow
inline on line 2 below the project name
- Footer mobile: hide language-picker-text on mobile (icon only) to
prevent 3rd line in 2-row footer
- Language picker: use mousedown instead of click for outside-close
handler — click bubbling is unreliable on iOS for non-interactive
elements; mousedown fires for all taps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mobile vertical layout: add key= based on direction so react-resizable-panels
remounts cleanly when switching between horizontal and vertical; also use
isVertical (not just pdfLayout) for the autoSaveId to avoid restoring a
mismatched layout from localStorage
- Language picker: replace stopPropagation pattern with a containment check on
the document click handler — more robust on React pages where Bootstrap JS or
React's event delegation can interfere with stopPropagation
- Presentation download dropdown: use popperConfig strategy:'fixed' so the menu
escapes overflow:hidden table cells; remove forced drop='up' and let Popper
choose; defer URL.revokeObjectURL by 10 s to give the browser time to start
the download before the blob URL is released
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The native <select> looked like a form control ("cheap"). Replace it with
the same HTML+CSS pattern as the React LanguagePicker: btn-inline-link
toggle with a translate icon, Bootstrap dropdown-menu for the list, active
item highlighted in green.
A tiny self-contained inline script handles the toggle since Bootstrap JS
is not loaded on React-layout pages. It builds the return_to URL from
window.location.pathname at click time (same as the old select approach).
Added top: auto / bottom: 100% to .language-picker .dropdown-menu so the
list always opens upward regardless of whether Popper.js is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Footer (both React and Pug):
- Below md: switch to flex-wrap so items wrap naturally instead of
overflowing; reset line-height from the fixed 49px to normal; add
row-gap so wrapped rows aren't crammed together
- Add footer-sep class to pipe separator <li>s so they can be hidden
on small screens (wrapping mid-separator looks wrong)
- Change col-lg-3 text-end → text-lg-end so the right column (licence,
source code) aligns left when it stacks full-width below lg
Project list:
- Show NewProjectButton on mobile in the header row (the sidebar that
holds the button is already hidden below md via d-none d-md-flex,
leaving users with no way to create projects on their phone)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Overleaf favicons with Verso icon mark across all sizes (16x16,
32x32, 180x180 apple-touch, 192x192/512x512 android-chrome, ICO). Add
OG social preview image (1200x630) for Discord/Twitter link cards.
- New favicon.svg: Verso icon mark with four overlapping circles and V
- mask-favicon.svg: monochrome V polygon (was Overleaf chevron)
- og-image.png: 1200x630 social card with icon mark and "Verso" wordmark
- web.sitemanifest.json: rename "Overleaf" → "Verso", theme_color updated
- _metadata.pug: add og:url tag, fix og:type missing content= attribute,
point CE default OG/twitter images to og-image.png instead of apple-touch-icon.png
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old dropdown relied on data-bs-toggle and AngularJS directives,
neither of which are loaded on React-layout pages (layout-react.pug
intentionally excludes Bootstrap JS). The toggle button was inert on
pages like /user/settings.
Replace with a plain <select> that navigates via window.location.href
onchange — works without any framework on all page types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrites LanguagePicker to use availableLanguages from ol-footer meta
instead of subdomainLang (which is always empty in single-domain setup)
- Passes availableLanguages through layout-react.pug → ol-footer meta so
React footer picks it up
- Adds InterfaceLanguageSetting component to the editor settings modal
("Spelling and language" tab) for use when no footer is present
- Adds interface_language key to all five locale files (en/fr/de/es/it)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now select their UI language directly without relying on
subdomain routing (fr.verso.alocoq.fr etc.).
Resolution order: (1) verso-lang cookie, (2) subdomain host header,
(3) OVERLEAF_SITE_LANGUAGE default — fully backward compatible.
Changes:
- Translations.mjs: read verso-lang cookie in middleware; include all
bundled locale files in availableLanguageCodes regardless of subdomain
config so every loaded locale appears in the picker
- User.mjs: add languageCode field to persist preference per user
- UserController.mjs: setLanguage handler — sets cookie (1 year) and
writes languageCode to DB when called by a logged-in user
- AuthenticationController.mjs: on login, sync DB languageCode to cookie
so preference follows the user to any new browser/device after login
- ExpressLocals.mjs: expose availableLanguages to all Pug templates
- router.mjs: GET /set-language?lng=<code> (anonymous + logged-in),
POST /user/language (logged-in, REST-style)
- language-picker.pug: replace subdomain href links with /set-language
redirect links; iterate availableLanguages instead of subdomainLang
- thin-footer.pug: show picker whenever availableLanguages.length > 1,
not only when multiple subdomains are configured
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uploads from slow connections consistently fail with 502 after ~60-120s
because an upstream proxy (Traefik or cloud load-balancer) has a
"first response byte" deadline that fires before the request body arrives.
Fix: add startStreamingResponse middleware (after auth, before multer)
that immediately writes HTTP 200 + Transfer-Encoding: chunked + '\n'.
With proxy_request_buffering off in Nginx, this reaches the proxy at T≈0,
so no timeout triggers. The upload body continues streaming; multer writes
to disk; the actual JSON result arrives as the final chunk. Periodic
heartbeat '\n' writes every 30s keep response-idle timeouts at bay too.
Client-side: override Uppy's getResponseData/validateStatus to trim
leading whitespace before JSON.parse so the extra '\n' bytes are ignored.
Server-side: sendUploadResponse() helper handles both streaming mode
(res.headersSent → res.end(json)) and normal mode (res.status(N).json()).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Theming: replace per-controller isLumiere lookups with a single
ExpressLocals middleware that sets res.locals.isLumiere for every
web request. Uses getOverallTheme() (now exported from
UserSettingsHelper) so the date-based default is handled correctly.
This covers 404, settings, setPassword, activate, and all future
server-rendered pages automatically.
Upload timeout: add client_body_timeout 15m to nginx.conf.template
at the http level (was defaulting to 60s globally). This is more
reliable than the location-specific override from build 229.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nginx: add dedicated upload location with client_body_timeout 15m,
client_max_body_size 550m, and proxy_request_buffering off. Default
client_body_timeout of 60s was the actual culprit cutting slow uploads.
Node.js requestTimeout (build 228) remains as a backstop.
Lumière: pass isLumiere from UserPagesController (settings),
PasswordResetController (set-password), and UserActivateController
(first-time activation). auth.scss adds card styling for auth pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend fetch gets AbortSignal.timeout(15 min) so hung connections
fail cleanly. Server requestTimeout raised from Node default (5 min)
to match, preventing large-file uploads from being cut off server-side.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- XS compact row: format column 70px→96px so "QUARTO SLIDES" stays on one line;
trim owner/date cols slightly to compensate
- Welcome page (0 projects): Lumière branch now renders before the 0-projects
check; ProjectListLumiere renders WelcomePageContent when totalProjectsCount=0
so new users get the full onboarding experience in the Lumière shell
- 404 page: notFound() now detects the user's overallTheme and passes isLumiere
to the template; layout-base.pug sets data-lumiere on the body; error-pages.scss
and project-list-lumiere.scss add [data-lumiere='true'] rules for the body
background gradient, navbar white+stripe, and styled error box
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>