From 102343c8fe0970fa5d4b27b76262fcda1e1d1d18 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sun, 3 May 2026 11:19:09 -0500 Subject: [PATCH] fix(gallery): implement voting JS + fix change-vote server path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voting was completely broken: gallery.el referenced d8251f5e5aa1.js which was never written. Buttons rendered disabled with no JS to enable them, load vote state, or call /api/vote. Fix 1 — client: inline the voting script directly in gallery.el. Initializes Supabase client from window.NEURON_CFG, calls /api/vote-state/ on load (with JWT if signed in) to populate scores and active states, wires vote buttons with toggle logic (same direction = undo/none), handles sign-in modal with magic-link flow, re-loads all vote states on auth state change. Fix 2 — server: replace supabase_upsert_user (upsert via user JWT) with delete-then-insert. Upsert requires both INSERT + UPDATE RLS policies; the UPDATE policy is typically absent on share_votes. Delete (user JWT, RLS-safe) + insert (service key, user already auth-validated) is reliable for both new votes and vote changes. --- src/gallery.el | 108 ++++++++++++++++++++++++++++++++++++++++++++++++- src/main.el | 14 +++++-- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/gallery.el b/src/gallery.el index 01528df..3ad5b4b 100644 --- a/src/gallery.el +++ b/src/gallery.el @@ -271,7 +271,113 @@ window.NEURON_CFG=window.NEURON_CFG||{}; window.NEURON_CFG.supabase_url=\"" + supabase_url + "\"; window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\"; - + " diff --git a/src/main.el b/src/main.el index ac0e8d0..2a4c0c4 100644 --- a/src/main.el +++ b/src/main.el @@ -1448,11 +1448,17 @@ fn handle_request(method: String, path: String, body: String) -> String { let del_url: String = v_sb_url + "/rest/v1/share_votes?share_id=eq." + v_id + "&user_id=eq." + v_uid let _del_resp: String = http_delete_auth(del_url, v_jwt, v_anon) } else { - // up/down - upsert. PostgREST resolves on the (share_id, user_id) PK - // when on_conflict matches. The trigger fires on insert and update. + // up/down — delete any existing vote first (idempotent), then insert fresh. + // Upsert via user JWT requires both INSERT + UPDATE RLS policies; the + // UPDATE policy is often absent. Delete-then-insert is more reliable: + // user JWT covers the DELETE (auth.uid()=user_id RLS passes), + // service key covers the INSERT (bypasses RLS; user identity already + // validated above via supabase_auth_user). The recalc trigger fires on + // both operations and keeps share_cards.score accurate. + let del_url: String = v_sb_url + "/rest/v1/share_votes?share_id=eq." + v_id + "&user_id=eq." + v_uid + let _del_r: String = http_delete_auth(del_url, v_jwt, v_anon) let row: String = "{\"share_id\":\"" + v_id + "\",\"user_id\":\"" + v_uid + "\",\"direction\":\"" + v_dir + "\"}" - let up_path: String = "share_votes?on_conflict=share_id,user_id" - let _up_resp: String = supabase_upsert_user(v_sb_url, v_anon, v_jwt, up_path, row) + let _ins_r: String = supabase_insert(v_sb_url, v_service, "share_votes", row) } // Re-fetch fresh aggregate from share_cards (service key - public read). // PostgREST returns a JSON array; use json_array_get(0) then json_get.