mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-09 03:03:18 +08:00
Restore cron sidebar UX: sync mode to editor and show orphan jobs.
DOM cron mode and React overlay could desync so the right pane stayed on the workspace chat; unassigned legacy jobs were also filtered out for normal users. Re-sync mode from the sidebar attribute and adopt/show reclaimable orphans again. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a0dc070372
commit
feae559fcf
3 changed files with 68 additions and 10 deletions
|
|
@ -697,7 +697,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function JobList({ t, jobs, runs, selection, expanded, viewer, onSelectJob, onSelectRun, onNew, onRun, onToggle, onRemove, onToggleGroup }) {
|
function JobList({ t, jobs, runs, selection, expanded, viewer, error, onSelectJob, onSelectRun, onNew, onRun, onToggle, onRemove, onToggleGroup }) {
|
||||||
const skin = workspaceSkin()
|
const skin = workspaceSkin()
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [searchOn, setSearchOn] = useState(false)
|
const [searchOn, setSearchOn] = useState(false)
|
||||||
|
|
@ -897,8 +897,9 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
addProjectIcon() || '+'),
|
addProjectIcon() || '+'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
error ? h('p', { className: 'dsh-ct-error' }, error) : null,
|
||||||
h('div', { className: skin.list },
|
h('div', { className: skin.list },
|
||||||
jobs.length === 0 ? h('p', { className: skin.empty }, t('emptyJobs'))
|
jobs.length === 0 ? h('p', { className: skin.empty }, error || t('emptyJobs'))
|
||||||
: visibleJobs.length === 0 ? h('p', { className: skin.empty }, t('searchEmpty'))
|
: visibleJobs.length === 0 ? h('p', { className: skin.empty }, t('searchEmpty'))
|
||||||
: (canViewAll ? renderGrouped() : visibleJobs.map((job) => renderJobGroup(job))),
|
: (canViewAll ? renderGrouped() : visibleJobs.map((job) => renderJobGroup(job))),
|
||||||
),
|
),
|
||||||
|
|
@ -1237,16 +1238,34 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const readDomMode = () => {
|
||||||
|
const root = document.querySelector('[data-dsh-ct-mode]')
|
||||||
|
return root?.getAttribute('data-dsh-ct-mode') === 'on'
|
||||||
|
}
|
||||||
|
// Catch events missed before mount / after sidebar remount.
|
||||||
|
setCronMode(readDomMode())
|
||||||
load()
|
load()
|
||||||
const id = setInterval(load, cronMode ? 1500 : 4000)
|
const id = setInterval(load, cronMode ? 1500 : 4000)
|
||||||
const onMode = (event) => setCronMode(!!event.detail)
|
const onMode = (event) => setCronMode(!!event.detail)
|
||||||
const onVis = () => { if (document.visibilityState === 'visible') load() }
|
const onVis = () => { if (document.visibilityState === 'visible') load() }
|
||||||
window.addEventListener('dsh-ct-mode', onMode)
|
window.addEventListener('dsh-ct-mode', onMode)
|
||||||
document.addEventListener('visibilitychange', onVis)
|
document.addEventListener('visibilitychange', onVis)
|
||||||
|
const modeObs = new MutationObserver(() => {
|
||||||
|
const next = readDomMode()
|
||||||
|
setCronMode((prev) => (prev === next ? prev : next))
|
||||||
|
})
|
||||||
|
if (document.body) {
|
||||||
|
modeObs.observe(document.body, {
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-dsh-ct-mode'],
|
||||||
|
})
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(id)
|
clearInterval(id)
|
||||||
window.removeEventListener('dsh-ct-mode', onMode)
|
window.removeEventListener('dsh-ct-mode', onMode)
|
||||||
document.removeEventListener('visibilitychange', onVis)
|
document.removeEventListener('visibilitychange', onVis)
|
||||||
|
modeObs.disconnect()
|
||||||
}
|
}
|
||||||
}, [load, cronMode])
|
}, [load, cronMode])
|
||||||
|
|
||||||
|
|
@ -1421,7 +1440,7 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
|
|
||||||
return h(React.Fragment, null,
|
return h(React.Fragment, null,
|
||||||
h(JobList, {
|
h(JobList, {
|
||||||
t, jobs, runs, selection, viewer, expanded,
|
t, jobs, runs, selection, viewer, expanded, error,
|
||||||
onSelectJob: selectJob,
|
onSelectJob: selectJob,
|
||||||
onSelectRun: selectRun,
|
onSelectRun: selectRun,
|
||||||
onNew: selectNew,
|
onNew: selectNew,
|
||||||
|
|
@ -1650,7 +1669,8 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
entry.addEventListener('click', (event) => {
|
entry.addEventListener('click', (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
cronOn = !cronOn
|
cronOn = !cronOn
|
||||||
setCronMode(findSidebarRoot(found), cronOn, entry, t)
|
const live = findNewSessionButton() || found
|
||||||
|
setCronMode(findSidebarRoot(live), cronOn, entry, t)
|
||||||
})
|
})
|
||||||
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||||
icon.setAttribute('viewBox', '0 0 16 16')
|
icon.setAttribute('viewBox', '0 0 16 16')
|
||||||
|
|
@ -1701,6 +1721,12 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba
|
||||||
|
|
||||||
hideNativeCronRows(sidebar)
|
hideNativeCronRows(sidebar)
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
|
// Keep DOM mode attribute in sync after remounts so React overlay can follow.
|
||||||
|
const want = cronOn ? 'on' : 'off'
|
||||||
|
if (sidebar.getAttribute('data-dsh-ct-mode') !== want) {
|
||||||
|
sidebar.setAttribute('data-dsh-ct-mode', want)
|
||||||
|
window.dispatchEvent(new CustomEvent('dsh-ct-mode', { detail: cronOn }))
|
||||||
|
}
|
||||||
const width = sidebar.getBoundingClientRect().width
|
const width = sidebar.getBoundingClientRect().width
|
||||||
const next = `${Math.round(width)}px`
|
const next = `${Math.round(width)}px`
|
||||||
if (document.documentElement.style.getPropertyValue('--dsh-ct-sidebar') !== next) {
|
if (document.documentElement.style.getPropertyValue('--dsh-ct-sidebar') !== next) {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,24 @@ export function canViewAllJobs(identity) {
|
||||||
return !!identity?.permissions?.canViewAllSessions
|
return !!identity?.permissions?.canViewAllSessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function empNoFromUserWorkspacePath(cwd) {
|
||||||
|
const norm = String(cwd || '').replace(/\\/g, '/')
|
||||||
|
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
||||||
|
return match ? decodeURIComponent(match[1]) : null
|
||||||
|
}
|
||||||
|
|
||||||
export function jobVisibleToIdentity(job, identity) {
|
export function jobVisibleToIdentity(job, identity) {
|
||||||
if (!identity?.empNo) return false
|
if (!identity?.empNo) return false
|
||||||
if (canViewAllJobs(identity)) return true
|
if (canViewAllJobs(identity)) return true
|
||||||
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
|
const owner = normalizeOwnerEmpNo(job?.ownerEmpNo)
|
||||||
if (owner === UNASSIGNED_OWNER) return false
|
// Legacy / tool-created rows often lack ownerEmpNo. Keep them visible so the
|
||||||
|
// sidebar is not empty, then claimUnassignedForViewer can adopt them — unless
|
||||||
|
// the cwd clearly belongs to another user.
|
||||||
|
if (owner === UNASSIGNED_OWNER) {
|
||||||
|
const pathOwner = empNoFromUserWorkspacePath(job?.cwd)
|
||||||
|
if (pathOwner && pathOwner !== String(identity.empNo)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
return owner === String(identity.empNo)
|
return owner === String(identity.empNo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,14 +140,23 @@ export function claimUnassignedForViewer(state, identity, deps = {}) {
|
||||||
? job.origin.sessionId.trim()
|
? job.origin.sessionId.trim()
|
||||||
: ''
|
: ''
|
||||||
let mine = false
|
let mine = false
|
||||||
// Prefer explicit session ownership / user-workspaces path — do not trust a loose isUserPath.
|
// Prefer session ownership / user-workspaces cwd. Adopt web/legacy orphans when
|
||||||
|
// no other owner is known so chat-created jobs reappear in the sidebar.
|
||||||
if (sessionId && typeof deps.getSessionOwner === 'function') {
|
if (sessionId && typeof deps.getSessionOwner === 'function') {
|
||||||
mine = deps.getSessionOwner(sessionId) === identity.empNo
|
const sessionOwner = deps.getSessionOwner(sessionId)
|
||||||
|
if (sessionOwner && sessionOwner !== identity.empNo) return job
|
||||||
|
mine = !sessionOwner || sessionOwner === identity.empNo
|
||||||
}
|
}
|
||||||
if (!mine && job.cwd) {
|
if (!mine && job.cwd) {
|
||||||
const norm = String(job.cwd).replace(/\\/g, '/')
|
const norm = String(job.cwd).replace(/\\/g, '/')
|
||||||
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
const match = norm.match(/\/user-workspaces\/([^/]+)(?:\/|$)/)
|
||||||
mine = !!(match && decodeURIComponent(match[1]) === identity.empNo)
|
if (match) {
|
||||||
|
if (decodeURIComponent(match[1]) === identity.empNo) mine = true
|
||||||
|
else return job
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mine && job.origin?.kind !== 'im') {
|
||||||
|
mine = true
|
||||||
}
|
}
|
||||||
if (!mine) return job
|
if (!mine) return job
|
||||||
changed = true
|
changed = true
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ test('jobVisibleToIdentity respects canViewAll and owner', () => {
|
||||||
const admin = { empNo: 'a1', permissions: { canViewAllSessions: true } }
|
const admin = { empNo: 'a1', permissions: { canViewAllSessions: true } }
|
||||||
assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u1' }, user), true)
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u1' }, user), true)
|
||||||
assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u2' }, user), false)
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: 'u2' }, user), false)
|
||||||
assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, user), false)
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, user), true)
|
||||||
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u2/x' }, user), false)
|
||||||
assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, admin), true)
|
assert.equal(jobVisibleToIdentity({ ownerEmpNo: UNASSIGNED_OWNER }, admin), true)
|
||||||
assert.equal(canViewAllJobs(admin), true)
|
assert.equal(canViewAllJobs(admin), true)
|
||||||
})
|
})
|
||||||
|
|
@ -82,14 +83,23 @@ test('claimUnassignedForViewer claims by session owner or user-workspaces cwd',
|
||||||
{ id: 'b', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u1/proj' },
|
{ id: 'b', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u1/proj' },
|
||||||
{ id: 'c', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u2/proj' },
|
{ id: 'c', ownerEmpNo: UNASSIGNED_OWNER, cwd: '/data/user-workspaces/u2/proj' },
|
||||||
{ id: 'd', ownerEmpNo: 'u2', cwd: '/data/user-workspaces/u1/x' },
|
{ id: 'd', ownerEmpNo: 'u2', cwd: '/data/user-workspaces/u1/x' },
|
||||||
|
{ id: 'e', ownerEmpNo: UNASSIGNED_OWNER, origin: { kind: 'im', peer: { botId: 'b' } } },
|
||||||
|
{ id: 'f', ownerEmpNo: UNASSIGNED_OWNER, origin: { kind: 'web', sessionId: 'foreign' } },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
const { changed, state: next } = claimUnassignedForViewer(state, user, {
|
const { changed, state: next } = claimUnassignedForViewer(state, user, {
|
||||||
getSessionOwner: (id) => (id === 's1' ? 'u1' : null),
|
getSessionOwner: (id) => {
|
||||||
|
if (id === 's1') return 'u1'
|
||||||
|
if (id === 'foreign') return 'u2'
|
||||||
|
return null
|
||||||
|
},
|
||||||
})
|
})
|
||||||
assert.equal(changed, true)
|
assert.equal(changed, true)
|
||||||
assert.equal(next.jobs[0].ownerEmpNo, 'u1')
|
assert.equal(next.jobs[0].ownerEmpNo, 'u1')
|
||||||
assert.equal(next.jobs[1].ownerEmpNo, 'u1')
|
assert.equal(next.jobs[1].ownerEmpNo, 'u1')
|
||||||
|
// Foreign user-workspaces path must not be stolen.
|
||||||
assert.equal(next.jobs[2].ownerEmpNo, UNASSIGNED_OWNER)
|
assert.equal(next.jobs[2].ownerEmpNo, UNASSIGNED_OWNER)
|
||||||
assert.equal(next.jobs[3].ownerEmpNo, 'u2')
|
assert.equal(next.jobs[3].ownerEmpNo, 'u2')
|
||||||
|
assert.equal(next.jobs[4].ownerEmpNo, UNASSIGNED_OWNER)
|
||||||
|
assert.equal(next.jobs[5].ownerEmpNo, UNASSIGNED_OWNER)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue