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
@@ -3,11 +3,12 @@ import { apiFetch, jsonResponse } from '../../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function POST(
_req: Request,
req: Request,
{ params }: { params: Promise<{ id: string }> },
): Promise<Response> {
const { id } = await params;
const res = await apiFetch(`/admin/messages/${id}/approve`, { method: 'POST' });
const reqBody = await req.text().catch(() => '');
const res = await apiFetch(`/admin/messages/${id}/approve`, { method: 'POST', body: reqBody || undefined });
const body = await res.text();
let payload: unknown = body;
try {
@@ -0,0 +1,15 @@
import { apiFetch, jsonResponse } from '../../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
): Promise<Response> {
const { id } = await params;
const res = await apiFetch(`/admin/messages/${id}/routes`);
const text = await res.text();
let payload: unknown = text;
try { payload = JSON.parse(text); } catch { /* keep as text */ }
return jsonResponse(payload, res.status);
}
+14 -1
View File
@@ -1,7 +1,20 @@
import { apiFetch } from '../../../_lib/api';
import { apiFetch, jsonResponse } from '../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function PATCH(
req: Request,
{ params }: { params: Promise<{ id: string }> },
): Promise<Response> {
const { id } = await params;
const body = await req.text();
const res = await apiFetch(`/routes/${id}`, { method: 'PATCH', body });
const text = await res.text();
let payload: unknown = text;
try { payload = JSON.parse(text); } catch { /* keep as text */ }
return jsonResponse(payload, res.status);
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
@@ -0,0 +1,26 @@
import { apiFetch, jsonResponse } from '../../../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function POST(
_req: Request,
{ params }: { params: Promise<{ id: string; tenantRuleId: string }> },
): Promise<Response> {
const { id, tenantRuleId } = await params;
const res = await apiFetch(`/routes/${id}/rules/${tenantRuleId}`, { method: 'POST' });
const text = await res.text();
let payload: unknown = text;
try { payload = JSON.parse(text); } catch { /* keep as text */ }
return jsonResponse(payload, res.status);
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string; tenantRuleId: string }> },
): Promise<Response> {
const { id, tenantRuleId } = await params;
const res = await apiFetch(`/routes/${id}/rules/${tenantRuleId}`, { method: 'DELETE' });
if (res.status === 204) return new Response(null, { status: 204 });
const body = await res.text();
return new Response(body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
}
@@ -0,0 +1,15 @@
import { apiFetch, jsonResponse } from '../../../../_lib/api';
export const dynamic = 'force-dynamic';
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
): Promise<Response> {
const { id } = await params;
const res = await apiFetch(`/routes/${id}/rules`);
const text = await res.text();
let payload: unknown = text;
try { payload = JSON.parse(text); } catch { /* keep as text */ }
return jsonResponse(payload, res.status);
}