Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions REPAIRS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,43 @@ one commit.
the absence of a caller is a missing surface, not proof the concept is gone.
Whoever takes this should decide which it is rather than assuming.

### The retention doc-comment in `mutations.ts` sits above the wrong function

- **Found by:** C3, on `chunk-c3`, at base `95d77a9`.
- **Not claimed, and deliberately not fixed here.** Moving a comment is a
one-line change, and a one-line change in somebody else's function is still a
change C3 did not come to write. R3 keeps it out of the feature diff.
- **Symptom:** the comment reading "Withdrawing consent stops future requests;
evidence already stored is retained" sits immediately above a SECOND comment
and then `addAgentIssueTask`, which has nothing to do with consent.
`setExternalAgentAuditEnabled`, which the claim is about, is declared after
both and now carries a doc-comment of its own about the history it writes.
- **The behaviour claim is true** — withdrawal stops future requests and stored
evidence is retained, and C3 asserts both. Only its placement is wrong, so
this is a comment move and not a behaviour change.
- **Care needed:** the fix is to move the retention sentence onto
`setExternalAgentAuditEnabled` and merge it with the doc-comment C3 added
there, not to copy it — two statements of one retention rule is the drift that
put it in the wrong place to begin with.

### The Ora note runs two sentences together on screen

