feat(users/forum-pref): persisted Fristenrechner inbox-channel column
Adds paliad.users.forum_pref so /tools/fristenrechner can pre-narrow the proceeding picker to the user's typical inbox channel without re-asking on every visit. The new column threads through the User model, the userColumns SELECT, and UpdateProfileInput so the existing PATCH /api/me handler accepts it without a new endpoint. Allowed values mirror the channel chips m named in t-paliad-157: - cms → UPC - bea → national-DE - posteingang → national-DE (slower channel, same forums) NULL means "no preference, picker shows everything"; URL ?inbox= overrides per-visit (frontend lands in the next commit). The CHECK constraint enforces the 3-value enum at the DB layer; isValidForumPref mirrors it in the service so callers see a typed error instead of a raw pq violation. Empty string in the PATCH body clears the preference, consistent with the EscalationContactID convention. Migration 064 applied to the live Supabase pool; tracker bumped to v64 so the boot-time runner skips re-applying. Refs m/paliad#15.
This commit is contained in:
5
internal/db/migrations/064_users_forum_pref.down.sql
Normal file
5
internal/db/migrations/064_users_forum_pref.down.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Reverse t-paliad-157 / m/paliad#15: drops the persisted
|
||||
-- Fristenrechner inbox-channel preference column.
|
||||
|
||||
ALTER TABLE paliad.users DROP CONSTRAINT IF EXISTS users_forum_pref_check;
|
||||
ALTER TABLE paliad.users DROP COLUMN IF EXISTS forum_pref;
|
||||
32
internal/db/migrations/064_users_forum_pref.up.sql
Normal file
32
internal/db/migrations/064_users_forum_pref.up.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- t-paliad-157 / m/paliad#15: persisted Fristenrechner inbox-channel
|
||||
-- preference.
|
||||
--
|
||||
-- Stores the user's typical inbox channel (cms = UPC, bea = national-DE,
|
||||
-- posteingang = national-DE — slower channel, same set of forums) so
|
||||
-- /tools/fristenrechner can pre-narrow the proceeding picker without
|
||||
-- re-asking on every visit. The chip on the page persists changes here
|
||||
-- via the existing PATCH /api/me endpoint. URL ?inbox= overrides for
|
||||
-- the current visit so a colleague can share a CMS-narrowed link
|
||||
-- without flipping anyone's saved preference.
|
||||
--
|
||||
-- The 3-value CHECK keeps the schema honest while leaving room to add
|
||||
-- epa / dpma channels later (the Fristenrechner already supports those
|
||||
-- forums via the fine-grained B2 chips; the inbox-channel chip starts
|
||||
-- with the channels m named explicitly in t-paliad-157).
|
||||
--
|
||||
-- NULL = no preference, picker shows everything. Default for existing
|
||||
-- rows.
|
||||
|
||||
ALTER TABLE paliad.users
|
||||
ADD COLUMN forum_pref text;
|
||||
|
||||
ALTER TABLE paliad.users
|
||||
ADD CONSTRAINT users_forum_pref_check
|
||||
CHECK (forum_pref IS NULL OR forum_pref IN ('cms', 'bea', 'posteingang'));
|
||||
|
||||
COMMENT ON COLUMN paliad.users.forum_pref IS
|
||||
'Persisted Fristenrechner inbox-channel preference (#15). '
|
||||
'cms = UPC; bea = national-DE; posteingang = national-DE (slower '
|
||||
'channel, same forums). NULL = no preference (picker shows '
|
||||
'everything). URL ?inbox= overrides for the current visit. Set via '
|
||||
'PATCH /api/me.';
|
||||
@@ -56,8 +56,13 @@ type User struct {
|
||||
// for overdue / DRINGEND mail. NULL means "fall back to global_admins".
|
||||
// Settings UI dropdown shipped 2026-04-29 (t-paliad-066).
|
||||
EscalationContactID *uuid.UUID `db:"escalation_contact_id" json:"escalation_contact_id,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
// ForumPref is the user's persisted Fristenrechner inbox-channel
|
||||
// preference (#15): "cms" → UPC; "bea" → national-DE;
|
||||
// "posteingang" → national-DE (slower channel, same forums). NULL =
|
||||
// no preference. URL ?inbox= overrides per-visit.
|
||||
ForumPref *string `db:"forum_pref" json:"forum_pref,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// Project is one node in the paliad.projects tree. Visibility is team-based
|
||||
|
||||
@@ -73,6 +73,7 @@ const userColumns = `id, email, display_name, office, additional_offices, practi
|
||||
reminder_timezone,
|
||||
reminder_warning_offset_days,
|
||||
escalation_contact_id,
|
||||
forum_pref,
|
||||
created_at, updated_at`
|
||||
|
||||
// GetByID returns the user row, or (nil, nil) if the user hasn't completed
|
||||
@@ -283,6 +284,10 @@ type UpdateProfileInput struct {
|
||||
// encode the clear signal as "" rather than juggling JSON null / missing
|
||||
// semantics).
|
||||
EscalationContactID *string `json:"escalation_contact_id,omitempty"`
|
||||
// ForumPref is the persisted Fristenrechner inbox-channel preference
|
||||
// (#15). Allowed values: "cms" | "bea" | "posteingang". Empty string
|
||||
// clears the preference (NULL in the DB). nil = don't touch.
|
||||
ForumPref *string `json:"forum_pref,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateProfile mutates the paliad.users row for the authenticated user.
|
||||
@@ -409,6 +414,21 @@ func (s *UserService) UpdateProfile(ctx context.Context, id uuid.UUID, input Upd
|
||||
args = append(args, val)
|
||||
i++
|
||||
}
|
||||
if input.ForumPref != nil {
|
||||
raw := strings.TrimSpace(*input.ForumPref)
|
||||
var val any
|
||||
if raw == "" {
|
||||
val = nil
|
||||
} else {
|
||||
if !isValidForumPref(raw) {
|
||||
return nil, fmt.Errorf("invalid forum_pref %q (expected cms, bea, or posteingang)", raw)
|
||||
}
|
||||
val = raw
|
||||
}
|
||||
sets = append(sets, fmt.Sprintf("forum_pref = $%d", i))
|
||||
args = append(args, val)
|
||||
i++
|
||||
}
|
||||
|
||||
if len(sets) == 0 {
|
||||
// No-op PATCH is legal — just return the current row.
|
||||
@@ -832,6 +852,27 @@ func (s *UserService) AdminDeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForumPref* are the allowed values of paliad.users.forum_pref (#15).
|
||||
// CMS = UPC; beA / Posteingang = national-DE (Posteingang is the slower
|
||||
// channel for the same forums). Empty string clears the preference.
|
||||
const (
|
||||
ForumPrefCMS = "cms"
|
||||
ForumPrefBeA = "bea"
|
||||
ForumPrefPosteingang = "posteingang"
|
||||
)
|
||||
|
||||
// isValidForumPref returns true when v is one of the allowed channel
|
||||
// slugs. The DB CHECK constraint mirrors this list; we validate in the
|
||||
// service layer so callers see a typed error instead of a raw pq
|
||||
// constraint violation.
|
||||
func isValidForumPref(v string) bool {
|
||||
switch v {
|
||||
case ForumPrefCMS, ForumPrefBeA, ForumPrefPosteingang:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnonboardedAuthUser is one row in auth.users that has no matching
|
||||
// paliad.users entry — i.e. the user logged in once via Supabase but never
|
||||
// completed onboarding. Surfaced to admins so they can bulk-add real
|
||||
|
||||
Reference in New Issue
Block a user