feat: Geteilte Links auf Dashboard anzeigen + URL-Fix

Dashboard:
- Neue Section 'Geteilte Links' zeigt alle aktiven Shares
- Pro Eintrag: Role-Icon, Text-Vorschau, Session-Name, Modus, Datum
- Buttons: Link kopieren, öffnen, löschen (mit sofortigem UI-Update)
- Section nur sichtbar wenn Shares vorhanden

Backend:
- SharedEndpoint HandleListShares erweitert: Nachricht-Preview,
  Role, Session-Name pro Share mitgeliefert

Frontend:
- apiListShares() Funktion hinzugefügt
- Bugfix: Share-URL '/#/shared/' → '/#shared/' (Extra-Slash
  verursachte 404 weil parseHash leeren Page-Namen ergab)
This commit is contained in:
Meleki committed 2026-03-05 12:01:28 +01:00
1 parent 4bdcebc1e1
commit 38a8223228
5 files changed
+148 -1

No files matched your search

+32 -1
View File
@@ -161,7 +161,38 @@ public static class SharedEndpoint
{
var userId = ctx.GetUserId();
var shares = await db.GetSharedMessagesByUser(userId);
await ctx.Response.WriteAsJsonAsync(shares);
var result = new List<object>();
foreach (var s in shares)
{
var message = await db.GetMessage(s.MessageId);
string sessionName = null;
if (message != null)
{
var chain = await db.GetChain(message.ChainId);
if (chain != null)
{
var session = await db.GetSession(chain.SessionId);
sessionName = session?.Name;
}
}
var preview = message?.Content ?? "";
if (preview.Length > 150) preview = preview.Substring(0, 150) + "…";
result.Add(new
{
s.ShareToken,
s.MessageId,
s.IncludeContext,
s.CreatedAt,
role = message?.Role,
preview,
sessionName
});
}
await ctx.Response.WriteAsJsonAsync(result);
}
private static async Task HandleDeleteShare(HttpContext ctx, DatabaseService db)
+60
View File
@@ -256,6 +256,66 @@
color: var(--text);
}
/* Shared Links */
.shared-links-list {
display: flex;
flex-direction: column;
gap: var(--badge-spacing);
}
.shared-link-row {
display: flex;
align-items: center;
gap: var(--box-spacing);
padding: var(--box-padding);
background: var(--f3-bg);
border: var(--border-width) solid var(--f3-border);
border-radius: var(--border-radius);
}
.shared-link-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: calc(var(--badge-spacing) / 2);
}
.shared-link-preview {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shared-link-preview .fa {
opacity: var(--semi-transparent);
margin-right: calc(var(--badge-spacing) / 2);
}
.shared-link-meta {
display: flex;
flex-wrap: wrap;
gap: var(--box-spacing);
opacity: var(--semi-transparent);
font-size: calc(var(--font-size) * 0.9);
}
.shared-link-actions {
display: flex;
gap: var(--badge-spacing);
flex-shrink: 0;
}
.danger-icon {
color: var(--dangerous);
opacity: var(--semi-transparent);
}
.danger-icon:hover {
opacity: 1;
}
/* Compact cards (no image) */
.card-compact {
padding: var(--box-padding);
+6
View File
@@ -80,6 +80,12 @@
</div>
<div class="card-grid" id="agentCards"></div>
</section>
<section id="sharedLinksSection" hidden>
<div class="section-header">
<h2>Geteilte Links</h2>
</div>
<div class="shared-links-list" id="sharedLinksList"></div>
</section>
</div>
</div>
</template>
+5
View File
@@ -376,6 +376,11 @@ async function apiRunCronjob(id) {
// ── Shared Message API ──
async function apiListShares() {
const resp = await fetch('/api/shared');
return resp.json();
}
async function apiShareMessage(messageId, includeContext = false) {
const resp = await fetch(`/api/messages/${messageId}/share`, {
method: 'POST',
+45
View File
@@ -325,6 +325,51 @@ registerPage('dashboard', {
// Neuer Cronjob Button
document.getElementById('newCronjobBtn').addEventListener('click', () => navigateTo('cronjob'));
// Shared Links
const shares = await apiListShares();
const sharedSection = document.getElementById('sharedLinksSection');
const sharedList = document.getElementById('sharedLinksList');
if (shares.length > 0) {
sharedSection.hidden = false;
sharedList.innerHTML = '';
for (const s of shares) {
const row = document.createElement('div');
row.className = 'shared-link-row';
const shareUrl = `${location.origin}/#shared/${s.shareToken}`;
const modeLabel = s.includeContext ? 'Thread' : 'Einzeln';
const roleIcon = s.role === 'user' ? 'fa-user' : 'fa-robot';
row.innerHTML = `
<div class="shared-link-info">
<span class="shared-link-preview"><i class="fa ${roleIcon}"></i> ${esc(s.preview || '(leer)')}</span>
<span class="shared-link-meta">
${s.sessionName ? `<span><i class="fa fa-comment"></i> ${esc(s.sessionName)}</span>` : ''}
<span><i class="fa fa-${s.includeContext ? 'comments' : 'comment-o'}"></i> ${modeLabel}</span>
<span><i class="fa fa-clock-o"></i> ${formatDate(s.createdAt)}</span>
</span>
</div>
<div class="shared-link-actions">
<button class="icon-btn" data-action="copy" title="Link kopieren"><i class="fa fa-clipboard"></i></button>
<button class="icon-btn" data-action="open" title="Öffnen"><i class="fa fa-external-link"></i></button>
<button class="icon-btn danger-icon" data-action="delete" title="Link löschen"><i class="fa fa-trash"></i></button>
</div>
`;
row.querySelector('[data-action="copy"]').addEventListener('click', async () => {
await navigator.clipboard.writeText(shareUrl);
showToast('Link kopiert!');
});
row.querySelector('[data-action="open"]').addEventListener('click', () => {
window.open(shareUrl, '_blank');
});
row.querySelector('[data-action="delete"]').addEventListener('click', async () => {
await apiDeleteShare(s.shareToken);
row.remove();
if (sharedList.children.length === 0) sharedSection.hidden = true;
showToast('Link gelöscht');
});
sharedList.appendChild(row);
}
}
},
destroy() {
if (this._closeCardMenus) {