Spaces:
Sleeping
Sleeping
Keith Yu commited on
Commit ·
7b14728
1
Parent(s): 533af38
merging changes
Browse files- backend/app.py +56 -0
- backend/supabase_schema.sql +56 -0
- index.html +5 -1
- script.js +117 -3
backend/app.py
CHANGED
|
@@ -44,6 +44,11 @@ SUPABASE_KEY = (
|
|
| 44 |
)
|
| 45 |
SUPABASE_CLAIMS_BUCKET = (os.getenv("SUPABASE_CLAIMS_BUCKET") or "").strip()
|
| 46 |
SUPABASE_CLAIMS_PREFIX = (os.getenv("SUPABASE_CLAIMS_PREFIX") or "").strip().strip("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
_supabase_client = None
|
| 49 |
|
|
@@ -204,6 +209,44 @@ def _next_id(existing_ids: List[str], prefix: str) -> str:
|
|
| 204 |
|
| 205 |
|
| 206 |
def _load_taxonomy() -> Tuple[Dict[str, str], Dict[str, str], Dict[str, str]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
codebook = _read_claim_json(CODEBOOK_NAME)
|
| 208 |
superclaims = _read_claim_json(SUPERCLAIMS_NAME)
|
| 209 |
claim_map = _read_claim_json(MAP_NAME)
|
|
@@ -350,6 +393,7 @@ class ProposalActionResponse(BaseModel):
|
|
| 350 |
|
| 351 |
class ReviewerActionRequest(BaseModel):
|
| 352 |
reviewer_name: str = Field(..., min_length=1, max_length=120)
|
|
|
|
| 353 |
|
| 354 |
|
| 355 |
def _parse_ts_to_epoch(value: Any) -> float:
|
|
@@ -876,6 +920,18 @@ def apply_proposal(proposal_id: str, req: ReviewerActionRequest) -> ProposalActi
|
|
| 876 |
|
| 877 |
codebook, superclaims, claim_map = _load_taxonomy()
|
| 878 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
if p.type == "new_superclaim":
|
| 880 |
text = str(p.payload.get("superclaimText") or "").strip()
|
| 881 |
if not text:
|
|
|
|
| 44 |
)
|
| 45 |
SUPABASE_CLAIMS_BUCKET = (os.getenv("SUPABASE_CLAIMS_BUCKET") or "").strip()
|
| 46 |
SUPABASE_CLAIMS_PREFIX = (os.getenv("SUPABASE_CLAIMS_PREFIX") or "").strip().strip("/")
|
| 47 |
+
SUPABASE_TAXONOMY_TABLES = (os.getenv("SUPABASE_TAXONOMY_TABLES") or "").strip().lower() in (
|
| 48 |
+
"1",
|
| 49 |
+
"true",
|
| 50 |
+
"yes",
|
| 51 |
+
)
|
| 52 |
|
| 53 |
_supabase_client = None
|
| 54 |
|
|
|
|
| 209 |
|
| 210 |
|
| 211 |
def _load_taxonomy() -> Tuple[Dict[str, str], Dict[str, str], Dict[str, str]]:
|
| 212 |
+
if SUPABASE_TAXONOMY_TABLES and _supabase_enabled():
|
| 213 |
+
sb = _get_supabase()
|
| 214 |
+
try:
|
| 215 |
+
sc_rows = sb.table("taxonomy_superclaims").select("id,text").execute().data or []
|
| 216 |
+
nc_rows = (
|
| 217 |
+
sb.table("taxonomy_subclaims")
|
| 218 |
+
.select("id,text,superclaim_id")
|
| 219 |
+
.execute()
|
| 220 |
+
.data
|
| 221 |
+
or []
|
| 222 |
+
)
|
| 223 |
+
except Exception as e:
|
| 224 |
+
raise HTTPException(status_code=500, detail=f"Supabase taxonomy table read failed: {e}") from e
|
| 225 |
+
|
| 226 |
+
super_norm = {}
|
| 227 |
+
for r in sc_rows:
|
| 228 |
+
if not isinstance(r, dict):
|
| 229 |
+
continue
|
| 230 |
+
sid = _normalize_superclaim_id(str(r.get("id") or "").strip())
|
| 231 |
+
txt = str(r.get("text") or "").strip()
|
| 232 |
+
if sid and txt:
|
| 233 |
+
super_norm[sid] = txt
|
| 234 |
+
|
| 235 |
+
codebook_norm = {}
|
| 236 |
+
map_norm = {}
|
| 237 |
+
for r in nc_rows:
|
| 238 |
+
if not isinstance(r, dict):
|
| 239 |
+
continue
|
| 240 |
+
nid = _normalize_subclaim_id(str(r.get("id") or "").strip())
|
| 241 |
+
txt = str(r.get("text") or "").strip()
|
| 242 |
+
sc = _normalize_superclaim_id(str(r.get("superclaim_id") or "").strip())
|
| 243 |
+
if nid and txt:
|
| 244 |
+
codebook_norm[nid] = txt
|
| 245 |
+
if nid and sc:
|
| 246 |
+
map_norm[nid] = sc
|
| 247 |
+
|
| 248 |
+
return codebook_norm, super_norm, map_norm
|
| 249 |
+
|
| 250 |
codebook = _read_claim_json(CODEBOOK_NAME)
|
| 251 |
superclaims = _read_claim_json(SUPERCLAIMS_NAME)
|
| 252 |
claim_map = _read_claim_json(MAP_NAME)
|
|
|
|
| 393 |
|
| 394 |
class ReviewerActionRequest(BaseModel):
|
| 395 |
reviewer_name: str = Field(..., min_length=1, max_length=120)
|
| 396 |
+
skip_taxonomy_update: bool = False
|
| 397 |
|
| 398 |
|
| 399 |
def _parse_ts_to_epoch(value: Any) -> float:
|
|
|
|
| 920 |
|
| 921 |
codebook, superclaims, claim_map = _load_taxonomy()
|
| 922 |
|
| 923 |
+
# In "browser-writes-taxonomy" mode, the frontend will update Supabase tables directly.
|
| 924 |
+
# The backend should only mark/log the proposal as applied.
|
| 925 |
+
if req.skip_taxonomy_update:
|
| 926 |
+
now = time.time()
|
| 927 |
+
p.payload = {**p.payload, "appliedAt": now}
|
| 928 |
+
p.appliedBy = req.reviewer_name.strip()
|
| 929 |
+
p.appliedAt = now
|
| 930 |
+
if p.type in ("merge_subclaims", "merge_superclaims"):
|
| 931 |
+
_append_merge_event(p)
|
| 932 |
+
p = _upsert_proposal(p)
|
| 933 |
+
return ProposalActionResponse(ok=True, proposal=p)
|
| 934 |
+
|
| 935 |
if p.type == "new_superclaim":
|
| 936 |
text = str(p.payload.get("superclaimText") or "").strip()
|
| 937 |
if not text:
|
backend/supabase_schema.sql
CHANGED
|
@@ -41,3 +41,59 @@ create table if not exists public.taxonomy_merge_log (
|
|
| 41 |
|
| 42 |
alter table public.taxonomy_merge_log enable row level security;
|
| 43 |
revoke all on public.taxonomy_merge_log from anon, authenticated;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
alter table public.taxonomy_merge_log enable row level security;
|
| 43 |
revoke all on public.taxonomy_merge_log from anon, authenticated;
|
| 44 |
+
|
| 45 |
+
-- ============================================================================
|
| 46 |
+
-- Public taxonomy tables (browser read/write, NO auth)
|
| 47 |
+
--
|
| 48 |
+
-- WARNING: The policies below intentionally allow anonymous users to mutate
|
| 49 |
+
-- taxonomy. Only enable this for trusted/private deployments.
|
| 50 |
+
-- ============================================================================
|
| 51 |
+
|
| 52 |
+
create table if not exists public.taxonomy_superclaims (
|
| 53 |
+
id text primary key,
|
| 54 |
+
text text not null,
|
| 55 |
+
created_at timestamptz not null default now(),
|
| 56 |
+
updated_at timestamptz not null default now()
|
| 57 |
+
);
|
| 58 |
+
|
| 59 |
+
create table if not exists public.taxonomy_subclaims (
|
| 60 |
+
id text primary key,
|
| 61 |
+
text text not null,
|
| 62 |
+
superclaim_id text not null references public.taxonomy_superclaims(id) on update cascade on delete restrict,
|
| 63 |
+
created_at timestamptz not null default now(),
|
| 64 |
+
updated_at timestamptz not null default now()
|
| 65 |
+
);
|
| 66 |
+
|
| 67 |
+
create index if not exists taxonomy_subclaims_superclaim_idx
|
| 68 |
+
on public.taxonomy_subclaims (superclaim_id);
|
| 69 |
+
|
| 70 |
+
alter table public.taxonomy_superclaims enable row level security;
|
| 71 |
+
alter table public.taxonomy_subclaims enable row level security;
|
| 72 |
+
|
| 73 |
+
-- Public read/write policies (anon + authenticated).
|
| 74 |
+
drop policy if exists "public superclaims read" on public.taxonomy_superclaims;
|
| 75 |
+
create policy "public superclaims read"
|
| 76 |
+
on public.taxonomy_superclaims for select
|
| 77 |
+
using (true);
|
| 78 |
+
|
| 79 |
+
drop policy if exists "public superclaims write" on public.taxonomy_superclaims;
|
| 80 |
+
create policy "public superclaims write"
|
| 81 |
+
on public.taxonomy_superclaims for all
|
| 82 |
+
using (true)
|
| 83 |
+
with check (true);
|
| 84 |
+
|
| 85 |
+
drop policy if exists "public subclaims read" on public.taxonomy_subclaims;
|
| 86 |
+
create policy "public subclaims read"
|
| 87 |
+
on public.taxonomy_subclaims for select
|
| 88 |
+
using (true);
|
| 89 |
+
|
| 90 |
+
drop policy if exists "public subclaims write" on public.taxonomy_subclaims;
|
| 91 |
+
create policy "public subclaims write"
|
| 92 |
+
on public.taxonomy_subclaims for all
|
| 93 |
+
using (true)
|
| 94 |
+
with check (true);
|
| 95 |
+
|
| 96 |
+
-- Allow PostgREST access for anon/authenticated roles.
|
| 97 |
+
grant usage on schema public to anon, authenticated;
|
| 98 |
+
grant select, insert, update, delete on public.taxonomy_superclaims to anon, authenticated;
|
| 99 |
+
grant select, insert, update, delete on public.taxonomy_subclaims to anon, authenticated;
|
index.html
CHANGED
|
@@ -5,8 +5,12 @@
|
|
| 5 |
<title>Native Ads Claim Mapping</title>
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
<!-- Optional: set to your deployed API origin when not using a same-origin /api rewrite (e.g. file:// or custom static hosting). Example: https://claims-backend-sigma.vercel.app -->
|
| 8 |
-
<meta name="claims-api-base" content="" />
|
|
|
|
|
|
|
|
|
|
| 9 |
<link rel="stylesheet" href="styles.css" />
|
|
|
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<main class="app">
|
|
|
|
| 5 |
<title>Native Ads Claim Mapping</title>
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
<!-- Optional: set to your deployed API origin when not using a same-origin /api rewrite (e.g. file:// or custom static hosting). Example: https://claims-backend-sigma.vercel.app -->
|
| 8 |
+
<meta name="claims-api-base" content="https://claims-backend-sigma.vercel.app" />
|
| 9 |
+
<!-- Optional: enable browser write access to Supabase taxonomy tables -->
|
| 10 |
+
<meta name="supabase-url" content="" />
|
| 11 |
+
<meta name="supabase-anon-key" content="" />
|
| 12 |
<link rel="stylesheet" href="styles.css" />
|
| 13 |
+
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
<main class="app">
|
script.js
CHANGED
|
@@ -701,6 +701,108 @@ function escapeHtml(s) {
|
|
| 701 |
|
| 702 |
const REVIEWER_STORAGE_KEY = "CLAIMS_REVIEWER_NAME";
|
| 703 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
function getReviewerName() {
|
| 705 |
const el = document.getElementById("reviewer-name");
|
| 706 |
const fromInput = el && el.value != null ? String(el.value).trim() : "";
|
|
@@ -1064,8 +1166,10 @@ async function refreshPendingProposals() {
|
|
| 1064 |
|
| 1065 |
const wrap = document.createElement("div");
|
| 1066 |
wrap.className = "proposal-list";
|
|
|
|
| 1067 |
|
| 1068 |
proposals.forEach((p) => {
|
|
|
|
| 1069 |
const card = document.createElement("article");
|
| 1070 |
card.className = "proposal-card";
|
| 1071 |
card.innerHTML = `
|
|
@@ -1107,9 +1211,19 @@ async function refreshPendingProposals() {
|
|
| 1107 |
el.disabled = false;
|
| 1108 |
return;
|
| 1109 |
}
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1113 |
// Applying should also refresh claims data next run; for now just refresh the list.
|
| 1114 |
await refreshPendingProposals();
|
| 1115 |
} catch (e) {
|
|
|
|
| 701 |
|
| 702 |
const REVIEWER_STORAGE_KEY = "CLAIMS_REVIEWER_NAME";
|
| 703 |
|
| 704 |
+
function getSupabaseConfig() {
|
| 705 |
+
const metaUrl = document.querySelector('meta[name="supabase-url"]');
|
| 706 |
+
const metaKey = document.querySelector('meta[name="supabase-anon-key"]');
|
| 707 |
+
const url = (metaUrl && metaUrl.getAttribute("content")) || "";
|
| 708 |
+
const key = (metaKey && metaKey.getAttribute("content")) || "";
|
| 709 |
+
return { url: String(url || "").trim(), key: String(key || "").trim() };
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
function getSupabaseClient() {
|
| 713 |
+
const cfg = getSupabaseConfig();
|
| 714 |
+
if (!cfg.url || !cfg.key) return null;
|
| 715 |
+
if (typeof window === "undefined") return null;
|
| 716 |
+
if (!window.supabase || typeof window.supabase.createClient !== "function") return null;
|
| 717 |
+
try {
|
| 718 |
+
return window.supabase.createClient(cfg.url, cfg.key);
|
| 719 |
+
} catch {
|
| 720 |
+
return null;
|
| 721 |
+
}
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
function nextIdFromRows(rows, prefix) {
|
| 725 |
+
let max = 0;
|
| 726 |
+
for (const r of rows || []) {
|
| 727 |
+
const id = String(r?.id || "");
|
| 728 |
+
if (!id.startsWith(prefix)) continue;
|
| 729 |
+
const n = parseInt(id.slice(prefix.length), 10);
|
| 730 |
+
if (!Number.isNaN(n)) max = Math.max(max, n);
|
| 731 |
+
}
|
| 732 |
+
return `${prefix}${max + 1}`;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
async function applyProposalToSupabaseTaxonomy(p) {
|
| 736 |
+
const sb = getSupabaseClient();
|
| 737 |
+
if (!sb) throw new Error("Supabase taxonomy config missing (set meta supabase-url and supabase-anon-key).");
|
| 738 |
+
|
| 739 |
+
const type = String(p?.type || "");
|
| 740 |
+
const payload = p?.payload || {};
|
| 741 |
+
|
| 742 |
+
if (type === "new_superclaim") {
|
| 743 |
+
const text = String(payload.superclaimText || "").trim();
|
| 744 |
+
if (!text) throw new Error("Missing superclaimText");
|
| 745 |
+
|
| 746 |
+
const { data: existing, error: selErr } = await sb
|
| 747 |
+
.from("taxonomy_superclaims")
|
| 748 |
+
.select("id")
|
| 749 |
+
.order("id", { ascending: false });
|
| 750 |
+
if (selErr) throw new Error(selErr.message || String(selErr));
|
| 751 |
+
|
| 752 |
+
const newId = nextIdFromRows(existing || [], "SC_");
|
| 753 |
+
const { error: insErr } = await sb.from("taxonomy_superclaims").insert({ id: newId, text });
|
| 754 |
+
if (insErr) throw new Error(insErr.message || String(insErr));
|
| 755 |
+
return;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
if (type === "merge_subclaims") {
|
| 759 |
+
const canonical = String(payload.canonicalSubclaimId || "").trim();
|
| 760 |
+
const remove = String(payload.removeSubclaimId || "").trim();
|
| 761 |
+
if (!canonical || !remove || canonical === remove) throw new Error("Missing canonical/remove subclaim ids");
|
| 762 |
+
|
| 763 |
+
const { data: rows, error } = await sb
|
| 764 |
+
.from("taxonomy_subclaims")
|
| 765 |
+
.select("id,superclaim_id")
|
| 766 |
+
.in("id", [canonical, remove]);
|
| 767 |
+
if (error) throw new Error(error.message || String(error));
|
| 768 |
+
const canonRow = (rows || []).find((r) => String(r.id) === canonical);
|
| 769 |
+
const remRow = (rows || []).find((r) => String(r.id) === remove);
|
| 770 |
+
if (!canonRow || !remRow) throw new Error("Unknown subclaim id(s) in taxonomy_subclaims");
|
| 771 |
+
if (String(canonRow.superclaim_id) !== String(remRow.superclaim_id)) {
|
| 772 |
+
throw new Error("Refusing merge: subclaims are not mapped to the same superclaim.");
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
const { error: delErr } = await sb.from("taxonomy_subclaims").delete().eq("id", remove);
|
| 776 |
+
if (delErr) throw new Error(delErr.message || String(delErr));
|
| 777 |
+
return;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
if (type === "merge_superclaims") {
|
| 781 |
+
const canonical = String(payload.canonicalSuperclaimId || "").trim();
|
| 782 |
+
const remove = String(payload.removeSuperclaimId || "").trim();
|
| 783 |
+
if (!canonical || !remove || canonical === remove) throw new Error("Missing canonical/remove superclaim ids");
|
| 784 |
+
|
| 785 |
+
const { data: rows, error } = await sb
|
| 786 |
+
.from("taxonomy_superclaims")
|
| 787 |
+
.select("id")
|
| 788 |
+
.in("id", [canonical, remove]);
|
| 789 |
+
if (error) throw new Error(error.message || String(error));
|
| 790 |
+
if (!Array.isArray(rows) || rows.length !== 2) throw new Error("Unknown superclaim id(s) in taxonomy_superclaims");
|
| 791 |
+
|
| 792 |
+
const { error: updErr } = await sb
|
| 793 |
+
.from("taxonomy_subclaims")
|
| 794 |
+
.update({ superclaim_id: canonical })
|
| 795 |
+
.eq("superclaim_id", remove);
|
| 796 |
+
if (updErr) throw new Error(updErr.message || String(updErr));
|
| 797 |
+
|
| 798 |
+
const { error: delErr } = await sb.from("taxonomy_superclaims").delete().eq("id", remove);
|
| 799 |
+
if (delErr) throw new Error(delErr.message || String(delErr));
|
| 800 |
+
return;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
throw new Error(`Unsupported proposal type for browser taxonomy apply: ${type}`);
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
function getReviewerName() {
|
| 807 |
const el = document.getElementById("reviewer-name");
|
| 808 |
const fromInput = el && el.value != null ? String(el.value).trim() : "";
|
|
|
|
| 1166 |
|
| 1167 |
const wrap = document.createElement("div");
|
| 1168 |
wrap.className = "proposal-list";
|
| 1169 |
+
const byId = new Map();
|
| 1170 |
|
| 1171 |
proposals.forEach((p) => {
|
| 1172 |
+
if (p && p.id) byId.set(String(p.id), p);
|
| 1173 |
const card = document.createElement("article");
|
| 1174 |
card.className = "proposal-card";
|
| 1175 |
card.innerHTML = `
|
|
|
|
| 1211 |
el.disabled = false;
|
| 1212 |
return;
|
| 1213 |
}
|
| 1214 |
+
if (action === "apply" && getSupabaseClient()) {
|
| 1215 |
+
const p = byId.get(String(id));
|
| 1216 |
+
if (!p) throw new Error("Missing proposal payload in UI.");
|
| 1217 |
+
await applyProposalToSupabaseTaxonomy(p);
|
| 1218 |
+
await postJson(`/api/proposals/${encodeURIComponent(id)}/${action}`, {
|
| 1219 |
+
reviewer_name: reviewer,
|
| 1220 |
+
skip_taxonomy_update: true,
|
| 1221 |
+
});
|
| 1222 |
+
} else {
|
| 1223 |
+
await postJson(`/api/proposals/${encodeURIComponent(id)}/${action}`, {
|
| 1224 |
+
reviewer_name: reviewer,
|
| 1225 |
+
});
|
| 1226 |
+
}
|
| 1227 |
// Applying should also refresh claims data next run; for now just refresh the list.
|
| 1228 |
await refreshPendingProposals();
|
| 1229 |
} catch (e) {
|