- **Found by:** C3, on `chunk-c3`, while merging `origin/main` (`07834fa`).
- **Not claimed, and deliberately not fixed here.** It is a one-character change
in a line C3 also edits, which is exactly the shape R3 warns about: a shared
defect buried in a feature diff cannot be reviewed or reverted on its own.
- **Symptom:** in `settings/page.tsx`, `{SETTINGS_SYSTEM_CONTRIBUTES.ora}` is
followed by ` Switching it on sends...` on the same line, and JSX drops that
leading space. The rendered note reads
"...you have to switch on.Switching it on sends...", with the DOM showing
`switch on.<!-- -->Switching`. Introduced by S9 (#93); no check reads rendered
copy, so CI is green on it.
- **Scope:** the only `{expr} Text` pair in that file, and the file uses `{" "}`
nowhere, so this is a one-off rather than a pattern.
- **Fix:** `{SETTINGS_SYSTEM_CONTRIBUTES.ora}{" "}` — or move the following word
onto its own line, which is what makes JSX keep the gap. Worth a look at S9's
other screens for the same pair before closing it.

## Landed

### Conflict markers committed into `DECISIONS.md`
Expand Down
9 changes: 8 additions & 1 deletion src/app/(app)/pages/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,14 @@ function ReadingsSection({
/>
}
>
<AgentAccessPanel access={access} caseHref={hrefForCase} />
<AgentAccessPanel
access={access}
caseHref={hrefForCase}
consent={{
on: store.externalAgentAuditEnabled === true,
history: store.externalAgentAuditConsentHistory ?? [],
}}
/>
<div
style={{
background: "var(--surface-card)",
Expand Down
117 changes: 100 additions & 17 deletions src/app/(app)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SegmentedControl } from "@/components/segmented-control";
import { useStore } from "@/components/store";
import { WebflowConnection } from "@/components/webflow-connection";
import { AGENT_CHECK_GROUPS, ALL_AGENT_CHECKS, agentCheckLabel, agentGroupLabel } from "@/lib/agentChecks";
import { consentCallerName, consentWasEverGranted } from "@/lib/agentConsent";
import { agentCheckKey, normalizeAgentIgnoreSettings } from "@/lib/agentScoring";
import { digestLimit } from "@/lib/digest-copy";
import { DIGEST_CADENCES, DIGEST_CADENCE_LABEL, normalizeDigestCadence } from "@/lib/digestCadence";
Expand Down Expand Up @@ -43,10 +44,18 @@ import {
SETTINGS_SENSITIVITY_LIMIT_LABEL,
SETTINGS_SYSTEMS_HELP,
SETTINGS_SYSTEMS_LABEL,
SETTINGS_CONSENT_HISTORY_LABEL,
SETTINGS_CONSENT_NEVER,
SETTINGS_CONSENT_RETENTION,
SETTINGS_CONSENT_UNRECORDED,
SETTINGS_SYSTEM_CONTRIBUTES,
settingsConsentGranted,
settingsConsentWithdrawn,
settingsSubtitle,
} from "@/lib/settings-copy";
import { excludedFromResults, type ExcludedRow } from "@/lib/settings-exclusions";
import type { ExternalAgentConsentEntry } from "@/lib/types";
import { formatDate } from "@/lib/watch-copy";
import { alertWebhookUrlIsValid } from "@/lib/webhook";
import {
DESTINATION_LABEL,
Expand Down Expand Up @@ -431,14 +440,67 @@ function ExcludedGroup({ disabled }: { disabled: boolean }) {
* control that ordered these would be a blend with a nicer name, and the group
* says so in its own help line.
*/
/**
* Every change to Ora consent, oldest first, and never anything else.
*
* The list IS the history rather than a summary derived from one (F5's rule),
* so nothing here folds, counts or collapses entries — a project that connected
* and disconnected four times has eight lines, because that is what happened.
*
* The three states are distinct on purpose, and none of them is a blank. Entries,
* so they render. No entries and never connected, which is a real answer and
* gets said in as many words. And no entries while connected — a project that
* turned Ora on before this record existed — which is not nothing to report:
* it is a grant with no date, and rule 18 says an absent measurement is not a
* small one. Two empty states, two different lines, because "never connected"
* would be a flat lie about a project that is connected right now.
*/
function ConsentHistory({
entries,
on,
}: {
entries: readonly ExternalAgentConsentEntry[];
on: boolean;
}) {
const everGranted = consentWasEverGranted(entries, on);
return (
<div className="settings-consent">
<h4 className="settings-consent__label">{SETTINGS_CONSENT_HISTORY_LABEL}</h4>
{entries.length === 0 ? (
<p className="settings-consent__none">
{everGranted ? SETTINGS_CONSENT_UNRECORDED : SETTINGS_CONSENT_NEVER}
</p>
) : (
<ul className="settings-consent__list">
{entries.map((entry, index) => (
<li className="settings-consent__entry" key={`${entry.at}:${index}`}>
<span>
{entry.enabled
? settingsConsentGranted(consentCallerName(entry.by))
: settingsConsentWithdrawn(consentCallerName(entry.by))}
</span>
{/* Absolute, never "3 days ago": a consent record is evidence
about a moment, and a relative date stops being true. */}
<span>{formatDate(entry.at)}</span>
</li>
))}
</ul>
)}
</div>
);
}

function ConnectedSystemsGroup({ disabled }: { disabled: boolean }) {
const {
pathFor,
alertWebhookUrl,
updateAlertWebhookUrl,
externalAgentAuditEnabled,
externalAgentAuditConsentHistory,
setExternalAgentAuditEnabled,
} = useStore();
const consentOn = externalAgentAuditEnabled === true;
const consentHistory = externalAgentAuditConsentHistory ?? [];
const stored = alertWebhookUrl ?? "";
const [webhookDraft, setWebhookDraft] = useState(stored);
const [syncedFrom, setSyncedFrom] = useState(stored);
Expand Down Expand Up @@ -492,24 +554,45 @@ function ConnectedSystemsGroup({ disabled }: { disabled: boolean }) {
</dl>
</div>

<div className="settings-system">
<div style={{ minWidth: 0 }}>
<h3 className="settings-system__name">{EVIDENCE_SOURCE_LABEL.ora}</h3>
<p className="settings-system__note">
{SETTINGS_SYSTEM_CONTRIBUTES.ora} Switching it on sends the live web address of each watched page to
Ora, whose scans are public: the result enters Ora&apos;s directory and anyone can read it. Webflow
staging addresses are never sent.
</p>
{/*
The Ora row IS the consent control, and it stays exactly where it is.
What it gains is the retention sentence — the half of the disclosure it
did not say — and the record of who changed it, beneath. The disclosure
reads ABOVE the control because it is what somebody needs before
deciding, not an explanation of what they just did.

Stacked, because the card now holds two things: the row, and the record
beneath it. Without this the card's own flex would lay the history out
BESIDE the control as a third column. `--stacked` is S8's existing
modifier and `.settings-consent__row` reproduces the original row inside
it, so the Ora row itself looks exactly as it did.
*/}
<div className="settings-system settings-system--stacked">
<div className="settings-consent__row">
<div style={{ minWidth: 0 }}>
<h3 className="settings-system__name">{EVIDENCE_SOURCE_LABEL.ora}</h3>
<p className="settings-system__note">
{SETTINGS_SYSTEM_CONTRIBUTES.ora} Switching it on sends the live web address of each watched page to
Ora, whose scans are public: the result enters Ora&apos;s directory and anyone can read it. Webflow
staging addresses are never sent.{" "}
{/* Draft, pending legal review. Rendered rather than withheld: a
reader deciding today needs it more than the review needs to
land first. Not reworded here — it states a consequence about
third-party publication. */}
{SETTINGS_CONSENT_RETENTION}
</p>
</div>
<SegmentedControl
ariaLabel="Ora"
value={externalAgentAuditEnabled ? "connected" : "off"}
options={[
{ value: "connected", label: "Connected", disabled },
{ value: "off", label: "Not connected", disabled },
]}
onChange={(next) => setExternalAgentAuditEnabled(next === "connected")}
/>
</div>
<SegmentedControl
ariaLabel="Ora"
value={externalAgentAuditEnabled ? "connected" : "off"}
options={[
{ value: "connected", label: "Connected", disabled },
{ value: "off", label: "Not connected", disabled },
]}
onChange={(next) => setExternalAgentAuditEnabled(next === "connected")}
/>
<ConsentHistory entries={consentHistory} on={consentOn} />
</div>

<div className="settings-system settings-system--stacked">
Expand Down
10 changes: 9 additions & 1 deletion src/app/api/settings/agent-audits/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { setExternalAgentAuditEnabled } from "@/lib/mutations";
import { projectStore } from "@/lib/projects";
import { identityFromRequest } from "@/lib/identity";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -12,8 +13,15 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "enabled must be a boolean" }, { status: 400 });
}
try {
// The verified account, not anything the body claims: the history records
// WHO consented, which is the only thing that makes it a consent record.
const identity = await identityFromRequest(req);
return NextResponse.json({
state: await setExternalAgentAuditEnabled(body.enabled, await projectStore(req)),
state: await setExternalAgentAuditEnabled(
body.enabled,
{ kind: "person", userId: identity.email },
await projectStore(req),
),
});
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
Expand Down
55 changes: 55 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,56 @@ button:disabled {
}
}

/* ── Consent, on the Ora row ───────────────────────────────────────────── */

/*
The Ora row's original layout, unchanged, now that the card also holds the
history beneath it. The row keeps its shape; only the card grew.
*/
.settings-consent__row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
}

.settings-consent {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--border-hairline);
}

.settings-consent__label {
margin: 0;
color: var(--text-body);
font-size: 12px;
font-weight: 600;
}

.settings-consent__list {
margin: 6px 0 0;
padding: 0;
list-style: none;
}

.settings-consent__entry {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 10px;
padding: 3px 0;
color: var(--text-muted);
font-size: 12px;
line-height: 1.5;
}

.settings-consent__none {
margin: 6px 0 0;
color: var(--text-muted);
font-size: 12px;
line-height: 1.5;
}

/* ── Excluded from results ─────────────────────────────────────────────── */

.excluded-list {
Expand Down Expand Up @@ -2602,6 +2652,11 @@ textarea:focus-visible,
align-items: stretch;
}

.settings-consent__row {
flex-direction: column;
align-items: stretch;
}

.guide-toolbar {
position: static;
}
Expand Down
Loading
Loading