Pattern merges and adoptions that stay entirely within the
uncategorised category never change the visitor-facing snapshot.
Only trigger ensureDraftVersionForBanner when a merge group
belongs to a consent category.
Signed-off-by: Émile Ré <emile@getprobo.com>
Translations are no longer part of the version snapshot, so the
banner_description text must be validated at write time to ensure
the {{cookie_policy_link}} placeholder is present. Without it
the cookie policy URL silently disappears from the rendered banner.
Signed-off-by: Émile Ré <emile@getprobo.com>
Decouple detection from the banner's active state so admins
can discover cookies before making the banner visible. The
client now starts the detector even when the config endpoint
returns 404, and the detector stops itself if the report
endpoint returns 404 (wrong or deleted banner ID).
Signed-off-by: Émile Ré <emile@getprobo.com>
The uncategorised category is an admin-side inbox for detected
cookies and should not be part of the visitor-facing consent
contract. Filter it out of snapshots so changes to uncategorised
patterns no longer trigger version bumps.
Signed-off-by: Émile Ré <emile@getprobo.com>
Translation changes are cosmetic, not consent-contract changes,
so they should not trigger a version bump. Translations are now
loaded live from the database at serve time instead of being
frozen in the snapshot.
Signed-off-by: Émile Ré <emile@getprobo.com>
A central snapshot-equality guard in ensureDraftVersion now returns
the latest version unchanged when the candidate snapshot matches it,
so no-op admin saves no longer force visitors to re-consent. Per-
operation short-circuits in UpdateCookieBanner, UpdateCookieCategory,
UpdateCookiePattern, DeleteCookiePattern, MoveCookiePatternToCategory,
ReorderCookieCategory, and UpsertCookieBannerTranslation skip the row
update and version bump when nothing visitor-facing changes (excluded
patterns, identical values, identical translation JSON).
Rank is now treated as admin-only metadata: buildSnapshot sorts
categories by (Kind weight, ID byte order) instead of relying on the
implicit rank-driven slice order, and ReorderCookieCategory no longer
calls ensureDraftVersionForBanner. Default banners keep their
visitor-facing order (insertion order matches Kind+ID); banners with
admin-customised ranks see a one-time order shift to insertion order
on the next snapshot rebuild.
Reusable equality helpers (Ptr generic + JSON canonicalisation) move
to a new pkg/equal package; snapshotsEqual stays in service.go as the
documented chokepoint for visitor-identical snapshot comparison.
Signed-off-by: Émile Ré <emile@getprobo.com>
Filter excluded and non-exact patterns in SQL when loading
patterns for the pattern analysis worker. Both merge group
building and uncategorised adoption only see non-excluded
exact patterns, so excluded patterns are preserved as
punch-out overrides.
Signed-off-by: Émile Ré <emile@getprobo.com>
In ReportDetectedCookies, silently skip cookies that match an
excluded pattern instead of recording them. Filter excluded
patterns in SQL when building version snapshots so they never
appear in the published banner config. Add Excluded field to
UpdateCookiePatternRequest.
Signed-off-by: Émile Ré <emile@getprobo.com>
Adds an `excluded` boolean column to the cookie_patterns table
so operators can mark patterns to be omitted from the consent
banner without deleting them. Includes the migration, struct
field, updated SQL queries, and filter support.
Signed-off-by: Émile Ré <emile@getprobo.com>
The adoptUncategorisedPatterns method loaded all patterns for a
banner then filtered in Go. This adds a CookiePatternFilter
(match_type + cookie_category_id) and wires it into
LoadAllByCookieBannerID so the two targeted loads only fetch
the rows they need.
Signed-off-by: Émile Ré <emile@getprobo.com>
When a cookie pattern already exists for a banner, the insert
conflict caused a continue that silently dropped the detected
cookie instead of linking it to the existing pattern. Load the
existing pattern to obtain its ID and proceed with cookie
insertion.
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the ad-hoc patternAnalysisTask struct with coredata.CookieBanner
as the worker type parameter, matching the pattern used by other workers
(esign, accessreview). SQL methods move back to coredata on *CookieBanner.
Signed-off-by: Émile Ré <emile@getprobo.com>
Use InsertIfNotExists instead of Insert with error check, since
a unique-violation aborts the PostgreSQL transaction even when caught.
Signed-off-by: Émile Ré <emile@getprobo.com>
Move the DurationInput component from the console app into @probo/ui
for reuse, add duration formatting helpers to @probo/helpers, and
update pattern merge to group by both category ID and prefix.
Signed-off-by: Émile Ré <emile@getprobo.com>
- Fix DurationInput fallback unit from "minutes" to "seconds" and add
seconds as a selectable unit to prevent silent duration inflation
- Use parseFloat instead of parseInt for duration input to preserve
fractional values
- Scope prefix merge groups by category ID to prevent cross-category
merging
- Relink cookies and delete exact patterns even when prefix pattern
already exists
- Prefer exact matches and longest prefix in pattern selection query
- Fix wrong error type in GetCookiePattern (ErrCookiePatternNotFound)
- Handle singular/plural in humanizeSeconds fallback branch
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the free-form duration TEXT column with a nullable
max_age_seconds INTEGER on both cookies and cookie_patterns
tables. The SDK detector now sends raw seconds instead of
humanized strings, eliminating locale-dependent comparisons
in the pattern merge worker. Humanization happens at display
time in the widget and console UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
The previous algorithm split on the first separator only, producing
overly broad prefixes (e.g. ph_ for ph_phc_abc123). Replace with a
greedy longest-shared-prefix approach that finds the longest
separator-boundary prefix shared by 3+ exact patterns, avoiding
false merges across unrelated cookie families.
Signed-off-by: Émile Ré <emile@getprobo.com>
Background worker polls cookie_banners with pattern_analysis_requested_at
set, groups EXACT patterns sharing a common prefix, and merges groups of
3+ into a PREFIX pattern. Detection sets the flag when new EXACT patterns
are created. The worker relinks cookies, removes orphaned patterns, and
updates the draft version via ensureDraftVersionForBanner.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add CookiePattern type, connection, and mutations to the GraphQL schema
with full resolver implementations. Add service methods for pattern
CRUD, category movement, listing, and counting. This enables the console
to manage cookie patterns instead of individual cookies.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a cookie_patterns table that groups cookies sharing a common
prefix (e.g. phc_*) into a single manageable row. Every cookie now
belongs to a pattern (EXACT or PREFIX match type). Category, description,
and display metadata move from cookies to patterns, making patterns the
unit of management and display in the console and published snapshots.
Signed-off-by: Émile Ré <emile@getprobo.com>
Cookies set by browser extensions are not the website operator's
compliance responsibility. This adds stack-trace inspection to
filter out extension-originated document.cookie writes, and
annotates pre-existing cookies with a source field so operators
can triage them separately.
Introduces a CookieSource enum (SCRIPT / PRE_EXISTING) across
the full stack: PostgreSQL, coredata, service, HTTP handler, and
GraphQL schema. On conflict, source is upgraded from PRE_EXISTING
to SCRIPT when a page script is later observed setting the cookie.
Signed-off-by: Émile Ré <emile@getprobo.com>
Batch-load CookieBanner and CookieCategory entities via
dataloadgen instead of making individual service calls in
GraphQL resolvers, matching the existing dataloader pattern
used for organizations, frameworks, etc.
Signed-off-by: Émile Ré <emile@getprobo.com>
Display record attributes and parsed consent data with
per-category consent state and cookies from the banner
version snapshot. The page lives outside the config layout
with its own breadcrumb navigation.
Signed-off-by: Émile Ré <emile@getprobo.com>
The consent cookie is automatically created as a database record in
the necessary category when a new cookie banner is set up, so it
appears alongside other necessary cookies in the banner UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include a `bid` field in the consent cookie so it explicitly
identifies which cookie banner it belongs to, making validation
direct instead of relying on the visitor ID as an implicit
discriminator. Existing cookies without `bid` self-heal on the
next load by falling through to the API fetch.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a required cookie_policy_url alongside the existing
privacy_policy_url (now optional) so banners can link directly to a
dedicated cookie policy — a compliance best practice recommended by
CNIL, ICO, and the EDPB. Existing rows are seeded from their current
privacy_policy_url value.
Both {{cookie_policy_link}} and {{privacy_policy_link}} placeholders
are supported independently in banner description translations.
Signed-off-by: Émile Ré <emile@getprobo.com>
Origin is a fundamental identity property of a banner tied to consent
records for a specific site. Changing it would break the audit trail
and violate GDPR consent specificity requirements.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add a partial unique index ensuring only one category per banner can
have posthog_consent enabled. Default it to the analytics category on
banner creation, clear the previous mapping before setting a new one,
and restrict the toggle to NORMAL categories in both the service layer
and the console UI.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add PostHog opt-in/opt-out consent support mirroring the existing Google
Consent Mode integration: database column, GraphQL field, console UI
toggle, and client-side posthog-js calls.
Extract both GCM and PostHog logic from CookieBannerClient into a
ConsentIntegration plugin interface so future integrations can be added
without modifying the client core.
Signed-off-by: Émile Ré <emile@getprobo.com>
Include gcm_consent_types in the published banner config
so the cookie banner SDK can read category-to-GCM mappings
from the config endpoint.
Signed-off-by: Émile Ré <emile@getprobo.com>
Address locale normalization for region-tagged values, guard
language detection for non-DOM runtimes, validate DefaultLanguage
on update, pass translated texts through the deactivation flow,
handle slug collisions in migration, add organizations FK, fix
consent migration from name-keyed to slug-keyed data, render all
template placeholders in previews, and wrap helper text for i18n.
Signed-off-by: Émile Ré <emile@getprobo.com>
label_description and label_duration are not user-customizable, so they
don't belong in the backend translation defaults. Hardcode them in the
cookie-banner SDK's i18n module instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
The mutation was wired up in the GraphQL schema and resolver
but never called from the console frontend. This removes the
service method, request type, resolver, schema entries, and
two unused error sentinels (ErrTranslationNotFound,
ErrTranslationAlreadyExists).
Signed-off-by: Émile Ré <emile@getprobo.com>
Validate translation string values server-side with NoHTML() and
MaxLen(2000) to reject HTML in the translations JSON blob. On the
client side, escape user-provided template text before innerHTML
injection in banner_description and placeholder_text paths.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce category-level name/description translations in the cookie
banner i18n flow. Seed default translations for fr/de/es on banner
creation, parse them from the stored JSON, and manage them via
react-hook-form Controllers instead of a manual ref/callback pattern.
Enhance the panel preview with category descriptions and all three
action buttons.
Signed-off-by: Émile Ré <emile@getprobo.com>
The category slug provides a stable, URL-safe key used as the
data-cookie-consent attribute value and consent data key, replacing
the fragile category name. This prevents breakage when categories
are renamed.
- Add slug column with unique-per-banner constraint and backfill migration
- Add Slug validator (lowercase alphanumeric + hyphens)
- Propagate slug through GraphQL schema, service layer, and snapshot
- Update console UI with slug field in create/edit forms
- Switch cookie-banner widget to use slug as consent data keys
Signed-off-by: Émile Ré <emile@getprobo.com>
Duration strings are static and not customizable through the console,
so they belong in the frontend cookie-banner package instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
The field was populated by the backend and declared in the TS interface
but never consumed by any client-side code. The banner resolves its
language via detectLanguage() and passes it as a query param.
Signed-off-by: Émile Ré <emile@getprobo.com>
New banners now get French, German, and Spanish UI string
translations seeded alongside English during creation.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add CookieBannerTranslation type, defaultLanguage field on
CookieBanner, and upsert/delete mutations for managing
per-language translations from the console.
Signed-off-by: Émile Ré <emile@getprobo.com>
Seed default English UI strings on banner creation, include
translations in version snapshots, and resolve language at
config-serving time. Add translation CRUD methods and the
language-aware buildBannerConfig helper. Extract default
categories and UI strings into defaults.go.
Signed-off-by: Émile Ré <emile@getprobo.com>
The JS SDK will override document.cookie to detect unknown cookies set
by the website and report them to a new POST /{bannerID}/detected-cookies
endpoint. Reported cookies are inserted into the "Uncategorised" category
and a draft version is created so the admin can review them.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduces a BRANDING boolean config (default true) propagated through
the standard config pipeline. Cookie banners now initialize their
show_branding column from this config instead of hardcoding true.
Signed-off-by: Émile Ré <emile@getprobo.com>
- Add scope parameter to UpdateShowBranding to prevent cross-tenant updates
- Use cmd.Context() instead of context.Background() in proboctl CLI
- Drop SQL column default after backfill in migration
- Add bounds check for int-to-int32 conversion in PG_POOL_SIZE
- Update branding link to getprobo.com homepage
Signed-off-by: Émile Ré <emile@getprobo.com>