Fix publish-presentation failing right after an editor compile
Build and Deploy Verso / deploy (push) Successful in 7m49s

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>
This commit is contained in:
claude
2026-06-01 15:30:46 +00:00
parent 59055aa67e
commit cb0d9ac9fa
4 changed files with 49 additions and 24 deletions
@@ -28,12 +28,17 @@ function generateBuildId() {
}
async function compile(projectId, userId, options = {}) {
const recentlyCompiled = await CompileManager._checkIfRecentlyCompiled(
projectId,
userId
)
if (recentlyCompiled) {
return { status: 'too-recently-compiled', outputFiles: [] }
// Publishing a presentation needs a full output set on demand, so it can ask
// to skip the debounce that suppresses compiles right after the editor's
// auto-compile (which would otherwise return no output files).
if (!options.bypassRecentCompileCheck) {
const recentlyCompiled = await CompileManager._checkIfRecentlyCompiled(
projectId,
userId
)
if (recentlyCompiled) {
return { status: 'too-recently-compiled', outputFiles: [] }
}
}
try {
@@ -23,14 +23,22 @@ function _serialize(record) {
async function publish(req, res) {
const projectId = req.params.Project_id
const userId = SessionManager.getLoggedInUserId(req.session)
const requested = req.body?.visibility
const visibility =
req.body?.visibility === 'public' ? 'public' : req.body?.visibility
const record = await PublishedPresentationManager.promises.publish(
projectId,
userId,
{ visibility: visibility === 'public' || visibility === 'private' ? visibility : undefined }
)
res.json(_serialize(record))
requested === 'public' || requested === 'private' ? requested : undefined
try {
const record = await PublishedPresentationManager.promises.publish(
projectId,
userId,
{ visibility }
)
res.json(_serialize(record))
} catch (err) {
logger.error({ err, projectId }, 'failed to publish presentation')
res
.status(400)
.json({ message: err.message || 'failed to publish presentation' })
}
}
async function status(req, res) {
@@ -55,14 +55,19 @@ async function _downloadOutputFile(
// omitted, an existing record keeps its visibility and a new one defaults to
// 'private'.
async function publish(projectId, userId, { visibility } = {}) {
const { outputFiles, clsiServerId, buildId } =
await CompileManager.promises.compile(projectId, userId, {})
const { status, outputFiles, clsiServerId, buildId } =
await CompileManager.promises.compile(projectId, userId, {
bypassRecentCompileCheck: true,
})
if (!buildId || !outputFiles?.some(f => f.path === 'output.html')) {
if (!outputFiles?.some(f => f.path === 'output.html')) {
throw new Errors.InvalidError(
'project did not produce an HTML presentation'
`project did not produce an HTML presentation (compile status: ${status})`
)
}
if (!buildId) {
throw new Errors.InvalidError('compile produced no build id')
}
// Output files are stored per-user unless per-user compiles are disabled;
// mirror CompileManager so the download URLs resolve to the right location.
@@ -1,7 +1,12 @@
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useProjectContext } from '@/shared/context/project-context'
import { getJSON, postJSON, deleteJSON } from '@/infrastructure/fetch-json'
import {
getJSON,
postJSON,
deleteJSON,
getUserFacingMessage,
} from '@/infrastructure/fetch-json'
import getMeta from '@/utils/meta'
import OLButton from '@/shared/components/ol/ol-button'
import OLNotification from '@/shared/components/ol/ol-notification'
@@ -26,7 +31,7 @@ export default function PublishPresentationSection() {
const [state, setState] = useState<PublishState>({ published: false })
const [visibility, setVisibility] = useState<Visibility>('private')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const [error, setError] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
useEffect(() => {
@@ -41,21 +46,21 @@ export default function PublishPresentationSection() {
const publish = useCallback(() => {
setLoading(true)
setError(false)
setError(null)
postJSON(`/project/${projectId}/publish-presentation`, {
body: { visibility },
})
.then((data: PublishState) => setState(data))
.catch(() => setError(true))
.catch(err => setError(getUserFacingMessage(err) ?? 'error'))
.finally(() => setLoading(false))
}, [projectId, visibility])
const unpublish = useCallback(() => {
setLoading(true)
setError(false)
setError(null)
deleteJSON(`/project/${projectId}/publish-presentation`)
.then(() => setState({ published: false }))
.catch(() => setError(true))
.catch(err => setError(getUserFacingMessage(err) ?? 'error'))
.finally(() => setLoading(false))
}, [projectId])
@@ -132,7 +137,9 @@ export default function PublishPresentationSection() {
<div className="mt-2">
<OLNotification
type="error"
content={t('generic_something_went_wrong')}
content={
error === 'error' ? t('generic_something_went_wrong') : error
}
/>
</div>
)}