[web] extend project admin page for history debugging (#32437)

* [web] extend project admin page for history debugging

* [web] address review feedback

Co-authored-by: Malik <malik.glossop@overleaf.com>

---------

Co-authored-by: Malik <malik.glossop@overleaf.com>
GitOrigin-RevId: 01866e8c8529bc8332c49baf4ad281e300f8cdd4
This commit is contained in:
Jakob Ackermann
2026-03-27 09:05:52 +00:00
committed by Copybot
co-authored by Malik
parent 8dd743c543
commit a8abc22e6c
8 changed files with 123 additions and 2 deletions
@@ -262,6 +262,35 @@ export function getResyncPending(req, res, next) {
})
}
const getDebugInfoSchema = z.object({
params: z.object({
project_id: zz.objectId(),
}),
})
export function getDebugInfo(req, res, next) {
const {
params: { project_id: projectId },
} = parseReq(req, getDebugInfoSchema)
SyncManager.getResyncState(projectId, (err, state) => {
if (err) return next(err)
ErrorRecorder.getFailureRecord(projectId, (err, failureRecord) => {
if (err) return next(err)
res.json({
failureRecord,
syncState: {
resyncPending: state.isSyncOngoing(),
resyncCount: state.resyncCount,
resyncPendingSince: state.resyncPendingSince,
lastUpdated: state.lastUpdated,
history: state.history,
...state.toRaw(),
},
})
})
})
}
const latestVersionSchema = z.object({
params: z.object({
project_id: zz.objectId().or(z.coerce.number()),
@@ -29,6 +29,7 @@ export function initialize(app) {
'/project/:project_id/resync-pending',
HttpController.getResyncPending
)
app.get('/project/:project_id/debug-info', HttpController.getDebugInfo)
app.post('/project/:project_id/resync', HttpController.resyncProject)
+43 -2
View File
@@ -143,12 +143,14 @@ async function setResyncState(projectId, syncState) {
// starting a new sync; prevent the entry expiring while sync is in ongoing
update.$inc = { resyncCount: 1 }
update.$unset = { expiresAt: true }
update.$min = { resyncPendingSince: new Date() }
} else {
// successful completion of existing sync; set the entry to expire in the
// future
update.$set.expiresAt = new Date(
Date.now() + EXPIRE_RESYNC_HISTORY_INTERVAL_MS
)
update.$unset = { resyncPendingSince: 1 }
}
// apply the update
@@ -270,11 +272,24 @@ async function expandSyncUpdates(
}
class SyncState {
constructor(projectId, resyncProjectStructure, resyncDocContents, origin) {
constructor(
projectId,
resyncProjectStructure,
resyncDocContents,
origin,
resyncCount,
resyncPendingSince,
lastUpdated,
history
) {
this.projectId = projectId
this.resyncProjectStructure = resyncProjectStructure
this.resyncDocContents = resyncDocContents
this.origin = origin
this.resyncCount = resyncCount
this.resyncPendingSince = resyncPendingSince
this.lastUpdated = lastUpdated
this.history = history
}
static fromRaw(projectId, rawSyncState) {
@@ -282,11 +297,37 @@ class SyncState {
const resyncProjectStructure = rawSyncState.resyncProjectStructure || false
const resyncDocContents = new Set(rawSyncState.resyncDocContents || [])
const origin = rawSyncState.origin
const resyncCount = rawSyncState.resyncCount || 0
let resyncPendingSince = rawSyncState.resyncPendingSince
const history = rawSyncState.history || []
if (
(resyncProjectStructure || resyncDocContents.size > 0) &&
!resyncPendingSince &&
history.length > 0
) {
// The resyncPendingSince field was added later.
// Back-fill it as the next ts after a successful sync. History is DESC.
for (const other of history.slice().reverse()) {
const isSyncOngoing =
other.syncState.resyncProjectStructure ||
other.syncState.resyncDocContents.length > 0
if (isSyncOngoing) {
resyncPendingSince = resyncPendingSince || other.timestamp
} else {
resyncPendingSince = undefined
}
}
}
const lastUpdated = rawSyncState.lastUpdated
return new SyncState(
projectId,
resyncProjectStructure,
resyncDocContents,
origin
origin,
resyncCount,
resyncPendingSince,
lastUpdated,
history
)
}