diff --git a/console/app/components/ExportTargetForm.vue b/console/app/components/ExportTargetForm.vue new file mode 100644 index 0000000..8d87145 --- /dev/null +++ b/console/app/components/ExportTargetForm.vue @@ -0,0 +1,331 @@ + + + diff --git a/console/app/components/ExportTargetSecret.vue b/console/app/components/ExportTargetSecret.vue new file mode 100644 index 0000000..47a079b --- /dev/null +++ b/console/app/components/ExportTargetSecret.vue @@ -0,0 +1,202 @@ + + + diff --git a/console/app/pages/admin/destinations.vue b/console/app/pages/admin/destinations.vue new file mode 100644 index 0000000..9401a2e --- /dev/null +++ b/console/app/pages/admin/destinations.vue @@ -0,0 +1,636 @@ + + + diff --git a/console/app/pages/recordings/[id].vue b/console/app/pages/recordings/[id].vue index 114ff12..dbb14b6 100644 --- a/console/app/pages/recordings/[id].vue +++ b/console/app/pages/recordings/[id].vue @@ -50,6 +50,12 @@ import { type RecordedSession, } from '~/utils/recordings' import { recordingTabQuery, recordingTabs } from '~/utils/recordingTabs' +import { + parseSessionDocuments, + publishedProtocols, + sessionDocumentsPath, + type SessionDocument, +} from '~/utils/sessionDocuments' import { transcriptAudioErased, transcriptPath, type SessionTranscript } from '~/utils/transcript' import type { SessionName } from '~/utils/sessionNaming' import { ApiError } from '~/utils/apiError' @@ -61,6 +67,17 @@ interface Recording { * The session is authoritative for the page's existence; a transcript * that failed is one tab's problem and not five. */ transcript: SessionTranscript | null + /** + * Every destination this meeting reached, or `null` when the listing + * could not be read. + * + * `null` and `[]` are deliberately different answers. An empty list is + * a real state — a meeting still being transcribed, a guild that has + * configured nowhere to publish — and drawing a failed read as one + * would tell somebody their meeting was published nowhere when in fact + * nobody asked. See `~/utils/sessionDocuments`. + */ + documents: SessionDocument[] | null } const { t } = useI18n() @@ -77,7 +94,7 @@ const { data, status, error, refresh } = await useAsyncData( // In parallel. They are two independent reads behind the same // authorisation, and doing them one after the other would double the // wait for no answer either of them needs from the other. - const [session, transcript] = await Promise.all([ + const [session, transcript, documents] = await Promise.all([ api(`/sessions/${encodeURIComponent(sessionId)}`), // Swallowed on purpose, and only here: a transcript that cannot be // read must not take down the audio, the tags or the metadata. What @@ -86,8 +103,14 @@ const { data, status, error, refresh } = await useAsyncData( // — "the words could not be read" covers both, and the session's own // 404 is what decides whether this recording is anybody's at all. api(transcriptPath(sessionId)).catch(() => null), + // Swallowed for the same reason, and `null` rather than `[]`: the + // difference between "nothing was published" and "where this went + // could not be read" is the whole point of this list existing. + api(sessionDocumentsPath(sessionId)) + .then(parseSessionDocuments) + .catch(() => null), ]) - return { session, transcript } + return { session, transcript, documents } }, { watch: [id] }, ) @@ -111,6 +134,23 @@ const session = computed(() => { return renamed.value === null ? found : { ...found, ...renamed.value } }) const transcript = computed(() => data.value?.transcript ?? null) + +/** The listing could not be read, as opposed to being empty. Only ever + * true once the page itself has loaded, so it does not flash during the + * first render. */ +const documentsFailed = computed(() => data.value !== null && data.value?.documents === null) + +/** + * What this meeting was published as, and how that squares with the one + * link the Discord announcement carries. + * + * `~/utils/sessionDocuments` decides all of it, including the state this + * section exists for: documents with no announced link means the + * destination Sturnus announces produced nothing while the others did. + */ +const protocols = computed(() => + publishedProtocols(session.value?.document_url, data.value?.documents ?? []), +) const length = computed(() => (session.value ? sessionLength(session.value) : null)) const naming = computed(() => (session.value ? recordingNaming(session.value) : null)) @@ -336,27 +376,112 @@ function onRenamed(name: SessionName) { +
-

{{ $t('recordings.aboutHeading') }}

+

{{ $t('recordings.protocolsHeading') }}

-
+ + + + +
+ +
+

{{ $t('recordings.aboutHeading') }}