Files
gott/wwwroot/js/pages/session-page.js
T
MelekiandClaude Opus 4.8 b23eba32b4 Let session owners reassign their session to another user
Previously a session could only be reassigned to another user via a
manual DB UPDATE. This adds it to the session edit UI for every owner of
a session (not just admins) - the ownership check in HandleUpdate
(existing.UserId == userId) already guarantees the caller owns the
session, so handing over one's own session is safe.

Backend:
- ProfileEndpoint: new GET /api/users returning a minimal user directory
  (userId/username/displayName only) for any logged-in user. The
  admin-gated /api/admin/users stays the source for sensitive fields
  (defaultPath/canUseCli/isAdmin).
- SessionEndpoint.HandleUpdate: validate the target user exists (400 if
  not) and pass allowUserReassign: true to UpdateSession.
- DatabaseService.UpdateSession: new allowUserReassign flag; only then is
  a non-empty userId applied as user_id (empty/null is ignored - a
  session must always have an owner).

Frontend:
- index.html: hidden #sessionOwnerRow with a user picker card in the
  session edit form (existing sessions only).
- cards.js: buildUserCard helper for the picker.
- session-page.js: initSessionUserPicker loads users via apiGetUsers(),
  preselects the current owner, and on an actual owner change asks for
  confirmation (irreversible - the session leaves the user's own list).
- api.js: apiGetUsers() -> /api/users.
- ARCHITECTURE.md: document /api/users.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:52:53 +02:00

355 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Session Edit Page ──
let _sessionAvatarEditor = null;
let _sessionContextMde = null;
registerPage('session', {
async init(params) {
const sessionId = params.id;
const isNew = !sessionId;
let session = null;
if (!isNew) {
session = await apiGetSession(sessionId);
if (!session) return navigateTo('dashboard');
}
const titleEl = document.getElementById('sessionPageTitle');
const nameEl = document.getElementById('sessionName');
const descEl = document.getElementById('sessionDescription');
const contextEl = document.getElementById('sessionContext');
const workspaceEl = document.getElementById('sessionWorkspace');
const agentSelect = document.getElementById('agentSelect');
const agentSelectCard = document.getElementById('agentSelectCard');
const preparerAgentSelect = document.getElementById('preparerAgentSelect');
const preparerAgentSelectCard = document.getElementById('preparerAgentSelectCard');
const reviewAgentSelect = document.getElementById('reviewAgentSelect');
const reviewAgentSelectCard = document.getElementById('reviewAgentSelectCard');
const reviewIterationsRow = document.getElementById('reviewIterationsRow');
const maxReviewIterationsEl = document.getElementById('maxReviewIterations');
const finalizerAgentSelect = document.getElementById('finalizerAgentSelect');
const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard');
const imagePreview = document.getElementById('sessionImagePreview');
const saveBtn = document.getElementById('sessionSaveBtn');
const cancelBtn = document.getElementById('sessionCancelBtn');
const deleteBtn = document.getElementById('sessionDeleteBtn');
const commandsList = document.getElementById('customCommandsList');
const addCommandBtn = document.getElementById('addCommandBtn');
const unityMcpUrlEl = document.getElementById('sessionUnityMcpUrl');
const unityMcpTokenEl = document.getElementById('sessionUnityMcpToken');
const ownerRow = document.getElementById('sessionOwnerRow');
const userSelectCard = document.getElementById('sessionUserSelectCard');
const userSelect = document.getElementById('sessionUserSelect');
const more = document.querySelector('.more');
// Avatar Editor initialisieren
_sessionAvatarEditor = AvatarEditor.init({
previewEl: imagePreview,
initialImage: (session && session.image) || null,
initialScale: (session && session.avatarScale) || 1,
initialOffsetX: (session && session.avatarOffsetX) || 0,
initialOffsetY: (session && session.avatarOffsetY) || 0
});
if (session) {
titleEl.textContent = session.name || 'Unbenannt';
nameEl.value = session.name || '';
descEl.value = session.description || '';
workspaceEl.value = session.workspace || '';
deleteBtn.hidden = false;
// Custom Commands laden
loadCommandsIntoUI(commandsList, session.customCommands);
// Unity MCP
unityMcpUrlEl.value = session.unityMcpUrl || '';
unityMcpTokenEl.value = session.unityMcpToken || '';
} else {
titleEl.textContent = 'Neue Session';
deleteBtn.hidden = true;
}
// EasyMDE für Kontext
_sessionContextMde = createMDE(contextEl, {
initialValue: (session && session.context) || '',
placeholder: 'Zusätzlicher Kontext für den System-Prompt (optional)'
});
// Befehl hinzufügen
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
// Telegram-Section nur für existierende Sessions
if (!isNew) {
initSessionTelegram(sessionId, session);
}
// Agents laden + Skills parallel
const agents = await apiGetAvailableAgents();
const agentMap = {};
for (const a of agents) agentMap[a.agentId] = a;
const agentSkillsMap = {};
for (const a of agents) {
agentSkillsMap[a.agentId] = a.skills || [];
}
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
const inner = agent ? buildAgentCard(agent, agentSkillsMap[agent.agentId] || []) : null;
_renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'agent-select-change');
}
function updateReviewVisibility() {
reviewIterationsRow.hidden = !reviewAgentSelect.value;
}
// Helper: Agent-Picker für eine Rolle einrichten (Initial-Rendering + Click-Handler)
function setupAgentPicker(cardEl, selectEl, initialId, emptyIcon, emptyText, onChange) {
if (initialId) {
selectEl.value = initialId;
renderAgentInCard(cardEl, agentMap[initialId] || null, emptyIcon, emptyText);
} else {
renderAgentInCard(cardEl, null, emptyIcon, emptyText);
}
cardEl.addEventListener('click', () => {
openAgentPickerModal(agents, agentSkillsMap, selectEl.value, (selectedId) => {
selectEl.value = selectedId || '';
renderAgentInCard(cardEl, selectedId ? agentMap[selectedId] : null, emptyIcon, emptyText);
if (onChange) onChange(selectedId);
});
});
}
setupAgentPicker(agentSelectCard, agentSelect, session && session.agentId, 'fa-user-circle-o', 'Kein Agent ausgewählt');
setupAgentPicker(preparerAgentSelectCard, preparerAgentSelect, session && session.preparerAgentId, 'fa-filter', 'Kein Vorbereiter-Agent');
setupAgentPicker(reviewAgentSelectCard, reviewAgentSelect, session && session.reviewAgentId, 'fa-check-circle-o', 'Kein Review-Agent', updateReviewVisibility);
if (session && session.reviewAgentId) maxReviewIterationsEl.value = session.maxReviewIterations || 3;
updateReviewVisibility();
setupAgentPicker(finalizerAgentSelectCard, finalizerAgentSelect, session && session.finalizerAgentId, 'fa-flag-checkered', 'Kein Finalizer-Agent');
// User-Zuweisung (jeder Besitzer, nur für bestehende Sessions)
if (!isNew) {
initSessionUserPicker(ownerRow, userSelectCard, userSelect, session && session.userId);
}
initMoreButton(more);
// Workspace Browse
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
if (workspaceBrowse) {
workspaceBrowse.addEventListener('click', () => {
const defaultPath = (currentUser && currentUser.defaultPath) || '';
const currentVal = workspaceEl.value.trim();
// Für den Folder-Browser den absoluten Startpfad zusammenbauen
let startPath = '/';
if (defaultPath && currentVal) {
startPath = defaultPath.replace(/\/+$/, '') + '/' + currentVal;
} else if (defaultPath) {
startPath = defaultPath;
} else if (currentVal) {
startPath = currentVal;
}
openFolderBrowser({
startPath,
basePath: defaultPath || null,
onSelect(absolutePath) {
// Relativen Pfad zum DefaultPath berechnen
if (defaultPath && absolutePath.startsWith(defaultPath)) {
let rel = absolutePath.slice(defaultPath.length).replace(/^\/+/, '');
workspaceEl.value = rel || '';
} else {
// Kein DefaultPath oder Pfad außerhalb → absoluten Pfad beibehalten (Legacy)
workspaceEl.value = absolutePath;
}
}
});
});
}
// Speichern
saveBtn.addEventListener('click', async () => {
const commands = getCommandsFromUI(commandsList);
const data = {
name: nameEl.value.trim() || 'Unbenannt',
description: descEl.value.trim(),
image: _sessionAvatarEditor.getImage(),
agentId: agentSelect.value || null,
reviewAgentId: reviewAgentSelect.value || null,
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
finalizerAgentId: finalizerAgentSelect.value || null,
preparerAgentId: preparerAgentSelect.value || null,
context: _sessionContextMde.value().trim() || null,
workspace: workspaceEl.value.trim() || null,
customCommands: commands,
avatarScale: _sessionAvatarEditor.getScale(),
avatarOffsetX: _sessionAvatarEditor.getOffsetX(),
avatarOffsetY: _sessionAvatarEditor.getOffsetY(),
unityMcpUrl: unityMcpUrlEl.value.trim() || null,
unityMcpToken: unityMcpTokenEl.value.trim() || null
};
// Session einem anderen Benutzer zuweisen. Wechselt der Besitzer tatsächlich,
// erst rückfragen – die Session verschwindet danach aus der eigenen Übersicht.
if (!isNew && userSelect.value && userSelect.value !== (session && session.userId)) {
var targetLabel = userSelect.dataset.userLabel || 'einem anderen Benutzer';
if (!confirm('Diese Session an "' + targetLabel + '" zuweisen? Sie verschwindet danach aus deiner Übersicht.')) {
return;
}
data.userId = userSelect.value;
}
if (isNew) {
await apiCreateSession(data);
} else {
await apiUpdateSession(sessionId, data);
// Telegram-Config mitspeichern
try {
var tgBotId = document.getElementById('telegramBotSelect').value || '';
var tgChatId = document.getElementById('sessionTelegramChatId').value.trim();
await apiSessionTelegramSave(sessionId, tgBotId, tgChatId);
} catch(e) {
showToast('Session gespeichert, aber Telegram-Fehler: ' + e.message, 'error');
return;
}
}
navigateTo('dashboard');
});
// Abbrechen
cancelBtn.addEventListener('click', () => navigateTo('dashboard'));
// Löschen
deleteBtn.addEventListener('click', async () => {
if (!confirm('Session wirklich löschen?')) return;
await apiDeleteSession(sessionId);
navigateTo('dashboard');
});
},
destroy() {
if (_sessionContextMde) {
_sessionContextMde.toTextArea();
_sessionContextMde = null;
}
if (_sessionAvatarEditor) {
_sessionAvatarEditor.destroy();
_sessionAvatarEditor = null;
}
}
});
function openAgentPickerModal(agents, agentSkillsMap, currentId, onSelect) {
const items = agents.map(a => {
a._pickerId = a.agentId;
return a;
});
_openPickerModal('Agent auswählen', items, currentId, onSelect, {
noneLabel: 'Kein Agent',
buildCard: (a) => buildAgentCard(a, agentSkillsMap[a.agentId] || [])
});
}
function initSessionTelegram(sessionId, session) {
var botSelectCard = document.getElementById('telegramBotSelectCard');
var botSelect = document.getElementById('telegramBotSelect');
var chatIdInput = document.getElementById('sessionTelegramChatId');
var unlinkBtn = document.getElementById('sessionTelegramUnlink');
// Unlink-Button
unlinkBtn.addEventListener('click', function() {
if (!chatIdInput.value) return;
if (!confirm('Telegram-Verknüpfung wirklich lösen?')) return;
chatIdInput.value = '';
showToast('Chat-ID entfernt. Speichern nicht vergessen.', 'info');
});
// Config laden
apiSessionTelegramGet(sessionId).then(function(config) {
if (config.chatId) chatIdInput.value = config.chatId;
if (config.telegramBotId) botSelect.value = config.telegramBotId;
// Bot-Picker initialisieren
initTelegramBotPicker(botSelectCard, botSelect, config.telegramBotId);
}).catch(function() {
initTelegramBotPicker(botSelectCard, botSelect, null);
});
}
async function initTelegramBotPicker(cardEl, selectEl, currentBotId) {
var bots = [];
try {
var result = await apiGetAvailableTelegramBots();
if (Array.isArray(result)) bots = result;
} catch(e) { /* ignore */ }
var botMap = {};
for (var i = 0; i < bots.length; i++) botMap[bots[i].botId] = bots[i];
function renderBotInCard(bot) {
var inner = bot ? buildTelegramBotCard(bot) : null;
_renderCardPreview(cardEl, inner, 'fa-paper-plane', 'Kein Bot zugewiesen', 'tg-bot-select-change');
}
if (currentBotId && botMap[currentBotId]) {
selectEl.value = currentBotId;
renderBotInCard(botMap[currentBotId]);
} else {
renderBotInCard(null);
}
cardEl.addEventListener('click', function() {
openTelegramBotPickerModal(bots, selectEl.value, function(selectedBotId) {
selectEl.value = selectedBotId || '';
renderBotInCard(selectedBotId ? botMap[selectedBotId] : null);
});
});
}
// Session einem anderen Benutzer zuweisen (jeder Besitzer seiner eigenen Session).
async function initSessionUserPicker(rowEl, cardEl, selectEl, currentOwnerId) {
var users = [];
try {
var result = await apiGetUsers();
if (Array.isArray(result)) users = result;
} catch (e) {
// Fehler beim Laden → Zuweisung bleibt versteckt
return;
}
var userMap = {};
for (var i = 0; i < users.length; i++) userMap[users[i].userId] = users[i];
function userLabel(user) {
return user ? (user.displayName || user.username || '') : '';
}
function renderUserInCard(user) {
var inner = user ? buildUserCard(user) : null;
_renderCardPreview(cardEl, inner, 'fa-user', 'Kein Benutzer', 'agent-select-change');
selectEl.dataset.userLabel = userLabel(user);
}
selectEl.value = currentOwnerId || '';
renderUserInCard(userMap[currentOwnerId] || null);
rowEl.hidden = false;
cardEl.addEventListener('click', function() {
openUserPickerModal(users, selectEl.value, function(selectedUserId) {
// "Keine Änderung" (null) ignorieren – eine Session muss immer einen Besitzer haben.
if (!selectedUserId) return;
selectEl.value = selectedUserId;
renderUserInCard(userMap[selectedUserId] || null);
});
});
}
function openUserPickerModal(users, currentId, onSelect) {
var items = users.map(function(u) {
u._pickerId = u.userId;
return u;
});
_openPickerModal('Benutzer auswählen', items, currentId, onSelect, {
noneLabel: 'Keine Änderung',
noneIcon: 'fa-user',
buildCard: function(u) { return buildUserCard(u); }
});
}