Stage 329: PR #1993 — fix(kanban): invalidate profile cache for assignee select by @franksong2702

This commit is contained in:
nesquena-hermes
2026-05-10 16:48:15 +00:00
2 changed files with 63 additions and 2 deletions
+17 -2
View File
@@ -1704,6 +1704,12 @@ async function createKanbanTask(){
let _kanbanTaskModalMode = 'create'; // 'create' | 'edit'
let _kanbanTaskModalEditingId = null; // task id when mode === 'edit'
let _kanbanProfileNamesCache = null; // populated lazily on first modal open
let _kanbanProfileNamesCacheAt = 0;
const _KANBAN_PROFILE_NAMES_CACHE_TTL_MS = 30000;
function _invalidateKanbanProfileCache() {
_kanbanProfileNamesCache = null;
_kanbanProfileNamesCacheAt = 0;
}
// Status the modal *displayed* on edit-mode open. If the user doesn't touch
// the dropdown, we must NOT send `status` in the PATCH payload — otherwise
// editing a task whose real status is non-editable in this dropdown
@@ -1714,9 +1720,13 @@ let _kanbanProfileNamesCache = null; // populated lazily on first modal open
let _kanbanTaskModalInitialDisplayedStatus = null;
async function _kanbanLoadProfileNames(){
// Hit /api/profiles once per session and cache; refresh is cheap if needed.
// Hit /api/profiles once per session and cache for a short TTL.
// Returns an array of profile names (sorted, default first if present).
if (Array.isArray(_kanbanProfileNamesCache)) return _kanbanProfileNamesCache;
const hasFreshCache = (
Array.isArray(_kanbanProfileNamesCache) &&
(Date.now() - _kanbanProfileNamesCacheAt) < _KANBAN_PROFILE_NAMES_CACHE_TTL_MS
);
if (hasFreshCache) return _kanbanProfileNamesCache;
try {
const data = await api('/api/profiles');
const profiles = Array.isArray(data && data.profiles) ? data.profiles : [];
@@ -1728,9 +1738,11 @@ async function _kanbanLoadProfileNames(){
return a.localeCompare(b);
});
_kanbanProfileNamesCache = names;
_kanbanProfileNamesCacheAt = Date.now();
return names;
} catch(_) {
_kanbanProfileNamesCache = [];
_kanbanProfileNamesCacheAt = Date.now();
return [];
}
}
@@ -4209,6 +4221,7 @@ async function deleteCurrentProfile(){
if(!_ok) return;
try {
await api('/api/profile/delete', { method: 'POST', body: JSON.stringify({ name }) });
_invalidateKanbanProfileCache();
_clearProfileDetail();
await loadProfilesPanel();
showToast(t('profile_deleted', name));
@@ -4486,6 +4499,7 @@ async function saveProfileForm(){
if (baseUrl) payload.base_url = baseUrl;
if (apiKey) payload.api_key = apiKey;
await api('/api/profile/create', { method: 'POST', body: JSON.stringify(payload) });
_invalidateKanbanProfileCache();
_profilePreFormDetail = null;
await loadProfilesPanel();
showToast(t('profile_created', name));
@@ -4506,6 +4520,7 @@ async function deleteProfile(name) {
if(!_delProf) return;
try {
await api('/api/profile/delete', { method: 'POST', body: JSON.stringify({ name }) });
_invalidateKanbanProfileCache();
await loadProfilesPanel();
showToast(t('profile_deleted', name));
} catch (e) { showToast(t('delete_failed') + e.message); }
+46
View File
@@ -801,6 +801,52 @@ def test_kanban_active_board_persisted_to_localstorage():
assert "_kanbanSetSavedBoard" in PANELS
def test_kanban_profile_assignee_cache_has_invalidation_path():
"""Kanban assignee suggestions should stay aligned with profile mutations.
The cache in _kanbanLoadProfileNames() can become stale when profiles are
created or deleted in the same session. This adds an explicit
invalidation path and a short TTL so modal opens recover from same-session
mutations and cross-tab/CLI changes.
"""
assert "_KANBAN_PROFILE_NAMES_CACHE_TTL_MS" in PANELS
assert "_kanbanProfileNamesCacheAt" in PANELS
assert "_invalidateKanbanProfileCache" in PANELS
load_start = PANELS.find("async function _kanbanLoadProfileNames(){")
assert load_start != -1, "Missing _kanbanLoadProfileNames() declaration"
load_end = PANELS.find("\n}\n\nasync function _kanbanPopulateAssigneeSelect", load_start)
if load_end == -1:
load_end = PANELS.find("\n}\n\nfunction openKanbanCreate", load_start)
load_body = PANELS[load_start:load_end] if load_end != -1 else PANELS[load_start:load_start + 2200]
assert "Date.now() - _kanbanProfileNamesCacheAt" in load_body
assert "_kanbanProfileNamesCacheAt = Date.now()" in load_body
save_start = PANELS.find("async function saveProfileForm(){")
assert save_start != -1, "Missing saveProfileForm() declaration"
save_end = PANELS.find("\n}\n\n// Back-compat", save_start)
save_body = PANELS[save_start:save_end if save_end != -1 else save_start + 2000]
assert "_invalidateKanbanProfileCache();" in save_body, (
"Profile create flow should invalidate Kanban assignee cache after success."
)
delete_start = PANELS.find("async function deleteProfile(name) {")
assert delete_start != -1, "Missing deleteProfile() declaration"
delete_end = PANELS.find("\n\n// ── Memory panel", delete_start)
delete_body = PANELS[delete_start:delete_end if delete_end != -1 else delete_start + 1300]
assert "_invalidateKanbanProfileCache();" in delete_body, (
"Profile delete flow should invalidate Kanban assignee cache after success."
)
ui_delete_start = PANELS.find("async function deleteCurrentProfile(){")
assert ui_delete_start != -1, "Missing deleteCurrentProfile() declaration"
ui_delete_end = PANELS.find("\n\nfunction renderProfileDropdown", ui_delete_start)
ui_delete_body = PANELS[ui_delete_start:ui_delete_end if ui_delete_end != -1 else ui_delete_start + 1300]
assert "_invalidateKanbanProfileCache();" in ui_delete_body, (
"Profile detail delete flow (deleteCurrentProfile) should invalidate Kanban assignee cache after success."
)
def test_kanban_archive_board_uses_showConfirmDialog():
"""Archive is destructive → must use the styled showConfirmDialog,
not native confirm() (which can't be styled or i18n'd)."""