Release 0.7.1

This commit is contained in:
hnzio committed 2026-10-25 23:29:00 +01:00
1 parent 6d96900d81
commit a08b02c34b
29 files changed
+1078 -401

No files matched your search

+1 -1
View File
@@ -51,7 +51,7 @@
</header>
<div class="shared-rundown">
{#each data.share.sections as section (section)}
<section class="quiet-panel shared-section">
<section class="shared-section">
<header>
<div>
{#if section.category}<small
+111 -72
View File
@@ -77,14 +77,19 @@
: draft.presentationProvider === 'tiktok' && draft.presentationProviderId
? `https://www.tiktok.com/player/v1/${draft.presentationProviderId}`
: '',
images: selectedMedia ? [{ id: selectedMedia.id, altText: selectedMedia.altText }] : []
images:
selectedMedia && ['text', 'image'].includes(draft.presentationMode)
? [{ id: selectedMedia.id, altText: selectedMedia.altText }]
: []
});
let presentationPreviewImage = $derived(
selectedMedia
selectedMedia && ['text', 'image'].includes(draft.presentationMode)
? draft.presentationMode === 'text'
? '/media'
: `/media/${selectedMedia.id}`
: sourcePreviewImage
: draft.presentationMode === 'link'
? sourcePreviewImage
: ''
);
let pendingHref: string | null = null;
let allowNavigation = false;
@@ -116,7 +121,7 @@
if (
control instanceof HTMLInputElement &&
control.name === 'presentationMode' &&
control.value !== 'text' &&
['link', 'video'].includes(control.value) &&
draft.presentationSourcePosition === null
) {
return;
@@ -348,10 +353,11 @@
/></label
>
<label
>Vorbereitungsaufwand<select name="preparationEffort" bind:value={draft.preparationEffort}
><option value={null}>Nicht angegeben</option><option value="low">Gering</option><option
value="medium">Mittel</option
><option value="high">Hoch</option></select
>Vorbereitungsaufwand<select
name="preparationEffort"
bind:value={draft.preparationEffort}
><option value={null}>Nicht angegeben</option><option value="low">Gering</option
><option value="medium">Mittel</option><option value="high">Hoch</option></select
></label
>
<label
@@ -533,21 +539,7 @@
</div>
<div class="piece-presentation-layout">
<div class="piece-presentation-controls">
<label
>Präsentationstitel<input
name="outputTitle"
maxlength="300"
placeholder="Optional, sonst interner Titel"
bind:value={draft.outputTitle}
/></label
>
<MarkdownEditorField
name="presentationText"
label="Öffentlicher Text"
placeholder="Text für die Publikumsausgabe"
rows={8}
bind:value={draft.presentationText}
/>
<p class="field-label">Präsentationsart</p>
<div class="presentation-mode-picker" role="group" aria-label="Darstellung">
{#each [['text', 'Text'], ['image', 'Nur Bild'], ['link', 'Linkvorschau'], ['video', 'Video']] as mode (mode[0])}<label
><input
@@ -558,60 +550,103 @@
/>{mode[1]}</label
>{/each}
</div>
<div class="presentation-settings">
{#if draft.presentationMode === 'text'}
<label
>Quelle<select
name="presentationSourcePosition"
bind:value={draft.presentationSourcePosition}
onchange={presentationSourceChanged}
><option value={null}>Quelle auswählen</option
>{#each sources as source, index (index)}{#if source.url}<option value={index}
>{source.label || source.url}</option
>{/if}{/each}</select
></label
>
<label
>Kartentitel<input
name="presentationTitle"
>Öffentliche Überschrift<input
name="outputTitle"
maxlength="300"
placeholder="Optionaler Anzeigetitel"
bind:value={draft.presentationTitle}
oninput={() => (draft.presentationTitleManual = true)}
placeholder="Optional, sonst interner Titel"
bind:value={draft.outputTitle}
/></label
>
<label
>Bild-URL<input
name="presentationImageUrl"
type="url"
maxlength="2000"
placeholder="https://…"
bind:value={draft.presentationImageUrl}
oninput={() => (draft.presentationImageManual = true)}
/></label
>
<label class="wide-field"
>Anreißer<textarea
name="presentationDescription"
rows="3"
maxlength="1000"
placeholder="Kurzer Text zur Vorschau"
bind:value={draft.presentationDescription}
oninput={() => (draft.presentationDescriptionManual = true)}></textarea></label
>
<div class="presentation-preview-actions wide-field">
<button
class="secondary-button"
type="button"
onclick={() => previewForm.requestSubmit()}
disabled={actionsBlocked || draft.presentationSourcePosition === null}
>Vorschau laden</button
><small
>{draft.presentationMetadataFetchedAtUtc
? `Zuletzt geladen: ${new Date(draft.presentationMetadataFetchedAtUtc).toLocaleString('de-DE')}`
: 'Noch keine Metadaten geladen.'}</small
<MarkdownEditorField
name="presentationText"
label="Öffentlicher Vorspann"
placeholder="Text für die Publikumsausgabe"
rows={8}
bind:value={draft.presentationText}
/>
{:else}
<input type="hidden" name="outputTitle" value={draft.outputTitle} />
<input type="hidden" name="presentationText" value={draft.presentationText} />
{/if}
{#if draft.presentationMode === 'link' || draft.presentationMode === 'video'}
<div class="presentation-settings">
<label
>{draft.presentationMode === 'video' ? 'Video-URL' : 'Quelle/URL'}<select
name="presentationSourcePosition"
bind:value={draft.presentationSourcePosition}
onchange={presentationSourceChanged}
><option value={null}>Quelle auswählen</option
>{#each sources as source, index (index)}{#if source.url}<option value={index}
>{source.label || source.url}</option
>{/if}{/each}</select
></label
>
<label
>{draft.presentationMode === 'video' ? 'Videotitel' : 'Kartentitel'}<input
name="presentationTitle"
maxlength="300"
placeholder="Optionaler Anzeigetitel"
bind:value={draft.presentationTitle}
oninput={() => (draft.presentationTitleManual = true)}
/></label
>
{#if draft.presentationMode === 'link'}
<label
>Vorschaubild-URL<input
name="presentationImageUrl"
type="url"
maxlength="2000"
placeholder="Wird aus der Linkvorschau geladen"
bind:value={draft.presentationImageUrl}
oninput={() => (draft.presentationImageManual = true)}
/></label
>
{:else}
<input
type="hidden"
name="presentationImageUrl"
value={draft.presentationImageUrl}
/>
{/if}
<label class="wide-field"
>Anreißer<textarea
name="presentationDescription"
rows="3"
maxlength="1000"
placeholder="Kurzer Text zur Vorschau"
bind:value={draft.presentationDescription}
oninput={() => (draft.presentationDescriptionManual = true)}></textarea></label
>
<div class="presentation-preview-actions wide-field">
<button
class="secondary-button"
type="button"
onclick={() => previewForm.requestSubmit()}
disabled={actionsBlocked || draft.presentationSourcePosition === null}
>Vorschau laden</button
><small
>{draft.presentationMetadataFetchedAtUtc
? `Zuletzt geladen: ${new Date(draft.presentationMetadataFetchedAtUtc).toLocaleString('de-DE')}`
: 'Noch keine Metadaten geladen.'}</small
>
</div>
</div>
</div>
{:else}
<input
type="hidden"
name="presentationSourcePosition"
value={draft.presentationSourcePosition ?? ''}
/>
<input type="hidden" name="presentationTitle" value={draft.presentationTitle} />
<input
type="hidden"
name="presentationDescription"
value={draft.presentationDescription}
/>
<input type="hidden" name="presentationImageUrl" value={draft.presentationImageUrl} />
{/if}
</div>
<aside class="piece-presentation-preview" aria-label="Präsentationsvorschau">
<PresentationDisplay
@@ -646,7 +681,11 @@
</section>
</form>
<section class="piece-editor-panel piece-media-panel" hidden={activeSection !== 'presentation'}>
<section
class="piece-editor-panel piece-media-panel"
hidden={activeSection !== 'presentation' ||
!['text', 'image'].includes(draft.presentationMode)}
>
<div class="piece-subsection-heading">
<div>
<p class="eyebrow">Hochgeladene Bilder</p>
+54 -7
View File
@@ -16,6 +16,7 @@ import {
rotatePresentationChannelKey,
resetSectionTimer,
resetTotalTimer,
resumeUsage,
resumeEndedLive,
resumeLive,
selectUsage,
@@ -25,7 +26,13 @@ import {
startOrResumeLive
} from '#lib/server/live.js';
import { listShows } from '#lib/server/shows.js';
import { createShare, deleteShare, listShares, updateShare } from '#lib/server/shares.js';
import {
createShare,
deleteShare,
listShares,
replaceShareToken,
updateShare
} from '#lib/server/shares.js';
import { berlinLocalToUtc } from '#lib/server/time.js';
function text(data: FormData, name: string) {
@@ -99,10 +106,16 @@ export const actions = {
const data = await request.formData();
try {
const share = await createShare(actor, {
showId: text(data, 'showId'), scope: text(data, 'scope'), password: text(data, 'password'),
showId: text(data, 'showId'),
scope: text(data, 'scope'),
password: text(data, 'password'),
expiresAtUtc: berlinLocalToUtc(text(data, 'expiresAt'), 'INVALID_EXPIRY')
});
return { intent: 'createShare', success: 'Leselink angelegt.', shareUrl: `/freigabe/${share.token}` };
return {
intent: 'createShare',
success: 'Leselink angelegt.',
shareUrl: `/freigabe/${share.token}`
};
} catch (cause) {
const problem = actionError(cause);
return fail(problem.status, { intent: 'createShare', ...problem });
@@ -112,16 +125,38 @@ export const actions = {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
try {
await updateShare(actor, text(data, 'shareId'), {
scope: text(data, 'scope'), passwordMode: text(data, 'passwordMode'), password: text(data, 'password'),
expiresAtUtc: berlinLocalToUtc(text(data, 'expiresAt'), 'INVALID_EXPIRY')
}, text(data, 'showId'));
await updateShare(
actor,
text(data, 'shareId'),
{
scope: text(data, 'scope'),
passwordMode: text(data, 'passwordMode'),
password: text(data, 'password'),
expiresAtUtc: berlinLocalToUtc(text(data, 'expiresAt'), 'INVALID_EXPIRY')
},
text(data, 'showId')
);
return { intent: 'updateShare', success: 'Freigabe aktualisiert.' };
} catch (cause) {
const problem = actionError(cause);
return fail(problem.status, { intent: 'updateShare', ...problem });
}
},
replaceShare: async ({ locals, request }) => {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
try {
const token = replaceShareToken(actor, text(data, 'shareId'), text(data, 'showId'));
return {
intent: 'replaceShare',
success: 'Ersatzlink angelegt. Der bisherige Link ist ungültig.',
shareUrl: `/freigabe/${token}`
};
} catch (cause) {
const problem = actionError(cause);
return fail(problem.status, { intent: 'replaceShare', ...problem });
}
},
deleteShare: async ({ locals, request }) => {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
@@ -253,6 +288,18 @@ export const actions = {
)
);
},
resumeUsage: async ({ locals, request }) => {
requireUser(locals.user);
const data = await request.formData();
return attempt('resumeUsage', () =>
resumeUsage(
text(data, 'showId'),
text(data, 'usageId'),
text(data, 'commandId'),
Number(data.get('revision'))
)
);
},
present: async ({ locals, request }) => {
requireUser(locals.user);
const data = await request.formData();
+76 -36
View File
@@ -591,7 +591,12 @@
</section>
<section>
<strong>Ausgabekanal</strong>
{#if data.canManageShares}<ShareManagement shows={[{ ...data.live.show, startsAtUtc: null }]} shares={data.shares} fixedShowId={data.live.show.id} {form} />{/if}
{#if data.canManageShares}<ShareManagement
shows={[{ ...data.live.show, startsAtUtc: null }]}
shares={data.shares}
fixedShowId={data.live.show.id}
{form}
/>{/if}
<form method="POST" action={liveAction('createOutput')} use:enhance={enhanceOutput}>
<input type="hidden" name="showId" value={data.live.show.id} />
<button class="secondary-button" type="submit">Ausgabe verbinden und öffnen</button>
@@ -814,9 +819,14 @@
{formatClock(viewedActualSeconds)} / {formatClock(viewedUsage.durationSeconds)}
</strong>{/if}
</div>
{#if viewedSection?.twitchTags.length}<button class="text-button live-twitch-tags" type="button" onclick={copyTwitchTags}>
<AppIcon name="copy" /> {viewedSection.twitchTags.join(' · ')}
</button>{/if}
{#if viewedSection?.twitchTags.length}<button
class="text-button live-twitch-tags"
type="button"
onclick={copyTwitchTags}
>
<AppIcon name="copy" />
{viewedSection.twitchTags.join(' · ')}
</button>{/if}
{#if viewedUsage.introHtml}<section class="live-script-section live-intro">
<span>Einleitung</span>
@@ -1000,6 +1010,22 @@
>
</button>
<div class="live-run-actions">
{#if usage.status === 'done' || usage.status === 'skipped'}<form
method="POST"
action={liveAction('resumeUsage')}
use:enhance={enhanceCommand}
>
<input type="hidden" name="showId" value={data.live.show.id} />
<input type="hidden" name="usageId" value={usage.id} />
<input type="hidden" name="revision" value={data.live.liveRevision} />
<input type="hidden" name="commandId" value={data.commandId} />
<button
class="text-button"
type="submit"
disabled={data.live.isPaused || Boolean(data.live.endedAtUtc)}
>Wiederaufnehmen</button
>
</form>{/if}
{#if usage.status === 'open'}<details class="live-run-order">
<summary aria-label={`${usage.title} sortieren`}
><AppIcon name="ellipsis" /></summary
@@ -1176,38 +1202,52 @@
: 'Präsentieren'}
</button>
</form>
<form method="POST" action={liveAction('skip')} use:enhance={enhanceCommand}>
<input type="hidden" name="showId" value={data.live.show.id} />
<input type="hidden" name="revision" value={data.live.liveRevision} />
<input type="hidden" name="commandId" value={data.commandId} />
<button
class="secondary-button"
type="submit"
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
title={!viewingLiveCurrent
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
: undefined}>Überspringen</button
>
</form>
<form method="POST" action={liveAction('complete')} use:enhance={enhanceCommand}>
<input type="hidden" name="showId" value={data.live.show.id} />
<input type="hidden" name="revision" value={data.live.liveRevision} />
<input type="hidden" name="commandId" value={data.commandId} />
<button
class="primary-button"
type="submit"
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
title={!viewingLiveCurrent
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
: undefined}
>
{data.live.next
? 'Erledigen & weiter'
: hasOtherOpenUsages
? 'Erledigen'
: 'Erledigen & Sendungsende'}
</button>
</form>
{#if (viewedUsage.status === 'done' || viewedUsage.status === 'skipped') && !viewingLiveCurrent}
<form method="POST" action={liveAction('resumeUsage')} use:enhance={enhanceCommand}>
<input type="hidden" name="showId" value={data.live.show.id} />
<input type="hidden" name="usageId" value={viewedUsage.id} />
<input type="hidden" name="revision" value={data.live.liveRevision} />
<input type="hidden" name="commandId" value={data.commandId} />
<button
class="primary-button"
type="submit"
disabled={data.live.isPaused || Boolean(data.live.endedAtUtc)}>Wiederaufnehmen</button
>
</form>
{:else}
<form method="POST" action={liveAction('skip')} use:enhance={enhanceCommand}>
<input type="hidden" name="showId" value={data.live.show.id} />
<input type="hidden" name="revision" value={data.live.liveRevision} />
<input type="hidden" name="commandId" value={data.commandId} />
<button
class="secondary-button"
type="submit"
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
title={!viewingLiveCurrent
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
: undefined}>Überspringen</button
>
</form>
<form method="POST" action={liveAction('complete')} use:enhance={enhanceCommand}>
<input type="hidden" name="showId" value={data.live.show.id} />
<input type="hidden" name="revision" value={data.live.liveRevision} />
<input type="hidden" name="commandId" value={data.commandId} />
<button
class="primary-button"
type="submit"
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
title={!viewingLiveCurrent
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
: undefined}
>
{data.live.next
? 'Erledigen & weiter'
: hasOtherOpenUsages
? 'Erledigen'
: 'Erledigen & Sendungsende'}
</button>
</form>
{/if}
{:else if sessionComplete}<strong class="live-finished-label">Ablauf abgeschlossen</strong
>{/if}
</div>
+7 -3
View File
@@ -2,8 +2,12 @@ import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { berlinDateKey, berlinMonthKey, normalizeMonthKey, shiftMonth } from '#lib/calendar.js';
import { getShowOverviewView, requireUser, setShowOverviewView } from '#lib/server/auth.js';
import { createShow, listShows, listShowTemplates } from '#lib/server/shows.js';
import { listPieces } from '#lib/server/content.js';
import {
createShow,
listPreparationUsages,
listShows,
listShowTemplates
} from '#lib/server/shows.js';
import { berlinLocalToUtc } from '#lib/server/time.js';
export const load: PageServerLoad = ({ locals, url }) => {
@@ -13,7 +17,7 @@ export const load: PageServerLoad = ({ locals, url }) => {
const selectedMonth = normalizeMonthKey(url.searchParams.get('month'), todayMonth);
return {
shows: listShows(['draft', 'ready', 'live']),
preparationPieces: listPieces({ statuses: ['preparing'] }),
preparationUsages: listPreparationUsages(),
templates: listShowTemplates(),
view: getShowOverviewView(user.id),
selectedMonth,
+38 -6
View File
@@ -53,6 +53,10 @@
}).format(new Date(`${value}T12:00:00Z`));
}
function preparationHref(showId: string, usageId: string) {
return `${resolve('/sendungen/[id]', { id: showId })}?usage=${encodeURIComponent(usageId)}#usage-${encodeURIComponent(usageId)}`;
}
function statusLabel(status: string) {
if (status === 'ready') return 'Bereit';
if (status === 'draft') return 'Entwurf';
@@ -125,13 +129,41 @@
</form>
</details>
{#if data.preparationPieces.length}<section class="show-library preparation-overview" aria-labelledby="preparation-title">
<header><h2 id="preparation-title">Noch vorzubereiten</h2><span>{data.preparationPieces.length}</span></header>
{#if data.preparationUsages.length}<section
class="show-library preparation-overview"
aria-labelledby="preparation-title"
>
<header>
<h2 id="preparation-title">Noch vorzubereiten</h2>
<span>{data.preparationUsages.length}</span>
</header>
<div class="preparation-list">
{#each data.preparationPieces as piece (piece.id)}<a href={resolve('/fundus/[id]', { id: piece.id })}>
<span><strong>{piece.title}</strong>{#if piece.summary}<small>{piece.summary}</small>{/if}</span>
{#if piece.preparationEffort}<b class={`effort ${piece.preparationEffort}`}>{piece.preparationEffort === 'low' ? 'Gering' : piece.preparationEffort === 'medium' ? 'Mittel' : 'Hoch'}</b>{/if}
</a>{/each}
{#each data.preparationUsages as usage (usage.usageId)}
<!-- The helper starts from a route resolved against the configured base path. -->
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
<a href={preparationHref(usage.showId, usage.usageId)}>
<div class="preparation-copy">
<span
class="preparation-topics"
aria-label={usage.topics.map((topic) => topic.name).join(', ')}
>{#each usage.topics as topic (topic.name)}<AppIcon
name={topic.iconName}
/>{/each}</span
>
<span
><strong>{usage.title}</strong><small>{usage.showTitle} · {usage.sectionName}</small
></span
>
</div>
{#if usage.preparationEffort}<b class={`effort ${usage.preparationEffort}`}
>{usage.preparationEffort === 'low'
? 'Gering'
: usage.preparationEffort === 'medium'
? 'Mittel'
: 'Hoch'}</b
>{/if}
</a>
{/each}
</div>
</section>{/if}
+21 -1
View File
@@ -4,7 +4,13 @@ import type { Actions, PageServerLoad } from './$types';
import { requireBarkeeper, requireUser } from '#lib/server/auth.js';
import { getHistoricalActualDurations } from '#lib/server/completion.js';
import { getTaxonomy, listPieces, listPieceTemplates } from '#lib/server/content.js';
import { createShare, deleteShare, listShares, updateShare } from '#lib/server/shares.js';
import {
createShare,
deleteShare,
listShares,
replaceShareToken,
updateShare
} from '#lib/server/shares.js';
import { berlinLocalToUtc } from '#lib/server/time.js';
import {
addPieceUsage,
@@ -192,6 +198,20 @@ export const actions = {
return fail(actionError(cause).status, { intent: 'updateShare', ...actionError(cause) });
}
},
replaceShare: async ({ locals, params, request }) => {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
try {
const token = replaceShareToken(actor, text(data, 'shareId'), params.id);
return {
intent: 'replaceShare',
success: 'Ersatzlink angelegt. Der bisherige Link ist ungültig.',
shareUrl: `/freigabe/${token}`
};
} catch (cause) {
return fail(actionError(cause).status, { intent: 'replaceShare', ...actionError(cause) });
}
},
deleteShare: async ({ locals, params, request }) => {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
+7 -1
View File
@@ -661,6 +661,7 @@
{#if section.usages.length}<div class="usage-list">
{#each section.usages as usage, usageIndex (usage.id)}
<div
id={`usage-${usage.id}`}
class="usage-drag-slot"
role="presentation"
ondragover={(event) => {
@@ -750,6 +751,7 @@
</form>
<UsageEditorDialog
{usage}
open={page.url.searchParams.get('usage') === usage.id}
revision={data.show.revision}
sectionId={section.id}
sections={data.show.sections}
@@ -893,7 +895,11 @@
placeholder="optional, Sekunden"
/></label
><label
>Twitch-Tags<input name="twitchTags" maxlength="300" placeholder="Gaming, Deutsch" /></label
>Twitch-Tags<input
name="twitchTags"
maxlength="300"
placeholder="Gaming, Deutsch"
/></label
><button class="primary-button" type="submit">Abschnitt hinzufügen</button>
</form>
+21 -1
View File
@@ -8,7 +8,13 @@ import {
type TaxonomyKind
} from '#lib/server/content.js';
import { getMailConfig } from '#lib/server/config.js';
import { createShare, deleteShare, listShares, updateShare } from '#lib/server/shares.js';
import {
createShare,
deleteShare,
listShares,
replaceShareToken,
updateShare
} from '#lib/server/shares.js';
import { listShows } from '#lib/server/shows.js';
import { berlinLocalToUtc } from '#lib/server/time.js';
import { createTool, listTools, updateTool, type ToolInput } from '#lib/server/tools.js';
@@ -118,6 +124,20 @@ export const actions = {
return fail(400, { intent: 'updateShare', error: actionError(cause) });
}
},
replaceShare: async ({ locals, request }) => {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
try {
const token = replaceShareToken(actor, String(data.get('shareId') || ''));
return {
intent: 'replaceShare',
success: 'Ersatzlink angelegt. Der bisherige Link ist ungültig.',
shareUrl: `/freigabe/${token}`
};
} catch (cause) {
return fail(400, { intent: 'replaceShare', error: actionError(cause) });
}
},
deleteShare: async ({ locals, request }) => {
const actor = requireBarkeeper(locals.user);
const data = await request.formData();
+75 -50
View File
@@ -33,6 +33,7 @@
{form.success}
</div>{/if}
<ShareManagement shows={data.shows} shares={data.shares} {form} />
<h2 class="section-heading">Schnittstellen</h2>
<section class="quiet-panel integration-panel">
<div class="panel-heading">
<div>
@@ -45,40 +46,43 @@
Aktive Feeds werden im gewählten Intervall abgerufen. Beiträge landen zuerst separat im
Eingang.
</p>
<form class="integration-form" method="POST" action="?/saveFeed">
<label>Name<input name="name" required maxlength="120" /></label>
<label
>Feed-URL<input
name="url"
type="url"
required
maxlength="2000"
placeholder="https://example.org/feed.xml"
/></label
>
<label
>Intervall in Minuten<input
name="refreshIntervalMinutes"
type="number"
min="15"
max="10080"
value="60"
required
/></label
>
<label
>Standardkategorie<select name="defaultCategoryId"
><option value="">Keine</option
>{#each data.taxonomy.categories as category (category.id)}<option value={category.id}
>{category.name}</option
>{/each}</select
></label
>
<label class="check-row"
><input type="checkbox" name="isEnabled" value="1" checked /> Automatisch abrufen</label
>
<button class="primary-button" type="submit">Feed anlegen</button>
</form>
<details class="integration-create">
<summary class="compact-add-button"><AppIcon name="plus" /> Feed hinzufügen</summary>
<form class="integration-form" method="POST" action="?/saveFeed">
<label>Name<input name="name" required maxlength="120" /></label>
<label
>Feed-URL<input
name="url"
type="url"
required
maxlength="2000"
placeholder="https://example.org/feed.xml"
/></label
>
<label
>Intervall in Minuten<input
name="refreshIntervalMinutes"
type="number"
min="15"
max="10080"
value="60"
required
/></label
>
<label
>Standardkategorie<select name="defaultCategoryId"
><option value="">Keine</option
>{#each data.taxonomy.categories as category (category.id)}<option value={category.id}
>{category.name}</option
>{/each}</select
></label
>
<label class="check-row"
><input type="checkbox" name="isEnabled" value="1" checked /> Automatisch abrufen</label
>
<button class="primary-button" type="submit">Feed anlegen</button>
</form>
</details>
<div class="integration-list">
{#each data.feeds as feed (feed.id)}
<details>
@@ -169,19 +173,27 @@
{#if form?.technicalToken}<div class="token-reveal" role="status">
<strong>Token, einmalig sichtbar</strong><code>{form.technicalToken}</code>
</div>{/if}
<form class="integration-form" method="POST" action="?/createTechnicalAccess">
<label
>Name<input name="name" required maxlength="120" placeholder="Redaktionsassistenz" /></label
>
<label
>Berechtigung<select name="permissions"
><option value="read">Nur lesen</option><option value="read_write"
>Lesen und schreiben</option
></select
></label
>
<button class="primary-button" type="submit">Zugang anlegen</button>
</form>
<details class="integration-create">
<summary class="compact-add-button"><AppIcon name="plus" /> Zugang hinzufügen</summary>
<form class="integration-form" method="POST" action="?/createTechnicalAccess">
<label
>Name<input
name="name"
required
maxlength="120"
placeholder="Redaktionsassistenz"
/></label
>
<label
>Berechtigung<select name="permissions"
><option value="read">Nur lesen</option><option value="read_write"
>Lesen und schreiben</option
></select
></label
>
<button class="primary-button" type="submit">Zugang anlegen</button>
</form>
</details>
<div class="integration-list">
{#each data.technicalAccesses as access (access.id)}
<div class="access-row">
@@ -456,6 +468,17 @@
gap: 0.8rem;
align-items: end;
}
.integration-create > summary {
width: fit-content;
cursor: pointer;
list-style: none;
}
.integration-create > summary::-webkit-details-marker {
display: none;
}
.integration-create[open] > .integration-form {
margin-top: 0.8rem;
}
.integration-form label:not(.check-row) {
display: grid;
gap: 0.35rem;
@@ -472,8 +495,10 @@
}
.integration-list details,
.access-row {
border-top: 1px solid var(--color-border);
padding-top: 0.8rem;
padding: 0.75rem 0.8rem;
border: 1px solid var(--line);
border-radius: var(--radius-medium);
background: var(--panel-soft);
}
.integration-list summary,
.access-row {
@@ -488,7 +513,7 @@
gap: 0.2rem;
}
.integration-list small {
color: var(--color-text-muted);
color: var(--text-muted);
}
.row-actions {
display: flex;