feat: route-centric rules, approve popup, emoji picker, 5-column sync table

- Schema: SyncRouteRuleMap junction table, ruleIntent on TenantRule,
  ForwardFormat/Frequency/ApprovalMode columns on SyncRoute, INTEREST
  match type
- Worker: remove global AUTO_APPROVE forwarding; per-route rule evaluation
  in ingest processor — POSITIVE rules auto-forward that route, NEGATIVE
  rules block it, fallback to approvalMode
- API: PATCH /routes/:id, rule assign/unassign endpoints, approve now
  accepts targetGroupIds, new GET /messages/:id/routes for popup data
- Web: 5-column route table with settings + rules dialogs, ForwardTarget
  popup with rule-based pre-selection, ruleIntent toggle + emoji dropdown
  (👍/👎 only) + INTEREST type in rules manager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 13:58:02 +05:30
parent e598073dc7
commit 73956fd1e3
24 changed files with 1277 additions and 225 deletions
@@ -0,0 +1,45 @@
-- CreateEnum
CREATE TYPE "RuleIntent" AS ENUM ('POSITIVE', 'NEGATIVE');
CREATE TYPE "ForwardFormat" AS ENUM ('THREADED', 'DIGEST', 'SUMMARY');
CREATE TYPE "ForwardFrequency" AS ENUM ('INSTANT', 'FIVE_MIN', 'FIFTEEN_MIN', 'ONE_HOUR', 'ONE_DAY', 'SEVEN_DAYS');
CREATE TYPE "ApprovalMode" AS ENUM ('MANUAL', 'AUTO');
-- AlterEnum: Add INTEREST to RuleMatchType
ALTER TYPE "RuleMatchType" ADD VALUE IF NOT EXISTS 'INTEREST';
-- AlterTable: TenantRule - add ruleIntent
ALTER TABLE "TenantRule" ADD COLUMN IF NOT EXISTS "ruleIntent" "RuleIntent" NOT NULL DEFAULT 'POSITIVE';
-- AlterTable: SyncRoute - add new columns (updatedAt with default NOW() for existing rows)
ALTER TABLE "SyncRoute"
ADD COLUMN IF NOT EXISTS "forwardFormat" "ForwardFormat" NOT NULL DEFAULT 'THREADED',
ADD COLUMN IF NOT EXISTS "withAttachment" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS "frequency" "ForwardFrequency" NOT NULL DEFAULT 'INSTANT',
ADD COLUMN IF NOT EXISTS "approvalMode" "ApprovalMode" NOT NULL DEFAULT 'MANUAL',
ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT NOW();
-- CreateTable: SyncRouteRuleMap
CREATE TABLE IF NOT EXISTS "SyncRouteRuleMap" (
"id" TEXT NOT NULL,
"syncRouteId" TEXT NOT NULL,
"tenantRuleId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SyncRouteRuleMap_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "SyncRouteRuleMap_syncRouteId_tenantRuleId_key"
ON "SyncRouteRuleMap"("syncRouteId", "tenantRuleId");
CREATE INDEX IF NOT EXISTS "SyncRouteRuleMap_syncRouteId_idx"
ON "SyncRouteRuleMap"("syncRouteId");
CREATE INDEX IF NOT EXISTS "SyncRouteRuleMap_tenantRuleId_idx"
ON "SyncRouteRuleMap"("tenantRuleId");
-- AddForeignKey
ALTER TABLE "SyncRouteRuleMap"
ADD CONSTRAINT "SyncRouteRuleMap_syncRouteId_fkey"
FOREIGN KEY ("syncRouteId") REFERENCES "SyncRoute"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SyncRouteRuleMap"
ADD CONSTRAINT "SyncRouteRuleMap_tenantRuleId_fkey"
FOREIGN KEY ("tenantRuleId") REFERENCES "TenantRule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+68 -19
View File
@@ -337,20 +337,40 @@ enum ApprovalDecision {
}
model SyncRoute {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
sourceGroupId String
sourceGroup Group @relation("sourceGroup", fields: [sourceGroupId], references: [id])
targetGroupId String
targetGroup Group @relation("targetGroup", fields: [targetGroupId], references: [id])
isActive Boolean @default(true)
createdAt DateTime @default(now())
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
sourceGroupId String
sourceGroup Group @relation("sourceGroup", fields: [sourceGroupId], references: [id])
targetGroupId String
targetGroup Group @relation("targetGroup", fields: [targetGroupId], references: [id])
isActive Boolean @default(true)
forwardFormat ForwardFormat @default(THREADED)
withAttachment Boolean @default(true)
frequency ForwardFrequency @default(INSTANT)
approvalMode ApprovalMode @default(MANUAL)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignedRules SyncRouteRuleMap[]
@@unique([sourceGroupId, targetGroupId])
@@index([tenantId])
}
model SyncRouteRuleMap {
id String @id @default(cuid())
syncRouteId String
syncRoute SyncRoute @relation(fields: [syncRouteId], references: [id], onDelete: Cascade)
tenantRuleId String
tenantRule TenantRule @relation(fields: [tenantRuleId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([syncRouteId, tenantRuleId])
@@index([syncRouteId])
@@index([tenantRuleId])
}
// ============================================================================
// Group claiming + sharing
// ============================================================================
@@ -515,6 +535,7 @@ enum RuleMatchType {
HASHTAG
PREFIX
REACTION_EMOJI
INTEREST
}
enum RuleAction {
@@ -524,17 +545,45 @@ enum RuleAction {
REJECT
}
enum RuleIntent {
POSITIVE
NEGATIVE
}
enum ForwardFormat {
THREADED
DIGEST
SUMMARY
}
enum ForwardFrequency {
INSTANT
FIVE_MIN
FIFTEEN_MIN
ONE_HOUR
ONE_DAY
SEVEN_DAYS
}
enum ApprovalMode {
MANUAL
AUTO
}
model TenantRule {
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
matchType RuleMatchType
matchValue String
action RuleAction
priority Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
matchType RuleMatchType
matchValue String
action RuleAction
ruleIntent RuleIntent @default(POSITIVE)
priority Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
syncRouteRuleMaps SyncRouteRuleMap[]
@@unique([tenantId, matchType, matchValue])
@@index([tenantId, isActive])