ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
"use client"
// "Your analytics data" (ADR-049): what the platform's own stores keep about the person's visits --
// session replay, errors, access logs, traces, logs and analytics -- erased or exported by one
// platform job. Both buttons call this app's server (account.analytics.*), which calls the platform
// with the deployment token; the browser holds only the ticket of the job it started, in memory.
// The erasure asks for a confirmation first, because it cannot be undone. While a job is open the
// panel polls its status; a completed export offers the download.
import { DataTable, useTransport } from "@conusai-kit/ui"
import { type ReactElement, useCallback, useEffect, useId, useState } from "react"
import type { AnalyticsView } from "../contract/procedures.js"
export interface AccountAnalyticsLabels {
readonly heading: string
readonly body: string
readonly erase: string
readonly exportSubmit: string
readonly busy: string
readonly confirmErase: string
readonly confirm: string
readonly cancel: string
readonly download: string
readonly filename: string
/** One per job status: pending, running, completed, failed, expired. */
readonly status: Readonly<Record<AnalyticsView["status"], string>>
readonly store: string
readonly storeStatus: string
readonly deleted: string
readonly anonymized: string
readonly exported: string
/** A readable name per store key; a key without one is shown as it is. */
readonly storeNames: Readonly<Record<string, string>>
/** The app's `app.failed` message, with `{detail}` still in it. */
readonly failed: string
}
/** How often an open job is asked about. */
export const ANALYTICS_POLL_MS = 5_000
type Busy = "erase" | "export" | "download" | null
function save(document: Record<string, unknown>, filename: string, exportedAt: string): void {
const href = URL.createObjectURL(new Blob([JSON.stringify(document, null, 2)], { type: "application/json" }))
const link = window.document.createElement("a")
link.href = href
link.download = `${filename}-${exportedAt.slice(0, 10)}.json`
window.document.body.append(link)
link.click()
link.remove()
setTimeout(() => URL.revokeObjectURL(href), 0)
}
const isOpen = (job: AnalyticsView | null) => job !== null && (job.status === "pending" || job.status === "running")
export function AccountAnalyticsPanel({
labels,
pollMs = ANALYTICS_POLL_MS,
}: {
labels: AccountAnalyticsLabels
pollMs?: number
}): ReactElement {
const { call, describe } = useTransport()
const headingId = useId()
const [jobs, setJobs] = useState<{ erase: AnalyticsView | null; export: AnalyticsView | null }>({
erase: null,
export: null,
})
const [busy, setBusy] = useState<Busy>(null)
const [confirming, setConfirming] = useState(false)
const [failure, setFailure] = useState<string | null>(null)
const remember = useCallback((job: AnalyticsView) => setJobs((current) => ({ ...current, [job.kind]: job })), [])
const start = async (kind: AnalyticsView["kind"]): Promise<void> => {
setBusy(kind)
setFailure(null)
setConfirming(false)
try {
remember((await call("account.analytics.request", { kind })) as AnalyticsView)
} catch (error) {
setFailure(describe(error))
} finally {
setBusy(null)
}
}
const download = async (job: AnalyticsView): Promise<void> => {
setBusy("download")
setFailure(null)
try {
const answer = (await call("account.analytics.export", { ticket: job.ticket })) as {
exportedAt: string
document: Record<string, unknown>
}
save(answer.document, labels.filename, answer.exportedAt)
} catch (error) {
setFailure(describe(error))
} finally {
setBusy(null)
}
}
// Follow every open job until it settles.
useEffect(() => {
const open = [jobs.erase, jobs.export].filter(isOpen) as AnalyticsView[]
if (open.length === 0) return
const timer = setTimeout(() => {
for (const job of open) {
call("account.analytics.status", { ticket: job.ticket })
.then((next) => remember(next as AnalyticsView))
.catch((error: unknown) => setFailure(describe(error)))
}
}, pollMs)
return () => clearTimeout(timer)
}, [jobs, call, describe, remember, pollMs])
const rows = (job: AnalyticsView) =>
Object.entries(job.stores).map(([store, outcome]) => ({
id: store,
store: labels.storeNames[store] ?? store,
status: outcome.status,
deleted: outcome.deleted,
anonymized: outcome.anonymized,
exported: outcome.exported,
}))
const jobView = (job: AnalyticsView | null) =>
job === null ? null : (
<div className="app-stack" data-kind={job.kind}>
<p role="status">{labels.status[job.status]}</p>
{Object.keys(job.stores).length > 0 ? (
<DataTable
columns={[
{ id: "store", header: labels.store },
{ id: "status", header: labels.storeStatus },
...(job.kind === "erase"
? [
{ id: "deleted" as const, header: labels.deleted },
{ id: "anonymized" as const, header: labels.anonymized },
]
: [{ id: "exported" as const, header: labels.exported }]),
]}
rows={rows(job)}
/>
) : null}
{job.kind === "export" && job.status === "completed" ? (
<button type="button" onClick={() => download(job)} disabled={busy !== null} aria-busy={busy === "download"}>
{busy === "download" ? labels.busy : labels.download}
</button>
) : null}
</div>
)
return (
<section aria-labelledby={headingId}>
<h2 id={headingId}>{labels.heading}</h2>
<p>{labels.body}</p>
<div className="app-actions">
<button
type="button"
onClick={() => start("export")}
disabled={busy !== null || isOpen(jobs.export)}
aria-busy={busy === "export"}
>
{busy === "export" ? labels.busy : labels.exportSubmit}
</button>
<button
type="button"
className="app-danger"
onClick={() => setConfirming(true)}
disabled={busy !== null || confirming || isOpen(jobs.erase)}
>
{busy === "erase" ? labels.busy : labels.erase}
</button>
</div>
{confirming ? (
<div role="alertdialog" aria-labelledby={`${headingId}-confirm`} className="app-confirm">
<p id={`${headingId}-confirm`}>{labels.confirmErase}</p>
<button type="button" className="app-danger" onClick={() => start("erase")}>
{labels.confirm}
</button>
<button type="button" onClick={() => setConfirming(false)}>
{labels.cancel}
</button>
</div>
) : null}
{jobView(jobs.export)}
{jobView(jobs.erase)}
{failure === null ? null : (
<p role="alert" className="app-error">
{labels.failed.replace("{detail}", failure)}
</p>
)}
</section>
)
}
+142
View File
@@ -0,0 +1,142 @@
"use client"
// "Delete my account": the person types the account's e-mail address and confirms, from a session
// whose sign-in is recent (the server checks both; this form only says so first). The answer is the
// deletion's state: completed, or erasing while a module's service is unavailable -- in both cases the
// person is signed out everywhere, and the app's browser modules clear what they kept (`onDeleted`).
import { useTransport } from "@conusai-kit/ui"
import { type FormEvent, type ReactElement, useId, useState } from "react"
import type { DeletionView } from "../contract/procedures.js"
export interface AccountDeletionLabels {
readonly heading: string
readonly body: string
/** The services whose data is erased, already named; empty when there are none besides the app. */
readonly services: string
/** Set when the person is the last owner of an organization others belong to: the form is closed. */
readonly lastOwner?: string
/** Set when an organization would be left without members. */
readonly onlyMember?: string
readonly reauth: string
readonly reauthLink: string
readonly notInteractive: string
readonly confirmLabel: string
readonly confirmHint: string
readonly submit: string
readonly busy: string
readonly completed: string
/** With `{modules}` still in it. */
readonly erasing: string
readonly home: string
/** The app's `app.failed` message, with `{detail}` still in it. */
readonly failed: string
readonly sectionNames: Readonly<Record<string, string>>
}
export interface AccountDeletionProps {
readonly labels: AccountDeletionLabels
/** Whether the session may delete now: a person, signed in recently, owning no organization alone. */
readonly interactive: boolean
readonly recentSignIn: boolean
/** Where a person signs in again and comes back to this page. */
readonly reauthHref: string
/** A deletion already under way, when the page was rendered. */
readonly deletion: DeletionView | null
/** Runs once the deletion answered, before the result is shown: the app ends its browser modules' context. */
readonly onDeleted?: (deletion: DeletionView) => Promise<unknown> | unknown
}
type State =
| { readonly kind: "idle" | "busy" }
| { readonly kind: "deleted"; readonly deletion: DeletionView }
| { readonly kind: "failed"; readonly detail: string; readonly code: string }
const codeOf = (error: unknown): string => {
const problem = (error as { problem?: { code?: unknown } } | null)?.problem
return typeof problem?.code === "string" ? problem.code : "unknown"
}
export function AccountDeletionPanel(props: AccountDeletionProps): ReactElement {
const { labels } = props
const { call, describe } = useTransport()
const headingId = useId()
const inputId = useId()
const hintId = useId()
const [state, setState] = useState<State>(
props.deletion ? { kind: "deleted", deletion: props.deletion } : { kind: "idle" },
)
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault()
const confirm = String(new FormData(event.currentTarget).get("confirm") ?? "").trim()
if (confirm.length === 0) return
setState({ kind: "busy" })
try {
const deletion = (await call("account.deletion.request", { confirm })) as DeletionView
// The session is revoked now: what the browser modules kept for it goes too (contract §12).
await Promise.resolve(props.onDeleted?.(deletion)).catch(() => undefined)
setState({ kind: "deleted", deletion })
} catch (error) {
setState({ kind: "failed", detail: describe(error), code: codeOf(error) })
}
}
const reauth = (
<p role="status">
{labels.reauth} <a href={props.reauthHref}>{labels.reauthLink}</a>
</p>
)
if (state.kind === "deleted") {
const pending = state.deletion.pending.map((key) => labels.sectionNames[key] ?? key).join(", ")
return (
<section aria-labelledby={headingId}>
<h2 id={headingId}>{labels.heading}</h2>
<p role="status">
{state.deletion.state === "completed" ? labels.completed : labels.erasing.replace("{modules}", pending)}
</p>
<a href="/">{labels.home}</a>
</section>
)
}
const blocked = !props.interactive || labels.lastOwner !== undefined
return (
<section aria-labelledby={headingId}>
<h2 id={headingId}>{labels.heading}</h2>
<p>{labels.body}</p>
{labels.services ? <p>{labels.services}</p> : null}
{labels.onlyMember ? <p>{labels.onlyMember}</p> : null}
{labels.lastOwner ? <p role="status">{labels.lastOwner}</p> : null}
{!props.interactive ? <p role="status">{labels.notInteractive}</p> : null}
{props.interactive && !props.recentSignIn ? reauth : null}
{blocked ? null : (
<form onSubmit={submit} aria-busy={state.kind === "busy"}>
<div className="kit-field">
<label htmlFor={inputId}>{labels.confirmLabel}</label>
<input
id={inputId}
name="confirm"
type="email"
required
autoComplete="off"
spellCheck={false}
aria-describedby={hintId}
/>
<p id={hintId}>{labels.confirmHint}</p>
</div>
<button type="submit" disabled={state.kind === "busy"}>
{state.kind === "busy" ? labels.busy : labels.submit}
</button>
</form>
)}
{state.kind === "failed" && state.code === "reauthentication_required" ? reauth : null}
{state.kind === "failed" && state.code !== "reauthentication_required" ? (
<p role="alert" className="app-error">
{labels.failed.replace("{detail}", state.detail)}
</p>
) : null}
</section>
)
}
+81
View File
@@ -0,0 +1,81 @@
"use client"
// "Export my data": one call, and the answer saved as a JSON file in the browser. A section that
// could not be exported is named on the page as well as in the file, so a person never takes a
// partial export for a whole one.
import { useTransport } from "@conusai-kit/ui"
import { type ReactElement, useId, useState } from "react"
import type { AccountExport } from "../contract/procedures.js"
export interface AccountExportLabels {
readonly heading: string
readonly body: string
readonly submit: string
readonly busy: string
readonly done: string
/** With `{sections}` still in it. */
readonly partial: string
readonly filename: string
/** The app's `app.failed` message, with `{detail}` still in it. */
readonly failed: string
/** A readable name per section key; a key without one is shown as it is. */
readonly sectionNames: Readonly<Record<string, string>>
}
type State =
| { readonly kind: "idle" | "busy" | "done" }
| { readonly kind: "partial"; readonly sections: string }
| { readonly kind: "failed"; readonly detail: string }
function save(exported: AccountExport, filename: string): void {
const href = URL.createObjectURL(new Blob([JSON.stringify(exported, null, 2)], { type: "application/json" }))
const link = document.createElement("a")
link.href = href
link.download = `${filename}-${exported.exportedAt.slice(0, 10)}.json`
document.body.append(link)
link.click()
link.remove()
setTimeout(() => URL.revokeObjectURL(href), 0)
}
export function AccountExportPanel({ labels }: { labels: AccountExportLabels }): ReactElement {
const { call, describe } = useTransport()
const headingId = useId()
const [state, setState] = useState<State>({ kind: "idle" })
const run = async (): Promise<void> => {
setState({ kind: "busy" })
try {
const exported = (await call("account.data.export", {})) as AccountExport
save(exported, labels.filename)
const missing = Object.entries(exported.sections)
.filter(([, section]) => section.status !== "ok")
.map(([key]) => labels.sectionNames[key] ?? key)
setState(exported.complete ? { kind: "done" } : { kind: "partial", sections: missing.join(", ") })
} catch (error) {
setState({ kind: "failed", detail: describe(error) })
}
}
return (
<section aria-labelledby={headingId}>
<h2 id={headingId}>{labels.heading}</h2>
<p>{labels.body}</p>
<button type="button" onClick={run} disabled={state.kind === "busy"} aria-busy={state.kind === "busy"}>
{state.kind === "busy" ? labels.busy : labels.submit}
</button>
{state.kind === "done" ? <p role="status">{labels.done}</p> : null}
{state.kind === "partial" ? (
<p role="alert" className="app-error">
{labels.partial.replace("{sections}", state.sections)}
</p>
) : null}
{state.kind === "failed" ? (
<p role="alert" className="app-error">
{labels.failed.replace("{detail}", state.detail)}
</p>
) : null}
</section>
)
}
+11
View File
@@ -0,0 +1,11 @@
// The account module's components: the export and the deletion panels, and the label builders the
// app's account page (`apps/web/app/settings/account`) builds them with.
export {
type AccountAnalyticsLabels,
AccountAnalyticsPanel,
ANALYTICS_POLL_MS,
} from "./account-analytics.js"
export { type AccountDeletionLabels, AccountDeletionPanel, type AccountDeletionProps } from "./account-deletion.js"
export { type AccountExportLabels, AccountExportPanel } from "./account-export.js"
export { analyticsLabels, deletionLabels, exportLabels, pageLabels, sectionNames, type Translate } from "./labels.js"
+111
View File
@@ -0,0 +1,111 @@
// The labels each component takes, built from a message lookup. Every key the account page uses is
// named here and nowhere else, so `__tests__/i18n.test.ts` can run these builders against the
// catalogues and fail on a key either language lacks -- the page itself holds no string.
//
// `t` takes a key relative to the module (`export.heading`); the page binds it to
// `i18n.message("account." + key)`. The app's `app.failed` is passed in already looked up.
import { RECENT_SIGN_IN_SECONDS } from "../contract/constants.js"
import type { DeletionStatus } from "../contract/procedures.js"
import type { AccountAnalyticsLabels } from "./account-analytics.js"
import type { AccountDeletionLabels } from "./account-deletion.js"
import type { AccountExportLabels } from "./account-export.js"
export type Translate = (key: string, values?: Record<string, string | number>) => string
/** The sections this module has a name for; another module's key is shown as it is. */
const NAMED_SECTIONS = ["account", "guides", "support"] as const
export function sectionNames(t: Translate): Record<string, string> {
return Object.fromEntries(NAMED_SECTIONS.map((key) => [key, t(`sections.${key}`)]))
}
const named = (keys: readonly string[], names: Readonly<Record<string, string>>): string =>
keys.map((key) => names[key] ?? key).join(", ")
export function pageLabels(t: Translate) {
return {
title: t("title"),
intro: t("intro"),
link: t("link"),
profileHeading: t("profile.heading"),
name: t("profile.name"),
email: t("profile.email"),
}
}
export function exportLabels(t: Translate, failed: string): AccountExportLabels {
return {
heading: t("export.heading"),
body: t("export.body"),
submit: t("export.submit"),
busy: t("export.busy"),
done: t("export.done"),
partial: t("export.partial", { sections: "{sections}" }),
filename: t("export.filename"),
failed,
sectionNames: sectionNames(t),
}
}
export function deletionLabels(
t: Translate,
failed: string,
status: Pick<DeletionStatus, "modules" | "lastOwnerOf" | "onlyMemberOf">,
email: string,
): AccountDeletionLabels {
const names = sectionNames(t)
const organizations = (list: readonly { name: string }[]) => list.map((org) => org.name).join(", ")
return {
heading: t("deletion.heading"),
body: t("deletion.body"),
services: status.modules.length > 0 ? t("deletion.services", { modules: named(status.modules, names) }) : "",
...(status.lastOwnerOf.length > 0
? { lastOwner: t("deletion.lastOwner", { organizations: organizations(status.lastOwnerOf) }) }
: {}),
...(status.onlyMemberOf.length > 0
? { onlyMember: t("deletion.onlyMember", { organizations: organizations(status.onlyMemberOf) }) }
: {}),
reauth: t("deletion.reauth", { minutes: Math.round(RECENT_SIGN_IN_SECONDS / 60) }),
reauthLink: t("deletion.reauthLink"),
notInteractive: t("deletion.notInteractive"),
confirmLabel: t("deletion.confirmLabel"),
confirmHint: t("deletion.confirmHint", { email }),
submit: t("deletion.submit"),
busy: t("deletion.busy"),
completed: t("deletion.completed"),
erasing: t("deletion.erasing", { modules: "{modules}" }),
home: t("deletion.home"),
failed,
sectionNames: names,
}
}
/** The platform stores an analytics request reports on (ADR-049 §3). */
const ANALYTICS_STORES = ["session_replay", "errors", "proxy_logs", "traces", "logs", "analytics"] as const
const ANALYTICS_STATUSES = ["pending", "running", "completed", "failed", "expired"] as const
export function analyticsLabels(t: Translate, failed: string): AccountAnalyticsLabels {
return {
heading: t("analytics.heading"),
body: t("analytics.body"),
erase: t("analytics.erase"),
exportSubmit: t("analytics.exportSubmit"),
busy: t("analytics.busy"),
confirmErase: t("analytics.confirmErase"),
confirm: t("analytics.confirm"),
cancel: t("analytics.cancel"),
download: t("analytics.download"),
filename: t("analytics.filename"),
status: Object.fromEntries(
ANALYTICS_STATUSES.map((key) => [key, t(`analytics.status.${key}`)]),
) as AccountAnalyticsLabels["status"],
store: t("analytics.store"),
storeStatus: t("analytics.storeStatus"),
deleted: t("analytics.deleted"),
anonymized: t("analytics.anonymized"),
exported: t("analytics.exported"),
storeNames: Object.fromEntries(ANALYTICS_STORES.map((key) => [key, t(`analytics.stores.${key}`)])),
failed,
}
}