everydaycats commited on
Commit
4440ce4
Β·
verified Β·
1 Parent(s): d370d4a

Update apps/aura_measure.js

Browse files
Files changed (1) hide show
  1. apps/aura_measure.js +336 -81
apps/aura_measure.js CHANGED
@@ -8,9 +8,10 @@ import { supabase } from '../config/supabaseClient.js';
8
  const router = express.Router();
9
 
10
  const TEMP_DIR = path.join(process.cwd(), 'tmp_scans');
11
- const tempScans = new Map();
12
  fs.mkdir(TEMP_DIR, { recursive: true }).catch(console.error);
13
 
 
14
  setInterval(async () => {
15
  const now = Date.now();
16
  for (const [tempId, data] of tempScans.entries()) {
@@ -22,20 +23,23 @@ setInterval(async () => {
22
  }, 5 * 60 * 1000);
23
 
24
  const cache = {
25
- feed_page_1: { data: null, expires: 0 },
26
- leaderboard_all: { data: null, expires: 0 },
27
- leaderboard_session: { data: null, expires: 0 },
28
- leaderboard_grade: { data: null, expires: 0 },
29
- challenges_active: { data: null, expires: 0 }
30
  };
31
- const CACHE_TTL = 60 * 1000;
32
 
33
  function invalidateCaches(type) {
34
- if (type === 'feed') cache.feed_page_1.expires = 0;
 
 
 
35
  if (type === 'leaderboard') {
36
- cache.leaderboard_all.expires = 0;
37
- cache.leaderboard_session.expires = 0;
38
- cache.leaderboard_grade.expires = 0;
39
  }
40
  }
41
 
@@ -68,28 +72,55 @@ Image 1: The Reference Image
68
  Image 2: The Submission
69
  Evaluate Image 2 strictly based on how well it matched the vibe and quality of Image 1. ${BASE_RULES}`;
70
 
 
 
71
  router.post('/analyze', async (req, res) => {
72
  try {
73
- const { device_id, username, avatar, media_base64, context_type = 'standalone', context_id = null, reference_data = null } = req.body;
74
- if (!media_base64 || !device_id) throw new Error("Missing media or device_id.");
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- let { data: profile } = await supabase
 
77
  .from('aura_profiles')
78
  .upsert({ device_id, username, avatar }, { onConflict: 'device_id' })
79
- .select('id').single();
 
 
 
 
 
 
 
 
 
 
80
 
81
- // FIX: Used maybeSingle() instead of single() to prevent PGRST116 crashes
82
- let { data: session } = await supabase.from('aura_sessions').select('id').eq('is_active', true).maybeSingle();
83
  if (!session) {
84
  const start = new Date();
85
  const end = new Date(start.getTime() + (3 * 24 * 60 * 60 * 1000));
86
- const { data: newSession } = await supabase.from('aura_sessions')
87
- .insert({ start_time: start, end_time: end, is_active: true }).select('id').single();
 
 
 
 
88
  session = newSession;
89
  }
90
 
 
91
  let systemPrompt = AURA_PROMPT_STANDALONE;
92
- let promptText = "Analyze this image and calculate my Aura. Return JSON.";
93
  let imagesToProcess = [media_base64];
94
 
95
  if ((context_type === 'challenge' || context_type === 'thread') && reference_data) {
@@ -99,186 +130,410 @@ router.post('/analyze', async (req, res) => {
99
  }
100
 
101
  const aiResult = await generateCompletion({
102
- model: "qwen-vl-max",
103
  prompt: promptText,
104
  system_prompt: systemPrompt,
105
- images: imagesToProcess
106
  });
107
 
108
  if (!aiResult.success) throw new Error(aiResult.error);
109
- const cleanJsonStr = aiResult.data.replace(/```json/gi, '').replace(/```/gi, '').trim();
110
- const parsedData = JSON.parse(cleanJsonStr);
111
 
112
- await supabase.rpc('aura_increment_points', {
113
- p_user_id: profile.id, p_session_id: session.id, p_score_to_add: parsedData.score, p_is_challenge: context_type === 'challenge'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  });
 
115
 
 
116
  const tempId = crypto.randomUUID();
117
  const filepath = path.join(TEMP_DIR, `${tempId}.jpg`);
118
  await fs.writeFile(filepath, Buffer.from(media_base64, 'base64'));
119
 
120
  tempScans.set(tempId, {
121
- filepath, timestamp: Date.now(), user_id: profile.id, session_id: session.id,
122
- score: parsedData.score, analysis: parsedData.analysis_markdown, context_type, context_id
 
 
 
 
 
 
123
  });
124
 
125
  invalidateCaches('leaderboard');
126
- res.json({ success: true, score: parsedData.score, analysis: parsedData.analysis_markdown, temp_scan_id: tempId });
 
 
 
 
 
 
 
127
  } catch (err) {
 
128
  res.status(500).json({ success: false, error: err.message });
129
  }
130
  });
131
 
 
 
132
  router.post('/publish', async (req, res) => {
133
  try {
134
  const { temp_scan_id } = req.body;
135
- if (!temp_scan_id || !tempScans.has(temp_scan_id)) return res.status(400).json({ success: false, error: "Scan expired." });
 
 
 
136
 
137
  const scanData = tempScans.get(temp_scan_id);
138
  const fileName = `${scanData.user_id}/${Date.now()}_${temp_scan_id}.jpg`;
139
  const fileBuffer = await fs.readFile(scanData.filepath);
140
 
141
- await supabase.storage.from('aura_media').upload(fileName, fileBuffer, { contentType: 'image/jpeg' });
142
- const { data: publicUrlData } = supabase.storage.from('aura_media').getPublicUrl(fileName);
 
143
 
144
- await supabase.from('aura_public_scans').insert({
145
- user_id: scanData.user_id, session_id: scanData.session_id, image_url: publicUrlData.publicUrl,
146
- score: scanData.score, analysis_json: scanData.analysis, context_type: scanData.context_type, context_id: scanData.context_id
147
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  await fs.unlink(scanData.filepath);
150
  tempScans.delete(temp_scan_id);
151
  invalidateCaches('feed');
152
- res.json({ success: true, message: "Published." });
 
 
153
  } catch (err) {
 
154
  res.status(500).json({ success: false, error: err.message });
155
  }
156
  });
157
 
 
 
158
  router.get('/feed', async (req, res) => {
159
  try {
160
- const { cursor, limit = 20 } = req.query;
 
 
 
161
  if (!cursor && cache.feed_page_1.expires > Date.now() && cache.feed_page_1.data) {
162
  return res.json({ success: true, data: cache.feed_page_1.data, cached: true });
163
  }
164
 
165
- let query = supabase.from('aura_public_scans')
166
- .select(`id, image_url, score, context_type, context_id, created_at, aura_profiles ( username, avatar, current_grade )`)
167
- .order('created_at', { ascending: false }).limit(parseInt(limit));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- if (cursor) query = query.lt('created_at', cursor);
170
  const { data, error } = await query;
171
  if (error) throw error;
172
 
173
- const nextCursor = data.length === parseInt(limit) ? data[data.length - 1].created_at : null;
 
 
 
174
  const responseData = { items: data, nextCursor };
175
 
176
- if (!cursor) cache.feed_page_1 = { data: responseData, expires: Date.now() + CACHE_TTL };
 
 
 
177
  res.json({ success: true, data: responseData });
 
178
  } catch (err) {
 
179
  res.status(500).json({ success: false, error: err.message });
180
  }
181
  });
182
 
 
 
183
  router.get('/leaderboard', async (req, res) => {
184
  try {
185
- const { view = 'all_time' } = req.query;
 
 
 
 
 
 
 
186
  const cacheKey = `leaderboard_${view}`;
 
187
  if (cache[cacheKey]?.expires > Date.now() && cache[cacheKey]?.data) {
188
  return res.json({ success: true, data: cache[cacheKey].data, cached: true });
189
  }
190
 
191
  let data, error;
 
192
  if (view === 'all_time') {
193
- ({ data, error } = await supabase.from('aura_profiles')
194
- .select('id, username, avatar, current_grade, cumulative_aura as score')
195
- .order('cumulative_aura', { ascending: false }).limit(50));
196
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  else if (view === 'session') {
198
- // FIX: maybeSingle()
199
- const { data: activeSession } = await supabase.from('aura_sessions').select('id').eq('is_active', true).maybeSingle();
200
- if (!activeSession) return res.json({ success: true, data: [] });
201
-
202
- ({ data, error } = await supabase.from('aura_user_session_stats')
203
- .select(`session_aura as score, aura_profiles ( id, username, avatar, current_grade )`)
204
- .eq('session_id', activeSession.id).order('session_aura', { ascending: false }).limit(50));
205
- if (data) data = data.map(d => ({ ...d.aura_profiles, score: d.score }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
207
  else if (view === 'grade') {
208
- ({ data, error } = await supabase.from('aura_profiles')
209
- .select('id, username, avatar, current_grade, cumulative_aura as score')
210
- .order('current_grade', { ascending: false }).order('cumulative_aura', { ascending: false }).limit(50));
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
 
213
  if (error) throw error;
 
214
  cache[cacheKey] = { data, expires: Date.now() + CACHE_TTL };
215
  res.json({ success: true, data });
 
216
  } catch (err) {
 
217
  res.status(500).json({ success: false, error: err.message });
218
  }
219
  });
220
 
 
 
221
  router.get('/challenges', async (req, res) => {
222
  try {
223
- const { status = 'active' } = req.query;
224
- if (status === 'active' && cache.challenges_active.expires > Date.now()) {
225
- return res.json({ success: true, data: cache.challenges_active.data, cached: true });
 
 
 
 
 
 
 
 
 
 
226
  }
227
 
228
- const { data: session } = await supabase.from('aura_sessions').select('id').eq('is_active', true).maybeSingle();
229
- let query = supabase.from('aura_challenges').select('*').order('created_at', { ascending: false });
230
- if (status === 'active' && session) query = query.eq('session_id', session.id);
231
- if (status === 'archive' && session) query = query.neq('session_id', session.id);
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  const { data, error } = await query;
234
  if (error) throw error;
235
 
236
- if (status === 'active') cache.challenges_active = { data, expires: Date.now() + CACHE_TTL };
 
 
 
237
  res.json({ success: true, data });
 
238
  } catch (err) {
 
239
  res.status(500).json({ success: false, error: err.message });
240
  }
241
  });
242
 
 
 
243
  router.get('/threads/:context_id', async (req, res) => {
244
  try {
245
  const { context_id } = req.params;
246
  const { limit = 20, offset = 0 } = req.query;
247
- const { data, error } = await supabase.from('aura_thread_summaries')
248
- .select(`collection_aura, submission_count, top_image_url, aura_profiles ( id, username, avatar, current_grade )`)
249
- .eq('context_id', context_id).order('collection_aura', { ascending: false }).range(offset, parseInt(offset) + parseInt(limit) - 1);
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  if (error) throw error;
252
- const items = data.map(d => ({ ...d.aura_profiles, collection_aura: d.collection_aura, submission_count: d.submission_count, top_image_url: d.top_image_url }));
 
 
 
 
 
 
 
253
  res.json({ success: true, data: items });
 
254
  } catch (err) {
 
255
  res.status(500).json({ success: false, error: err.message });
256
  }
257
  });
258
 
 
 
259
  router.post('/tag', async (req, res) => {
260
  try {
261
  const { tagger_scan_id, tagged_scan_id } = req.body;
262
- const { error } = await supabase.from('aura_thread_tags').insert({ tagger_scan_id, tagged_scan_id });
263
- if (error && error.code !== '23505') throw error;
264
- res.json({ success: true, message: "Tagged successfully" });
 
 
 
 
 
 
 
 
 
 
 
265
  } catch (err) {
 
266
  res.status(500).json({ success: false, error: err.message });
267
  }
268
  });
269
 
 
 
270
  router.get('/profile/:username', async (req, res) => {
271
  try {
272
  const { username } = req.params;
273
- const { data: profile, error: profErr } = await supabase.from('aura_profiles')
274
- .select('id, username, avatar, current_grade, cumulative_aura').eq('username', username).single();
275
-
276
- if (profErr || !profile) throw new Error("Sorcerer not found.");
277
- const { data: scans, error: scansErr } = await supabase.from('aura_public_scans')
278
- .select('id, image_url, score, context_type, created_at').eq('user_id', profile.id).order('created_at', { ascending: false });
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
  res.json({ success: true, data: { profile, scans } });
 
281
  } catch (err) {
 
282
  res.status(500).json({ success: false, error: err.message });
283
  }
284
  });
 
8
  const router = express.Router();
9
 
10
  const TEMP_DIR = path.join(process.cwd(), 'tmp_scans');
11
+ const tempScans = new Map();
12
  fs.mkdir(TEMP_DIR, { recursive: true }).catch(console.error);
13
 
14
+ // Clean up expired temp scans every 5 minutes
15
  setInterval(async () => {
16
  const now = Date.now();
17
  for (const [tempId, data] of tempScans.entries()) {
 
23
  }, 5 * 60 * 1000);
24
 
25
  const cache = {
26
+ feed_page_1: { data: null, expires: 0 },
27
+ leaderboard_all: { data: null, expires: 0 },
28
+ leaderboard_session:{ data: null, expires: 0 },
29
+ leaderboard_grade: { data: null, expires: 0 },
30
+ challenges_active: { data: null, expires: 0 }
31
  };
32
+ const CACHE_TTL = 60 * 1000; // 1 minute
33
 
34
  function invalidateCaches(type) {
35
+ if (type === 'feed') {
36
+ cache.feed_page_1.expires = 0;
37
+ cache.feed_page_1.data = null;
38
+ }
39
  if (type === 'leaderboard') {
40
+ cache.leaderboard_all.expires = 0; cache.leaderboard_all.data = null;
41
+ cache.leaderboard_session.expires = 0; cache.leaderboard_session.data = null;
42
+ cache.leaderboard_grade.expires = 0; cache.leaderboard_grade.data = null;
43
  }
44
  }
45
 
 
72
  Image 2: The Submission
73
  Evaluate Image 2 strictly based on how well it matched the vibe and quality of Image 1. ${BASE_RULES}`;
74
 
75
+ // ─── POST /analyze ─────────────────────────────────────────────────────────────
76
+
77
  router.post('/analyze', async (req, res) => {
78
  try {
79
+ const {
80
+ device_id,
81
+ username,
82
+ avatar,
83
+ media_base64,
84
+ context_type = 'standalone',
85
+ context_id = null,
86
+ reference_data = null
87
+ } = req.body;
88
+
89
+ if (!media_base64 || !device_id) {
90
+ return res.status(400).json({ success: false, error: 'Missing media or device_id.' });
91
+ }
92
 
93
+ // Upsert profile
94
+ const { data: profile, error: profileError } = await supabase
95
  .from('aura_profiles')
96
  .upsert({ device_id, username, avatar }, { onConflict: 'device_id' })
97
+ .select('id')
98
+ .single();
99
+
100
+ if (profileError) throw profileError;
101
+
102
+ // Get or create active session
103
+ let { data: session } = await supabase
104
+ .from('aura_sessions')
105
+ .select('id')
106
+ .eq('is_active', true)
107
+ .maybeSingle();
108
 
 
 
109
  if (!session) {
110
  const start = new Date();
111
  const end = new Date(start.getTime() + (3 * 24 * 60 * 60 * 1000));
112
+ const { data: newSession, error: sessionError } = await supabase
113
+ .from('aura_sessions')
114
+ .insert({ start_time: start, end_time: end, is_active: true })
115
+ .select('id')
116
+ .single();
117
+ if (sessionError) throw sessionError;
118
  session = newSession;
119
  }
120
 
121
+ // Build prompt
122
  let systemPrompt = AURA_PROMPT_STANDALONE;
123
+ let promptText = 'Analyze this image and calculate my Aura. Return JSON.';
124
  let imagesToProcess = [media_base64];
125
 
126
  if ((context_type === 'challenge' || context_type === 'thread') && reference_data) {
 
130
  }
131
 
132
  const aiResult = await generateCompletion({
133
+ model: 'qwen-vl-max',
134
  prompt: promptText,
135
  system_prompt: systemPrompt,
136
+ images: imagesToProcess
137
  });
138
 
139
  if (!aiResult.success) throw new Error(aiResult.error);
 
 
140
 
141
+ const cleanJsonStr = aiResult.data
142
+ .replace(/```json/gi, '')
143
+ .replace(/```/gi, '')
144
+ .trim();
145
+
146
+ let parsedData;
147
+ try {
148
+ parsedData = JSON.parse(cleanJsonStr);
149
+ } catch (parseErr) {
150
+ throw new Error(`AI returned invalid JSON: ${cleanJsonStr.substring(0, 100)}`);
151
+ }
152
+
153
+ if (typeof parsedData.score !== 'number') {
154
+ throw new Error('AI response missing valid score field.');
155
+ }
156
+
157
+ // Increment aura points via RPC
158
+ const { error: rpcError } = await supabase.rpc('aura_increment_points', {
159
+ p_user_id: profile.id,
160
+ p_session_id: session.id,
161
+ p_score_to_add: parsedData.score,
162
+ p_is_challenge: context_type === 'challenge'
163
  });
164
+ if (rpcError) throw rpcError;
165
 
166
+ // Store temp scan for optional publish
167
  const tempId = crypto.randomUUID();
168
  const filepath = path.join(TEMP_DIR, `${tempId}.jpg`);
169
  await fs.writeFile(filepath, Buffer.from(media_base64, 'base64'));
170
 
171
  tempScans.set(tempId, {
172
+ filepath,
173
+ timestamp: Date.now(),
174
+ user_id: profile.id,
175
+ session_id: session.id,
176
+ score: parsedData.score,
177
+ analysis: parsedData.analysis_markdown,
178
+ context_type,
179
+ context_id
180
  });
181
 
182
  invalidateCaches('leaderboard');
183
+
184
+ res.json({
185
+ success: true,
186
+ score: parsedData.score,
187
+ analysis: parsedData.analysis_markdown,
188
+ temp_scan_id: tempId
189
+ });
190
+
191
  } catch (err) {
192
+ console.error('[Analyze] Error:', err);
193
  res.status(500).json({ success: false, error: err.message });
194
  }
195
  });
196
 
197
+ // ─── POST /publish ─────────────────────────────────────────────────────────────
198
+
199
  router.post('/publish', async (req, res) => {
200
  try {
201
  const { temp_scan_id } = req.body;
202
+
203
+ if (!temp_scan_id || !tempScans.has(temp_scan_id)) {
204
+ return res.status(400).json({ success: false, error: 'Scan expired or not found.' });
205
+ }
206
 
207
  const scanData = tempScans.get(temp_scan_id);
208
  const fileName = `${scanData.user_id}/${Date.now()}_${temp_scan_id}.jpg`;
209
  const fileBuffer = await fs.readFile(scanData.filepath);
210
 
211
+ const { error: uploadError } = await supabase.storage
212
+ .from('aura_media')
213
+ .upload(fileName, fileBuffer, { contentType: 'image/jpeg' });
214
 
215
+ if (uploadError) throw uploadError;
216
+
217
+ const { data: publicUrlData } = supabase.storage
218
+ .from('aura_media')
219
+ .getPublicUrl(fileName);
220
+
221
+ const { error: insertError } = await supabase
222
+ .from('aura_public_scans')
223
+ .insert({
224
+ user_id: scanData.user_id,
225
+ session_id: scanData.session_id,
226
+ image_url: publicUrlData.publicUrl,
227
+ score: scanData.score,
228
+ analysis_json: scanData.analysis,
229
+ context_type: scanData.context_type,
230
+ context_id: scanData.context_id
231
+ });
232
+
233
+ if (insertError) throw insertError;
234
 
235
  await fs.unlink(scanData.filepath);
236
  tempScans.delete(temp_scan_id);
237
  invalidateCaches('feed');
238
+
239
+ res.json({ success: true, message: 'Published.' });
240
+
241
  } catch (err) {
242
+ console.error('[Publish] Error:', err);
243
  res.status(500).json({ success: false, error: err.message });
244
  }
245
  });
246
 
247
+ // ─── GET /feed ─────────────────────────────────────────────────────────────────
248
+
249
  router.get('/feed', async (req, res) => {
250
  try {
251
+ const { cursor, limit = 20 } = req.query;
252
+ const parsedLimit = parseInt(limit, 10);
253
+
254
+ // Serve cache only for first page
255
  if (!cursor && cache.feed_page_1.expires > Date.now() && cache.feed_page_1.data) {
256
  return res.json({ success: true, data: cache.feed_page_1.data, cached: true });
257
  }
258
 
259
+ let query = supabase
260
+ .from('aura_public_scans')
261
+ .select(`
262
+ id,
263
+ image_url,
264
+ score,
265
+ context_type,
266
+ context_id,
267
+ created_at,
268
+ aura_profiles ( username, avatar, current_grade )
269
+ `)
270
+ .order('created_at', { ascending: false })
271
+ .limit(parsedLimit);
272
+
273
+ if (cursor) {
274
+ query = query.lt('created_at', cursor);
275
+ }
276
 
 
277
  const { data, error } = await query;
278
  if (error) throw error;
279
 
280
+ const nextCursor = data.length === parsedLimit
281
+ ? data[data.length - 1].created_at
282
+ : null;
283
+
284
  const responseData = { items: data, nextCursor };
285
 
286
+ if (!cursor) {
287
+ cache.feed_page_1 = { data: responseData, expires: Date.now() + CACHE_TTL };
288
+ }
289
+
290
  res.json({ success: true, data: responseData });
291
+
292
  } catch (err) {
293
+ console.error('[Feed] Error:', err);
294
  res.status(500).json({ success: false, error: err.message });
295
  }
296
  });
297
 
298
+ // ─── GET /leaderboard ──────────────────────────────────────────────────────────
299
+
300
  router.get('/leaderboard', async (req, res) => {
301
  try {
302
+ const { view = 'all_time' } = req.query;
303
+
304
+ // Validate view param to prevent cache poisoning
305
+ const validViews = ['all_time', 'session', 'grade'];
306
+ if (!validViews.includes(view)) {
307
+ return res.status(400).json({ success: false, error: 'Invalid view parameter.' });
308
+ }
309
+
310
  const cacheKey = `leaderboard_${view}`;
311
+
312
  if (cache[cacheKey]?.expires > Date.now() && cache[cacheKey]?.data) {
313
  return res.json({ success: true, data: cache[cacheKey].data, cached: true });
314
  }
315
 
316
  let data, error;
317
+
318
  if (view === 'all_time') {
319
+ // NOTE: Supabase does not support "col as alias" in .select() strings.
320
+ // Fetch raw column then map to { score } client-side.
321
+ ({ data, error } = await supabase
322
+ .from('aura_profiles')
323
+ .select('id, username, avatar, current_grade, cumulative_aura')
324
+ .order('cumulative_aura', { ascending: false })
325
+ .limit(50));
326
+
327
+ if (data) {
328
+ data = data.map(row => ({
329
+ id: row.id,
330
+ username: row.username,
331
+ avatar: row.avatar,
332
+ current_grade: row.current_grade,
333
+ score: row.cumulative_aura ?? 0
334
+ }));
335
+ }
336
+ }
337
  else if (view === 'session') {
338
+ const { data: activeSession } = await supabase
339
+ .from('aura_sessions')
340
+ .select('id')
341
+ .eq('is_active', true)
342
+ .maybeSingle();
343
+
344
+ if (!activeSession) {
345
+ return res.json({ success: true, data: [] });
346
+ }
347
+
348
+ ({ data, error } = await supabase
349
+ .from('aura_user_session_stats')
350
+ .select('session_aura, aura_profiles ( id, username, avatar, current_grade )')
351
+ .eq('session_id', activeSession.id)
352
+ .order('session_aura', { ascending: false })
353
+ .limit(50));
354
+
355
+ if (data) {
356
+ data = data.map(d => ({
357
+ id: d.aura_profiles?.id,
358
+ username: d.aura_profiles?.username,
359
+ avatar: d.aura_profiles?.avatar,
360
+ current_grade: d.aura_profiles?.current_grade,
361
+ score: d.session_aura ?? 0
362
+ }));
363
+ }
364
  }
365
  else if (view === 'grade') {
366
+ ({ data, error } = await supabase
367
+ .from('aura_profiles')
368
+ .select('id, username, avatar, current_grade, cumulative_aura')
369
+ .order('current_grade', { ascending: false })
370
+ .order('cumulative_aura', { ascending: false })
371
+ .limit(50));
372
+
373
+ if (data) {
374
+ data = data.map(row => ({
375
+ id: row.id,
376
+ username: row.username,
377
+ avatar: row.avatar,
378
+ current_grade: row.current_grade,
379
+ score: row.cumulative_aura ?? 0
380
+ }));
381
+ }
382
  }
383
 
384
  if (error) throw error;
385
+
386
  cache[cacheKey] = { data, expires: Date.now() + CACHE_TTL };
387
  res.json({ success: true, data });
388
+
389
  } catch (err) {
390
+ console.error('[Leaderboard] Error:', err);
391
  res.status(500).json({ success: false, error: err.message });
392
  }
393
  });
394
 
395
+ // ─── GET /challenges ───────────────────────────────────────────────────────────
396
+
397
  router.get('/challenges', async (req, res) => {
398
  try {
399
+ const { status = 'active' } = req.query;
400
+
401
+ // Fix: guard both expires AND data before serving cache
402
+ if (
403
+ status === 'active' &&
404
+ cache.challenges_active.expires > Date.now() &&
405
+ cache.challenges_active.data
406
+ ) {
407
+ return res.json({
408
+ success: true,
409
+ data: cache.challenges_active.data,
410
+ cached: true
411
+ });
412
  }
413
 
414
+ const { data: session } = await supabase
415
+ .from('aura_sessions')
416
+ .select('id')
417
+ .eq('is_active', true)
418
+ .maybeSingle();
419
+
420
+ let query = supabase
421
+ .from('aura_challenges')
422
+ .select('*')
423
+ .order('created_at', { ascending: false });
424
+
425
+ if (status === 'active' && session) {
426
+ query = query.eq('session_id', session.id);
427
+ } else if (status === 'archive' && session) {
428
+ query = query.neq('session_id', session.id);
429
+ }
430
 
431
  const { data, error } = await query;
432
  if (error) throw error;
433
 
434
+ if (status === 'active') {
435
+ cache.challenges_active = { data, expires: Date.now() + CACHE_TTL };
436
+ }
437
+
438
  res.json({ success: true, data });
439
+
440
  } catch (err) {
441
+ console.error('[Challenges] Error:', err);
442
  res.status(500).json({ success: false, error: err.message });
443
  }
444
  });
445
 
446
+ // ─── GET /threads/:context_id ──────────────────────────────────────────────────
447
+
448
  router.get('/threads/:context_id', async (req, res) => {
449
  try {
450
  const { context_id } = req.params;
451
  const { limit = 20, offset = 0 } = req.query;
452
+ const parsedLimit = parseInt(limit, 10);
453
+ const parsedOffset = parseInt(offset, 10);
454
+
455
+ const { data, error } = await supabase
456
+ .from('aura_thread_summaries')
457
+ .select(`
458
+ collection_aura,
459
+ submission_count,
460
+ top_image_url,
461
+ aura_profiles ( id, username, avatar, current_grade )
462
+ `)
463
+ .eq('context_id', context_id)
464
+ .order('collection_aura', { ascending: false })
465
+ .range(parsedOffset, parsedOffset + parsedLimit - 1);
466
 
467
  if (error) throw error;
468
+
469
+ const items = data.map(d => ({
470
+ ...d.aura_profiles,
471
+ collection_aura: d.collection_aura,
472
+ submission_count: d.submission_count,
473
+ top_image_url: d.top_image_url
474
+ }));
475
+
476
  res.json({ success: true, data: items });
477
+
478
  } catch (err) {
479
+ console.error('[Threads] Error:', err);
480
  res.status(500).json({ success: false, error: err.message });
481
  }
482
  });
483
 
484
+ // ─── POST /tag ─────────────────────────────────────────────────────────────────
485
+
486
  router.post('/tag', async (req, res) => {
487
  try {
488
  const { tagger_scan_id, tagged_scan_id } = req.body;
489
+
490
+ if (!tagger_scan_id || !tagged_scan_id) {
491
+ return res.status(400).json({ success: false, error: 'Missing scan IDs.' });
492
+ }
493
+
494
+ const { error } = await supabase
495
+ .from('aura_thread_tags')
496
+ .insert({ tagger_scan_id, tagged_scan_id });
497
+
498
+ // Ignore duplicate tag (unique constraint violation)
499
+ if (error && error.code !== '23505') throw error;
500
+
501
+ res.json({ success: true, message: 'Tagged successfully.' });
502
+
503
  } catch (err) {
504
+ console.error('[Tag] Error:', err);
505
  res.status(500).json({ success: false, error: err.message });
506
  }
507
  });
508
 
509
+ // ─── GET /profile/:username ────────────────────────────────────────────────────
510
+
511
  router.get('/profile/:username', async (req, res) => {
512
  try {
513
  const { username } = req.params;
514
+
515
+ const { data: profile, error: profErr } = await supabase
516
+ .from('aura_profiles')
517
+ .select('id, username, avatar, current_grade, cumulative_aura')
518
+ .eq('username', username)
519
+ .single();
520
+
521
+ if (profErr || !profile) {
522
+ return res.status(404).json({ success: false, error: 'Sorcerer not found.' });
523
+ }
524
+
525
+ const { data: scans, error: scansErr } = await supabase
526
+ .from('aura_public_scans')
527
+ .select('id, image_url, score, context_type, created_at')
528
+ .eq('user_id', profile.id)
529
+ .order('created_at', { ascending: false });
530
+
531
+ if (scansErr) throw scansErr;
532
 
533
  res.json({ success: true, data: { profile, scans } });
534
+
535
  } catch (err) {
536
+ console.error('[Profile] Error:', err);
537
  res.status(500).json({ success: false, error: err.message });
538
  }
539
  });