- 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>
- 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>
After a successful compile, web service calls a new CLSI endpoint
(GET /project/:id/user/:uid/build/:bid/thumbnail) which runs pdftocairo
page-1 to a 190px-wide JPEG using the existing thumbnail preset. The
JPEG is stored in Redis (90-day TTL, overwritten on next compile) by
the new ThumbnailManager.
GET /project/:Project_id/thumbnail serves the cached JPEG to authenticated
users, returning 404 when no thumbnail exists. Project cards in the
Lumière grid show the image overlaying the coloured gradient tile; if
the image 404s (project never compiled or cache expired) the onerror
handler hides it and the gradient + initial letter shows through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New theme with gradient document cards, serif title typography and a
light airy palette. Set as the default for new users. Existing users
keep their current theme; all users can switch via the theme toggle
(new sparkle icon). Classic Dark / Classic Light are renamed accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
userCanInstallPython passed null as the token, so anonymous users
accessing via a share link got privilege level NONE from the WithoutUser
path and allowPythonInstall was always false for them.
Read the token from req.session.anonTokenAccess via
TokenAccessHandler.getRequestToken and forward it through
userCanInstallPython to getPrivilegeLevelForProject. For TOKEN_BASED
projects this resolves the anonymous user's access level via
getPrivilegeLevelForProjectWithToken, enabling package installation.
Also update Quarto Slides badge color to #e4637c.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
options.compiler is set from req.body.compiler which the frontend never
sends, so the condition was never true and quartoFlavor was never written.
Use ProjectGetter to read the stored compiler instead. Fire-and-forget so
it does not delay the compile response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously userCanInstallPython used ignorePublicAccess: true, which
blocked token-link users (not-yet-joined) and logged-in readers of public
projects from installing packages. This caused Quarto presentations with
Python cells to fail for shared read-only users even when the required
packages were already listed in requirements.vrf.
The security model is: what gets installed is fully controlled by
requirements.vrf, which is only writable by members with write access.
There is therefore no security reason to block other readers from
triggering installation of already-approved packages.
Drop ignorePublicAccess so all users with any privilege level (direct,
token-based, or public-project) can trigger the venv install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix wrong import path '../models/Project.mjs' → '../../models/Project.mjs'
(from Features/Compile/, '..' is Features/, not src/; the server would
crash on startup with ERR_MODULE_NOT_FOUND in Node.js ESM)
- Log MongoDB errors instead of silently swallowing them
- Remove null from Mongoose String enum (not a valid enum value for strings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a quartoFlavor field ('revealjs' | 'pdf') to the Project model.
After each successful Quarto compile, CompileController detects the output
type (output.html → revealjs, otherwise pdf) and persists it.
ProjectListController includes it in the projection and serialization so
it reaches the frontend without an extra round-trip.
Badge variants:
- quartoFlavor unset (new/uncompiled) → "Quarto PDF" #447099
- quartoFlavor 'pdf' → "Quarto PDF" #447099 (Quarto blue)
- quartoFlavor 'revealjs' → "Quarto Slides" #7e56c2 (purple)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rate limit: auto-compile requests already have a client-side debounce; skip
the 1-second server-side recently-compiled gate for them to avoid spurious
'too-recently-compiled' rejections that were blocking ~1/3 of Typst compiles.
PDF flicker: add _snapshotCanvases() fallback for browsers without element-level
View Transitions (Chrome <126, Firefox, Safari). Before setDocument() clears the
canvases it copies each rendered page to a positioned overlay; the overlay is
removed once the first page of the new document fires pagerendered, giving a
seamless old→new swap in all browsers. Chrome 126+ continues to use the
startViewTransition async callback path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Replace token-link email with 6-digit code on SSO registration
Unverified SSO emails previously received a long-lived token link
(90-day TTL) via UserEmailsConfirmationHandler. This replaces that
flow with the same 6-digit code verification used for password
registration, redirecting through /registration/confirm-email.
- SSOManager.registerSSO now always confirms email (caller must
verify first); removes sendConfirmationEmail / _finishRegistration
- SSOController._signUp sends confirmation code and stores
pendingSSORegistration in session when IdP email_verified is false
- New SSOConfirmEmailHandler completes registration after code check
via completeSSOEmailConfirmation module hook
- OnboardingController confirm-email handlers accept
pendingSSORegistration alongside pendingUserRegistration
confirmEmailFromToken (POST /user/emails/confirm) removal is deferred
to a follow-up PR to avoid breaking in-flight 90-day tokens.
Closes#28607
* Fix unverified-email edge cases; Add ORCID e2e tests;
* Rename `confirmEmail` parameter to `emailVerifiedByIdP` in _signUp function
* Remove `sendConfirmationEmail`
* Mock getUserByAnyEmail in tests
* Extract _finishSSORegistration helper to deduplicate the register →
set session flags → allocate referral → finishSaasLogin → finishLogin
sequence shared by both the direct and deferred (code-confirmed) paths.
* Stop duplicating session data in pendingSSORegistration
analyticsId, splitTests, and referal_* are already in the session at
confirmation time — no need to copy them into pendingSSORegistration.
Re-fetch splitTests fresh on completion instead.
* Simplify the code
* Remove dead confirmEmail template
No callers remain after sendConfirmationEmail was deleted. The token-link
flow (confirmEmailFromToken) only validates tokens, never sends email.
* Remove dead reconfirmEmail template
* Address comments from Copilot
* Clear stale pending registration when starting a new flow
* Add unit tests for completeSSOEmailConfirmation
* Add `verificationMethod` param
* Fix camelcase issues
* Extract _createSSOUser and _registerAndFinish helpers to deduplicate registration logic
* Remove obscure "registration_error"
* Prevent FormTextIcon from shrinking
* Enable "email_already_registered_sso" error
* Misc. improvements to confirm-email-form.tsx
* Remove `UserEmailsConfirmationHandler` mock
Co-authored-by: Olzhas Askar <olzhas.askar@overleaf.com>
* Add info on sso_email.pug page
---------
Co-authored-by: Olzhas Askar <olzhas.askar@overleaf.com>
GitOrigin-RevId: d0196ebc6d81ff61bcd27726d0b899b743d08d64
* [web] Order plans in Change Plan modal consistently
Reorder the plans returned by `buildPlansListForSubscriptionDash` so the
Subscription page "Change plan" modal lists them top-to-bottom as:
1. Student annual
2. Student monthly
3. Standard monthly
4. Standard annual
5. Pro monthly
6. Pro annual
Previously `buildPlansList` produced three per-period buckets which the
dash function concatenated, giving an order that flipped per family.
Replace that with an explicit `CHANGE_PLAN_MODAL_PLAN_CODES` list so the
order matches the Design QA spec at a glance. The now-unused
`studentAccounts`, `individualMonthlyPlans`, `individualAnnualPlans`,
`groupMonthlyPlans`, and `groupAnnualPlans` buckets are dropped from
`buildPlansList` (no other callers).
Closes#34024
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Update personal-plan acceptance test for new buildPlansList shape
The previous test asserted `buildPlansList().individualMonthlyPlans`,
which no longer exists after the change-plan modal reorder dropped the
per-period buckets. Move the assertion to
`buildPlansListForSubscriptionDash()`, which is where the personal-plan
exclusion is now enforced (via `CHANGE_PLAN_MODAL_PLAN_CODES`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Drop now-dead client-side plan filter
`IndividualPlansTable` used to filter out `paid-personal`,
`paid-personal-annual` and `institutional_commons` defensively because
the old `buildPlansListForSubscriptionDash` returned every non-group
plan that wasn't `hideFromUsers`. The previous commit pins the modal to
an explicit six-plan list (`CHANGE_PLAN_MODAL_PLAN_CODES`), so none of
those plan codes ever reach the frontend and the filter is dead. Remove
it and the now-unused `useMemo` import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "[web] Drop now-dead client-side plan filter"
This reverts commit 83e8448f2cfa2c68e44b749d5a2bc350a7443c6d.
We'll do that in a later cleanup
* Swap "Student monthly" and "Student annual" for consistency
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 046a235e14e7ad6622288f5a5a723f5a4f7f14da
* [web] Redirect missing AI add-on purchase to subscription dashboard
The two error paths in `previewAddonPurchase` redirected to
`/user/subscription/plans#ai-assist`, but the `#ai-assist` anchor was
removed when the AI Assist add-on was retired, so users land at the top
of the plans page with no context. Align both with the other error
branches in the same function and the `plans-2026-phase-1` enabled
branch, which already redirect to
`/user/subscription?redirect-reason=ai-assist-unavailable` — the
subscription dashboard shows the matching warning alert
(`redirect-alerts.tsx`).
Update the acceptance test to match the new redirect target.
Closes#34074
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Update ai-assist-unavailable warning to reflect bundled AI features
The previous copy said "AI Assist isn't available to you due to your
current subscription type", which read as a hard block. Now that the AI
Assist add-on has been retired and AI features are included with every
paid plan, the warning should point users to the pricing page instead of
implying their plan can't access AI at all.
Keep the existing translation key for now — a follow-up can rename it
once #33624 (AI page CTA destination) is resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Link the ai-assist-unavailable warning to the pricing page
* [web] Rename key `ai_assist_unavailable_due_to_subscription_type` -> `ai_assist_unavailable`
* [web] Update french and german translations
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: ae1319fa5b857d8f292de77c82ef0bda1c7ad144
* Allow admin access to user PATs
* Tests for new screen in admin panel
* Adding error for invalid token and way to parse error for OAuth 2
* Git bridge handles expired PAT
* Script for alerting on close to expiry and expired git tokens
* Refactoring and simplifying
* Updating email templates to match agreed docs
* tweak to email subject to include Overleaf
* Allowing dry run in scripts and general tidy up
* removing redundant tests and dry running script
* Fixing CI errors
* Adding new tab to admin test expectation
* Address PR feedback on oauth2-server changes
- Replace ad-hoc overleafErrorCode prop with a TokenExpiredError subclass
- Collapse listTokens/listTokensForAdmin into a single hook
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Adding cron definitions for alerting on expiring git pat
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
GitOrigin-RevId: 69b9fd901a201592a580c69abe7bd7d603e85d3a
- Hide the Present button when the current output is a PDF (it only makes
sense for HTML/RevealJS decks).
- Publish now supports PDF projects: snapshot output.pdf and serve it inline
via a small index.html wrapper at /p/:token, so link holders can view the
PDF straight from the published version.
- Add a Typst document outline (scans '=' headings) wired into the file
outline panel.
- Dashboard branding: enlarge the instance-name/version text and let the
sidebar Verso wordmark span the full column width.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add a Typst language (stream highlighting + completions) for .typ, and
Quarto completions (code chunks, callouts, cross-refs) for .qmd/markdown.
- Project dashboard: new Format column (Quarto/Typst/LaTeX) from the cheap
project compiler field, surfaced through the projects list API.
- Compiler dropdown: grey out engines that don't match the root file's
extension (.qmd->Quarto, .typ->Typst, .tex->LaTeX engines).
- Replace the Overleaf fill loader with an animated Verso logo: the four
quadrant circles drift on their own orbits while colour warms up with load
progress; reused on the token-access screen too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'Python packages' button to the file-tree toolbar that opens a modal to
edit the project's requirements.vrf (one package per line, pip syntax), backed
by GET/POST /project/:id/python-requirements (read via ProjectEntityHandler,
write via EditorController.upsertDocWithPath, write-gated). The .vrf file is now
hidden from the file tree, so it is managed only through this editor rather than
appearing as a loose file. Adds python_packages / python_packages_help i18n.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base image: add opencv-python-headless (cv2) and tqdm to the bundled
scientific stack, and python3-venv (needed to build per-project venvs).
Per-project dependencies: a project's requirements.txt is now installed into a
venv cached by its sha256 (python3 -m venv --system-site-packages, so the
bundled stack stays visible and only extra packages are installed); QuartoRunner
points Quarto at it via QUARTO_PYTHON. A per-hash flock serialises concurrent
builds; pip output is merged into output.log; on failure the render falls back
to the base interpreter. Venvs live under PYTHON_VENVS_DIR
(default /var/lib/overleaf/data/python-venvs).
Gating: PythonVenvGate.userCanInstallPython restricts installs to the project
owner + invited collaborators (ignorePublicAccess excludes anonymous/link
users), threaded to CLSI as allowPythonInstall on the editor compile,
presentation export, and publish paths. Behind OVERLEAF_ENABLE_PROJECT_PYTHON_VENV
(enabled in the deployment). Design doc updated; Phase 2 (egress policy) and
Phase 3 (venv eviction) remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The slide-PDF export failed because the CLSI runtime user has no writable
HOME, so Chromium's crashpad couldn't create its database and the browser
died on launch ('chrome_crashpad_handler: --database is required'). Give
decktape's Chromium a fresh writable temp dir via HOME/XDG_*/--user-data-dir
(plus --disable-gpu).
The standalone-HTML export kept returning the old non-embedded file partly
because the GET response had no cache headers, so the browser served its
cached copy; add Cache-Control: no-store to both export responses. Also
switch the embed-resources flags to the long '--metadata KEY:VALUE' form
(the documented Quarto syntax) to remove any ambiguity vs the '-M' alias.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The standalone-HTML export produced a non-self-contained file (no slide
CSS/JS, math or images when opened away from the server) because Quarto's
--metadata/-M flag uses KEY:VALUE (colon), not KEY=VALUE. '-M
embed-resources=true' silently registered a bogus key and left
embed-resources unset. Switch to colon syntax and also embed MathJax
(self-contained-math:true) so equations render offline.
For the slide PDF, add --disable-dev-shm-usage (the usual cause of
Chromium crashing inside a container with a small /dev/shm), and have the
export controller return the compile log as text/plain on failure so a
failed PDF export shows the real decktape/Chromium error instead of an
HTML page the browser saves as 'pdf.htm'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In RevealJS mode the download button becomes a 2-choice menu:
- Standalone HTML: a one-off compile with embed-resources (chalkboard and other
runtime-only plugins are dropped, since they don't survive self-containment),
yielding a single portable .html.
- Slide PDF: render the deck, then print it with decktape (headless Chromium)
to a faithful one-slide-per-page PDF.
Implementation:
- Dockerfile-base: install decktape + headless Chromium (open-source; deps via
playwright install-deps for Ubuntu-Noble correctness). Base-only change.
- QuartoRunner honours options.exportMode ('html-standalone' | 'pdf-slides');
exportMode is threaded web ClsiManager -> CLSI RequestParser -> CompileManager
-> runner.
- New GET /project/:id/presentation-export/:format compiles in the matching
export mode and streams the result as a download (PresentationExportController,
reusing ClsiManager.getOutputFileStream).
- pdf-hybrid-download-button shows the dropdown when the output is output.html;
PDF/LaTeX projects keep the single download button.
- i18n: download_as_standalone_html / download_as_pdf_slides (en + fr +
extracted-translations.json).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a project-members-only link tier and independent link rotation.
- Three tokens per project instead of two: publicToken (anyone), loginToken
(any logged-in user), memberToken (only users who can read the project).
serve() resolves the token to its tier and enforces accordingly — 'member'
requires AuthorizationManager.canUserReadProject.
- New POST /project/:id/publish-presentation/regenerate { tier } rotates a
single tier's token (invalidating only that old link), leaving the snapshot
and the other links intact.
- Share dialog now shows three links (members / logged-in / anyone), each with
its own Copy and Reset buttons; Publish refreshes, Unpublish removes all.
Preview button opens the logged-in-users link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single token + visibility toggle with two stable tokens per project
pointing at the same snapshot:
- publicToken → anyone with the link
- privateToken → any logged-in Verso user
This fixes both reported issues: changing visibility no longer mutates a link
(there's no toggle — both links always exist), and a public link can never
become private by accident. It also fixes public links redirecting to login:
access is now decided purely by which token was used (public token = open),
not a per-record flag.
- Model: storageId (snapshot dir) + publicToken + privateToken; drop token/
visibility.
- Manager.publish: mints both tokens once and reuses them on re-publish; serve
resolves a token to its record and treats the public token as open.
- Controller: returns { publicUrl, privateUrl }.
- Share dialog: shows the private and public links side by side, each with its
own copy button; Publish refreshes, Unpublish removes. Preview button opens
the private link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A deck served at /p/:token (no trailing slash) made the browser resolve its
relative asset references (main_files/... CSS+JS) against /p/, 404ing them —
so the deck rendered as unstyled HTML with no reveal.js. Publish links now end
in a slash, and the bare /p/:token URL 301-redirects to /p/:token/, so relative
assets resolve under /p/:token/ and load correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CompileManager.compile debounces compiles via a Redis key set on every compile
(_checkIfRecentlyCompiled), returning {status:'too-recently-compiled',
outputFiles:[]} when the editor has just auto-compiled. Publishing called
compile() and then required output.html, so it threw "did not produce an HTML
presentation" — which is why Preview/Publish errored whenever the deck was
freshly compiled.
- CompileManager.compile: honour options.bypassRecentCompileCheck to skip the
debounce (still runs the normal autocompile-limit guards).
- PublishedPresentationManager: publish with bypassRecentCompileCheck, and put
the compile status in the error message for diagnosis.
- Controller: catch publish errors, log them, and return the message so the
Share dialog can show what went wrong instead of a generic error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the engine + API for publishing a project's compiled HTML/RevealJS deck as
a stable, standalone snapshot served at /p/:token, independent of the editor.
- PublishedPresentation model: one per project { token, visibility, buildId },
re-publishing keeps the same token so shared links stay stable.
- Manager.publish: compiles the project, then copies the HTML deck + its _files
assets + referenced media (now included thanks to the OutputFileFinder fix)
into a persistent snapshot dir (Settings.path.publishedPresentationsFolder,
override with PUBLISHED_PRESENTATIONS_PATH). Logs/aux are excluded.
- Routes: GET/POST/DELETE /project/:id/publish-presentation (owner/reader) for
status/publish/unpublish; public GET /p/:token(/*) serves the deck full-page.
Visibility is enforced in the handler: 'public' = anonymous, 'private' = any
logged-in Verso user. CSP is dropped on these responses so reveal.js renders.
Frontend entry points (share-modal section + top-right Preview button) follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>