Files
gott/wwwroot/js/cards.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

169 lines
8.1 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.
// ── Avatar Transform Helper ──
function _buildAvatarTransformStyle(obj) {
const parts = [];
if (obj.avatarScale && obj.avatarScale !== 1) parts.push(`scale(${obj.avatarScale})`);
if (obj.avatarOffsetX || obj.avatarOffsetY) parts.push(`translate(${obj.avatarOffsetX || 0}%, ${obj.avatarOffsetY || 0}%)`);
return parts.length ? ` style="transform: ${parts.join(' ')}"` : '';
}
// ── Session Card Helper ──
// Erzeugt ein .card-Element für eine Session (wiederverwendbar für Dashboard + Picker).
function buildSessionCard(session, agent) {
const card = document.createElement('div');
card.className = 'card';
const transformStyle = _buildAvatarTransformStyle(session);
const imageHtml = session.image ? `<img src="${session.image}" alt=""${transformStyle}>` : '';
// Badges
const badges = [];
if (agent) badges.push(`<span class="badge">${esc(agent.name)}</span>`);
if (session.reviewAgentId) badges.push(`<span class="badge badge-feature" title="Review-Agent"><i class="fa fa-check-circle-o"></i></span>`);
if (session.finalizerAgentId) badges.push(`<span class="badge badge-feature" title="Finalizer-Agent"><i class="fa fa-flag-checkered"></i></span>`);
if (session.context) badges.push(`<span class="badge badge-feature" title="${esc(session.context)}"><i class="fa fa-file-text-o"></i></span>`);
if (session.telegramConfigured) badges.push(`<span class="badge ${session.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${session.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
// Meta
const metaParts = [];
if (session.messageCount) metaParts.push(`<span class="card-meta-item"><i class="fa fa-comment"></i> ${esc(session.messageCount + ' Nachrichten')}</span>`);
if (session.archiveMessageCount) metaParts.push(`<span class="card-meta-item"><i class="fa fa-archive"></i> ${esc(session.archiveMessageCount + ' Nachrichten archiviert')}</span>`);
if (session.workspace) metaParts.push(`<span class="card-meta-item card-workspace" title="${esc(session.workspace)}"><i class="fa fa-folder"></i> ${esc(session.workspace)}</span>`);
card.innerHTML = `
<div class="card-image">${imageHtml}</div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(session.name || 'Unbenannt')}</span>
${badges.join('')}
</div>
${metaParts.length ? `<div class="card-meta">${metaParts.join('')}</div>` : ''}
</div>
`;
return card;
}
// ── Telegram Bot Card Helper ──
function buildTelegramBotCard(bot) {
const card = document.createElement('div');
card.className = 'card card-compact';
const badges = [];
if (bot.isPublic) {
badges.push('<span class="badge badge-feature"><i class="fa fa-globe"></i> Public</span>');
}
if (bot.isOwner === false) {
badges.push('<span class="badge badge-off"><i class="fa fa-user"></i> Fremd</span>');
}
if (bot.sessionCount > 0) {
badges.push(`<span class="badge badge-feature"><i class="fa fa-comment"></i> ${bot.sessionCount} ${bot.sessionCount === 1 ? 'Session' : 'Sessions'}</span>`);
}
// Meta-Items nur anzeigen wenn Daten vorhanden
const metaParts = [];
if (bot.botToken) {
const maskedToken = '•••' + bot.botToken.slice(-4);
metaParts.push(`<span class="card-meta-item"><i class="fa fa-key"></i> ${esc(maskedToken)}</span>`);
}
if (bot.createdAt) {
metaParts.push(`<span class="card-meta-item"><i class="fa fa-clock-o"></i> ${formatDate(bot.createdAt)}</span>`);
}
card.innerHTML = `
<div class="card-body">
<div class="card-title-row">
<span class="card-title"><i class="fa fa-paper-plane"></i> ${esc(bot.name)}</span>
${badges.join('')}
</div>
${metaParts.length ? `<div class="card-meta">${metaParts.join('')}</div>` : ''}
</div>
`;
return card;
}
// ── User Card Helper ──
// Erzeugt ein kompaktes .card-Element für einen Benutzer (für den Zuweisungs-Picker).
function buildUserCard(user) {
const card = document.createElement('div');
card.className = 'card card-compact';
const badges = [];
if (user.isAdmin) {
badges.push('<span class="badge badge-feature"><i class="fa fa-shield"></i> Admin</span>');
}
const metaParts = [];
metaParts.push(`<span class="card-meta-item"><i class="fa fa-at"></i> ${esc(user.username)}</span>`);
const title = user.displayName || user.username;
card.innerHTML = `
<div class="card-body">
<div class="card-title-row">
<span class="card-title"><i class="fa fa-user"></i> ${esc(title)}</span>
${badges.join('')}
</div>
${metaParts.length ? `<div class="card-meta">${metaParts.join('')}</div>` : ''}
</div>
`;
return card;
}
// ── Agent Card Helper ──
// Erzeugt ein .card-Element für einen Agent (wiederverwendbar für Dashboard + Picker).
function buildAgentCard(agent, skills) {
const card = document.createElement('div');
card.className = 'card';
const agentTransformStyle = _buildAvatarTransformStyle(agent);
const imageHtml = agent.avatar ? `<img src="${agent.avatar}" alt=""${agentTransformStyle}>` : '';
// Badges
const badges = [];
if (agent.allowUserChoice) badges.push('<span class="badge badge-feature" title="Rückfragen per Auswahl erlaubt"><i class="fa fa-question-circle"></i> Rückfragen</span>');
if (agent.autoCommit === 'mention') badges.push('<span class="badge badge-feature" title="Git-Commits nur auf Anweisung"><i class="fa fa-code-fork"></i> Anmerken</span>');
if (agent.autoCommit === 'enforce') badges.push('<span class="badge badge-forced-feature" title="Git-Commits nach jeder Änderung erzwungen"><i class="fa fa-code-fork"></i> Erzwungen</span>');
if (agent.autoCommit === 'forbid') badges.push('<span class="badge badge-forced-feature" title="Git-Commits strikt verboten"><i class="fa fa-code-fork"></i> Verboten</span>');
if (agent.useSoul) badges.push('<span class="badge badge-feature" title="Hat eine Seele (SOUL.md)"><i class="fa fa-heart"></i> Seele</span>');
if (agent.useIdentity) badges.push('<span class="badge badge-feature" title="Hat eine Identität (IDENTITY.md)"><i class="fa fa-id-badge"></i> Ich</span>');
if (agent.useUserInfo) badges.push('<span class="badge badge-feature" title="Kennt sein Gegenüber (Benutzer-Persona)"><i class="fa fa-user"></i> Du</span>');
if (agent.useMemory) badges.push('<span class="badge badge-feature" title="Zugriff auf Erinnerungen (Memory-Tools)"><i class="fa fa-database"></i> Erinnerungen</span>');
if (agent.useAutoContext) badges.push('<span class="badge badge-feature" title="Relevante Erinnerungen werden automatisch abgerufen"><i class="fa fa-magic"></i> Auto-Kontext</span>');
if (agent.useGitHubSearch) badges.push('<span class="badge badge-feature" title="Öffentlichen Code auf GitHub durchsuchen"><i class="fa fa-github"></i> GitHub Search</span>');
if (agent.useSerperSearch) badges.push('<span class="badge badge-feature" title="Google-Suche via Serper.dev API"><i class="fa fa-google"></i> Serper Search</span>');
if (agent.webSearchVersion) {
const wsYear = agent.webSearchVersion.includes('2025') ? 'Version 2025' : 'v2026';
badges.push(`<span class="badge badge-feature" title="Web-Suche - ${wsYear}"><i class="fa fa-search"></i> Web-Suche</span>`);
}
if (agent.isPublic) badges.push('<span class="badge badge-feature" title="Public – für alle User verfügbar"><i class="fa fa-globe"></i> Public</span>');
if (agent.isOwner === false) badges.push('<span class="badge badge-other" title="Gehört einem anderen User"><i class="fa fa-user-o"></i> Fremd</span>');
if (skills) {
for (const skill of skills) {
badges.push(`<span class="badge badge-skill" title="${esc(skill.description || skill.slug)}"><i class="fa fa-puzzle-piece"></i> ${esc(skill.name)}</span>`);
}
}
card.innerHTML = `
<div class="card-image">${imageHtml}</div>
<div class="card-body">
<div class="card-title-row">
<span class="card-title">${esc(agent.name)}</span>
<span class="badge">${prettyModelName(agent.model)}</span>
</div>
${badges.length ? `<div class="card-badges">${badges.join('')}</div>` : ''}
</div>
`;
return card;
}