mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-08 00:02:09 +00:00
* feat: index advisor findings by subject and parent entity so a list page fetches its whole surface once and every row resolves its own advice from the shared cache instead of firing a request per row * feat: rebuild the advisor fix drawer as a three-screen resolution flow (why it fired with the measured evidence, the exact before and after, then an animated outcome with undo) with a progress rail and direction-aware transitions, and deep-link manual fixes to the screen where they are made * feat: add AdvisorRowFlag, the inline per-row advisor indicator that renders on the mailbox or campaign the problem is about and opens that row's findings in an anchored panel instead of making the reader join a card list against a table * feat: add AdvisorSummaryBar, a one-line collapsible page summary that replaces the stack of advisor cards above a list, counts the distinct rows implicated rather than the findings, and forces itself open only for critical or workspace-level advice no row flag can carry * feat: put advisor advice on the mailbox row it is about in the accounts list, replace the card stack above the table with the collapsible summary bar, and support ?mailbox=<id> so a finding can deep-link straight to the mailbox detail instead of the top of the list * feat: flag advisor findings on the campaign row in the campaigns list, including step-level copy problems which index onto their parent campaign since a step has no row of its own, and add the collapsible summary bar above the list * feat: move the deliverability and contacts pages onto the collapsible advisor summary bar so their findings stop pushing the numbers they describe below the fold * feat: add an ordered Steps field to advisor findings, persisted as text[] and always refreshed from the current build, and write real how-to steps for the deliverability checks that have no one-click fix (bounce rate, spam placement, tracking domain, and per-record SPF/DKIM/DMARC instructions) * feat: write ordered how-to steps for the manual advisor findings where the remedy alone leaves someone stuck (broken template syntax, missing first-name fallback, unsubscribed contacts still enrolled, a campaign with no resolvable sender, and a mailbox that lost warmup pool standing) and correct the personalization detail that named a merge syntax this product does not use * feat: show a mailbox's advisor findings at the top of its detail drawer, which is where both the row flag and the ?mailbox deep link now land * feat: open the resolution flow from findings that have no one-click fix too, since the ordered how-to lives there and a card with no Fix button previously left the steps unreachable * docs: document the per-row advisor flags, the collapsible page summary, the three-screen resolution flow, and the ordered manual steps for findings with no one-click fix * feat: align the advisor summary bar to the px-5 page gutter used by SectionBar and the list rows on all four surfaces, instead of sitting flush against the edge while the table it describes is indented * fix: stop the resolution drawer collapsing to zero height between screens by switching the step transition to popLayout with a layout-animated container, so the dialog resizes into the next screen instead of snapping shut and reopening * feat: wire the advisor repository, narrator, service, tool registration, and background runner into the backend boot path so findings evaluate on a schedule and the assistant can read them * docs: register the advisor guide in the sidebar, add its endpoint scope table to the API reference, and document the sandbox advisor showcase * fix: darken the advisor nav badge to solid orange-600 on white instead of a pale amber-100 chip that read as a disabled control beside the sidebar's saturated indicators, and drop the critical badge to rose-600 so the two stay in the same weight class * fix: use orange-500 for the advisor nav badge, matching the high-severity dot on the row it points at, rather than the darker orange-600 * feat: add an Auto safety class to advisor actions and mark the seven fixes autopilot may apply unattended (the cap cuts, the send-gap widen, the campaign limit matches, and the unsubscribe header), with a test pinning the boundary so nothing that halts sending or generates new outbound mail can drift into it * feat: add advisor autopilot, which applies the auto-safe fixes unattended as the member who switched it on, resolving their live permissions each run so it fails closed when they leave the org, bounded to 10 changes per evaluation and audited per fix like any hand-made change * feat: add the advisor agent fix, a bounded per-finding agent run that resolves the problems a settings change cannot (broken template syntax, bulk-reading copy, shared-inbox lists) as the calling member inside a tool allowlist scoped to the finding's category, metered per iteration and marked applied only when it actually called a write tool * feat: surface autopilot and the agent fix in the dashboard, adding the workspace toggle that names exactly which changes it may make, an Auto chip on the findings it is allowed to take, and an agent-fix path in the resolution drawer that reports the tools it actually called rather than only its own account of them * docs: document the agent fix and autopilot, naming the exact set of changes autopilot may make, that it acts as the member who enabled it and stops when they leave, and why the agent-fix endpoint is JWT only * fix: gate the agent fix per detector instead of per category, so a missing DMARC record no longer offers a Fix-with-agent button it can never satisfy and then reports failure; findings whose fix lives in DNS or a provider console now show their manual steps, and the client is told which is which via agent_fixable * feat: soften the advisor surfaces to translucent washes, replacing the filled nav badge with a tinted pill that carries its colour in the text, frosting the row panel and the resolution drawer, and turning the severity chips and cards into layers the page shows through * docs: correct the agent-fix scope to name the findings it cannot resolve, and why a DNS record shows steps instead of a button * feat: ship the actual DNS records for the findings that live outside the platform, with the provider's SPF include resolved, the DMARC record scoped to the sending domain and starting at p=none, the DKIM host plus the console that generates its value, and a tracking CNAME pointing at this install's own tracking host * feat: render advisor snippets as labelled copy-button rows so a DNS record is one click per field rather than a text-selection exercise, with no copy affordance on a value the server could not supply * docs: document the pasteable DNS records and the guarantee that every check offers a fix, an agent, or ordered steps * fix: bump golang.org/x/text to 0.39.0 to clear CVE-2026-56852, a HIGH-severity infinite loop in norm.Iter that Trivy started failing the security scan on
169 lines
8.2 KiB
SQL
169 lines
8.2 KiB
SQL
-- Advisor: continuously-evaluated, org-scoped findings about deliverability,
|
|
-- mailbox config, warmup, campaign performance, copy, and list hygiene.
|
|
--
|
|
-- Detection is deterministic Go (see internal/app/advisor); the LLM only
|
|
-- narrates a finding's title/detail/remedy from its evidence, and that
|
|
-- narration is cached per (detector, evidence shape) so a whole org costs a
|
|
-- handful of completions a day. A finding is identified by its fingerprint
|
|
-- (detector key + subject entity), so re-running the engine updates the same
|
|
-- row instead of duplicating advice.
|
|
|
|
CREATE TABLE IF NOT EXISTS advisor_findings (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
|
|
-- Stable identity across runs: <detector_key>:<entity_type>:<entity_id>.
|
|
fingerprint text NOT NULL,
|
|
detector_key text NOT NULL,
|
|
category text NOT NULL,
|
|
severity text NOT NULL,
|
|
-- Dashboard nav tab the fix lives on, so the UI can badge the right tab
|
|
-- and render the finding on the page where the problem actually is.
|
|
surface text NOT NULL DEFAULT 'deliverability',
|
|
|
|
entity_type text NOT NULL DEFAULT '',
|
|
entity_id uuid,
|
|
entity_label text NOT NULL DEFAULT '',
|
|
-- The entity this one belongs to, when they differ: a copy problem lives on
|
|
-- a sequence step, but the person looking for it is on the campaign page.
|
|
-- Entity-scoped reads match either column, so a finding surfaces on the
|
|
-- page where someone would go looking for it.
|
|
parent_type text NOT NULL DEFAULT '',
|
|
parent_id uuid,
|
|
|
|
status text NOT NULL DEFAULT 'open',
|
|
-- 0-100 ranking weight; severity breaks ties on equal impact.
|
|
impact smallint NOT NULL DEFAULT 0,
|
|
|
|
title text NOT NULL,
|
|
-- How this finding names itself when several of its kind are shown
|
|
-- together ("{count} mailboxes are capped above the safe band"). A
|
|
-- workspace that misconfigured twenty mailboxes the same way should get
|
|
-- one card, not twenty; empty means this finding never collapses.
|
|
group_title text NOT NULL DEFAULT '',
|
|
detail text NOT NULL DEFAULT '',
|
|
remedy text NOT NULL DEFAULT '',
|
|
-- The ordered manual how-to, set only by checks with no one-click fix.
|
|
-- Empty means the remedy prose is the whole answer.
|
|
steps text[] NOT NULL DEFAULT '{}',
|
|
-- Copy-pasteable values for a fix that lives outside the platform, such as
|
|
-- the DNS records for SPF, DKIM and DMARC.
|
|
snippets jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
-- False while the row still carries the deterministic fallback copy, so a
|
|
-- later run can upgrade it once AI is configured / credits exist.
|
|
narrated boolean NOT NULL DEFAULT false,
|
|
|
|
evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
-- Bucketed hash of the evidence: narration is reused while this is stable,
|
|
-- and re-generated when the numbers move enough to change the advice.
|
|
evidence_hash text NOT NULL DEFAULT '',
|
|
-- One-click fix descriptor: {tool, args, label, preview:[{field,from,to}]}.
|
|
-- NULL means the finding is informational and has no automated remedy.
|
|
action jsonb,
|
|
|
|
first_seen_at timestamptz NOT NULL DEFAULT now(),
|
|
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
|
resolved_at timestamptz,
|
|
|
|
snoozed_until timestamptz,
|
|
dismissed_at timestamptz,
|
|
dismissed_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
dismiss_reason text NOT NULL DEFAULT '',
|
|
|
|
applied_at timestamptz,
|
|
applied_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
applied_result text NOT NULL DEFAULT '',
|
|
|
|
CONSTRAINT advisor_findings_status_check
|
|
CHECK (status IN ('open', 'snoozed', 'dismissed', 'applied', 'resolved')),
|
|
CONSTRAINT advisor_findings_severity_check
|
|
CHECK (severity IN ('critical', 'high', 'medium', 'low')),
|
|
CONSTRAINT advisor_findings_org_fingerprint_key
|
|
UNIQUE (organization_id, fingerprint)
|
|
);
|
|
|
|
-- The two hot reads: the badge counts per surface, and the strip on an entity.
|
|
CREATE INDEX IF NOT EXISTS idx_advisor_findings_open
|
|
ON advisor_findings (organization_id, surface, severity)
|
|
WHERE status IN ('open', 'snoozed');
|
|
CREATE INDEX IF NOT EXISTS idx_advisor_findings_entity
|
|
ON advisor_findings (organization_id, entity_type, entity_id)
|
|
WHERE status IN ('open', 'snoozed');
|
|
CREATE INDEX IF NOT EXISTS idx_advisor_findings_parent
|
|
ON advisor_findings (organization_id, parent_type, parent_id)
|
|
WHERE status IN ('open', 'snoozed') AND parent_id IS NOT NULL;
|
|
-- Snooze expiry sweep.
|
|
CREATE INDEX IF NOT EXISTS idx_advisor_findings_snoozed
|
|
ON advisor_findings (snoozed_until)
|
|
WHERE status = 'snoozed';
|
|
|
|
-- Narration cache. Keyed per org because the copy names the org's own
|
|
-- campaigns/mailboxes and follows its voice grounding.
|
|
CREATE TABLE IF NOT EXISTS advisor_narrations (
|
|
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
cache_key text NOT NULL,
|
|
title text NOT NULL,
|
|
detail text NOT NULL,
|
|
remedy text NOT NULL,
|
|
model text NOT NULL DEFAULT '',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (organization_id, cache_key)
|
|
);
|
|
|
|
-- Was this advice useful? Feeds detector tuning and lets a member's "not
|
|
-- useful" mute a detector for the org without hiding it from admins.
|
|
CREATE TABLE IF NOT EXISTS advisor_feedback (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
finding_id uuid NOT NULL REFERENCES advisor_findings(id) ON DELETE CASCADE,
|
|
detector_key text NOT NULL,
|
|
user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
helpful boolean NOT NULL,
|
|
reason text NOT NULL DEFAULT '',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (finding_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_advisor_feedback_detector
|
|
ON advisor_feedback (organization_id, detector_key, helpful);
|
|
|
|
-- Per-org advisor controls. A row is created lazily on first write; absence
|
|
-- means "all defaults" (enabled, nothing muted).
|
|
CREATE TABLE IF NOT EXISTS advisor_settings (
|
|
organization_id uuid PRIMARY KEY REFERENCES organizations(id) ON DELETE CASCADE,
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
-- Categories the org has switched off entirely.
|
|
muted_categories text[] NOT NULL DEFAULT '{}',
|
|
-- Individual detector keys the org has switched off.
|
|
muted_detectors text[] NOT NULL DEFAULT '{}',
|
|
-- Lowest severity the org wants surfaced.
|
|
min_severity text NOT NULL DEFAULT 'low',
|
|
-- Autopilot applies the fixes marked auto-safe without asking. Off by
|
|
-- default: acting on someone's sending configuration unattended is opt-in.
|
|
autopilot boolean NOT NULL DEFAULT false,
|
|
-- The member autopilot acts as. It runs with their permissions and is
|
|
-- audited as them, so removing them from the org stops it rather than
|
|
-- leaving an unattributable actor with standing write access.
|
|
autopilot_actor_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT advisor_settings_min_severity_check
|
|
CHECK (min_severity IN ('critical', 'high', 'medium', 'low'))
|
|
);
|
|
|
|
-- One evaluation pass, for observability and to rate-limit manual refreshes.
|
|
CREATE TABLE IF NOT EXISTS advisor_runs (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
trigger text NOT NULL DEFAULT 'schedule',
|
|
started_at timestamptz NOT NULL DEFAULT now(),
|
|
finished_at timestamptz,
|
|
findings_new integer NOT NULL DEFAULT 0,
|
|
findings_open integer NOT NULL DEFAULT 0,
|
|
findings_closed integer NOT NULL DEFAULT 0,
|
|
narrated integer NOT NULL DEFAULT 0,
|
|
error text NOT NULL DEFAULT ''
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_advisor_runs_org
|
|
ON advisor_runs (organization_id, started_at DESC);
|