superbrain-server 1.0.59 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superbrain-server",
3
- "version": "1.0.59",
3
+ "version": "2.0.0",
4
4
  "description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "payload/"
23
23
  ],
24
24
  "publishConfig": {
25
- "registry": "https://registry.npmjs.org/",
26
25
  "access": "public"
27
26
  }
28
27
  }
28
+
package/payload/api.py CHANGED
@@ -726,24 +726,82 @@ async def check_cache(shortcode: str, token: str = Depends(verify_token)):
726
726
 
727
727
 
728
728
  @app.get("/recent")
729
- async def get_recent_analyses(limit: int = Query(default=10000, ge=1), token: str = Depends(verify_token)):
729
+ async def get_recent_analyses(
730
+ limit: int = Query(default=50, ge=1, le=1000),
731
+ offset: int = Query(default=0, ge=0),
732
+ token: str = Depends(verify_token),
733
+ ):
730
734
  """
731
- Get recent analyses from database
732
-
733
- - Returns most recently analyzed content
734
- - Default limit: 10000 (all lightweight posts)
735
+ Get recent analyses from database (lightweight — no analysis blobs)
736
+
737
+ - Returns most recently analyzed content with UI-essential fields only
738
+ - Supports pagination via offset
739
+ - Default limit: 50, max: 1000
735
740
  - Requires API authentication
736
741
  """
737
742
  try:
738
743
  db = get_db()
739
- results = db.get_recent(limit=limit)
740
-
744
+ results = db.get_recent_light(limit=limit, offset=offset)
745
+ total = db.get_total_count()
746
+
741
747
  return {
742
748
  "success": True,
743
749
  "count": len(results),
750
+ "total": total,
751
+ "offset": offset,
744
752
  "data": results
745
753
  }
746
-
754
+
755
+ except Exception as e:
756
+ raise HTTPException(status_code=500, detail=str(e))
757
+
758
+
759
+ @app.get("/sync")
760
+ async def sync_posts(
761
+ since: str = Query(..., description="ISO timestamp — return posts updated after this time"),
762
+ limit: int = Query(default=500, ge=1, le=1000),
763
+ token: str = Depends(verify_token),
764
+ ):
765
+ """
766
+ Delta sync endpoint — returns posts modified after the given timestamp.
767
+ Used by the mobile app to incrementally update its local SQLite database.
768
+ Only returns lightweight fields (no analysis blobs).
769
+ """
770
+ try:
771
+ db = get_db()
772
+ results = db.get_posts_since(since, limit=limit)
773
+
774
+ return {
775
+ "success": True,
776
+ "count": len(results),
777
+ "since": since,
778
+ "data": results
779
+ }
780
+
781
+ except Exception as e:
782
+ raise HTTPException(status_code=500, detail=str(e))
783
+
784
+
785
+ @app.get("/sync/deleted")
786
+ async def sync_deleted(
787
+ since: str = Query(..., description="ISO timestamp — return posts deleted after this time"),
788
+ token: str = Depends(verify_token),
789
+ ):
790
+ """
791
+ Returns shortcodes of posts that were soft-deleted after the given timestamp.
792
+ The mobile app uses this to remove posts from its local database.
793
+ """
794
+ try:
795
+ db = get_db()
796
+ results = db.get_deleted_since(since)
797
+
798
+ return {
799
+ "success": True,
800
+ "count": len(results),
801
+ "since": since,
802
+ "data": results
803
+ }
804
+
747
805
  except Exception as e:
748
806
  raise HTTPException(status_code=500, detail=str(e))
749
807
 
@@ -873,7 +931,7 @@ async def status():
873
931
  """
874
932
  return {
875
933
  "status": "online",
876
- "version": "1.02",
934
+ "version": "2.0.0",
877
935
  "message": "Server is running. Configure app with server URL and Access Token from token.txt."
878
936
  }
879
937
 
@@ -893,7 +951,7 @@ async def connect_info(request: Request):
893
951
  return {
894
952
  "url": base_url,
895
953
  "token": API_TOKEN,
896
- "version": "1.02",
954
+ "version": "2.0.0",
897
955
  "name": "SuperBrain"
898
956
  }
899
957
 
@@ -1,310 +1,310 @@
1
1
  {
2
2
  "groq_gpt_oss_20b": {
3
3
  "key": "groq_gpt_oss_20b",
4
- "avg_response_s": 2.1030592078849115,
5
- "success_count": 311,
6
- "fail_count": 9,
7
- "down_until": "2026-04-12T09:18:56.258250",
8
- "last_used": "2026-04-12T08:48:46.365989",
9
- "last_error": "Error code: 429 - {'error': {'message': 'Rate limit reached for model `openai/gpt-oss-20b` in organization `org_01jrmafjknfc8tmcz6f2zka3q2` service tier `on_demand` on tokens per day (TPD): Limit 2000",
4
+ "avg_response_s": 1.5213420135082059,
5
+ "success_count": 85,
6
+ "fail_count": 7,
7
+ "down_until": "2026-04-16T05:23:39.731939",
8
+ "last_used": "2026-04-10T06:49:02.416228",
9
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
10
10
  "base_priority": 0.5
11
11
  },
12
12
  "groq_llama33_70b": {
13
13
  "key": "groq_llama33_70b",
14
- "avg_response_s": 14.10594888995464,
15
- "success_count": 131,
14
+ "avg_response_s": 1.423364281654358,
15
+ "success_count": 2,
16
16
  "fail_count": 6,
17
- "down_until": null,
18
- "last_used": "2026-04-12T08:37:54.068541",
19
- "last_error": null,
17
+ "down_until": "2026-04-16T05:23:40.130355",
18
+ "last_used": "2026-02-24T07:53:30.927446",
19
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
20
20
  "base_priority": 1
21
21
  },
22
22
  "groq_llama4_scout": {
23
23
  "key": "groq_llama4_scout",
24
- "avg_response_s": 11.992687265344381,
25
- "success_count": 7,
26
- "fail_count": 5,
27
- "down_until": null,
28
- "last_used": "2026-04-12T08:08:47.469811",
29
- "last_error": null,
24
+ "avg_response_s": null,
25
+ "success_count": 0,
26
+ "fail_count": 6,
27
+ "down_until": "2026-04-16T05:23:40.533266",
28
+ "last_used": null,
29
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
30
30
  "base_priority": 1.5
31
31
  },
32
32
  "groq_llama31_8b": {
33
33
  "key": "groq_llama31_8b",
34
- "avg_response_s": 1.9519281985067138,
35
- "success_count": 347,
36
- "fail_count": 5,
37
- "down_until": null,
38
- "last_used": "2026-04-12T09:01:50.290420",
39
- "last_error": null,
34
+ "avg_response_s": null,
35
+ "success_count": 0,
36
+ "fail_count": 6,
37
+ "down_until": "2026-04-16T05:23:41.115883",
38
+ "last_used": null,
39
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
40
40
  "base_priority": 2
41
41
  },
42
42
  "groq_qwen3_32b": {
43
43
  "key": "groq_qwen3_32b",
44
- "avg_response_s": 3.935154914855957,
45
- "success_count": 1,
46
- "fail_count": 5,
47
- "down_until": null,
48
- "last_used": "2026-04-12T08:16:41.617443",
49
- "last_error": null,
44
+ "avg_response_s": null,
45
+ "success_count": 0,
46
+ "fail_count": 6,
47
+ "down_until": "2026-04-16T05:23:41.511590",
48
+ "last_used": null,
49
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
50
50
  "base_priority": 2.5
51
51
  },
52
52
  "groq_gpt_oss_120b": {
53
53
  "key": "groq_gpt_oss_120b",
54
54
  "avg_response_s": null,
55
55
  "success_count": 0,
56
- "fail_count": 5,
57
- "down_until": "2026-04-09T12:01:26.322619",
56
+ "fail_count": 6,
57
+ "down_until": "2026-04-16T05:23:41.909253",
58
58
  "last_used": null,
59
- "last_error": "No module named 'groq'",
59
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
60
60
  "base_priority": 3
61
61
  },
62
62
  "groq_gemma2_9b": {
63
63
  "key": "groq_gemma2_9b",
64
64
  "avg_response_s": null,
65
65
  "success_count": 0,
66
- "fail_count": 5,
67
- "down_until": "2026-04-09T12:01:26.324124",
66
+ "fail_count": 6,
67
+ "down_until": "2026-04-16T05:23:42.359488",
68
68
  "last_used": null,
69
- "last_error": "No module named 'groq'",
69
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
70
70
  "base_priority": 3.5
71
71
  },
72
72
  "groq_deepseek_r1_32b": {
73
73
  "key": "groq_deepseek_r1_32b",
74
74
  "avg_response_s": null,
75
75
  "success_count": 0,
76
- "fail_count": 5,
77
- "down_until": "2026-04-09T12:01:26.325130",
76
+ "fail_count": 6,
77
+ "down_until": "2026-04-16T05:23:42.761311",
78
78
  "last_used": null,
79
- "last_error": "No module named 'groq'",
79
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
80
80
  "base_priority": 3.8
81
81
  },
82
82
  "gemini_25_flash": {
83
83
  "key": "gemini_25_flash",
84
84
  "avg_response_s": null,
85
85
  "success_count": 0,
86
- "fail_count": 6,
87
- "down_until": "2026-04-09T12:01:26.327130",
86
+ "fail_count": 7,
87
+ "down_until": "2026-04-16T05:23:42.763290",
88
88
  "last_used": null,
89
- "last_error": "No module named 'google'",
89
+ "last_error": "No module named 'google.generativeai'",
90
90
  "base_priority": 4
91
91
  },
92
92
  "gemini_25_flash_lite": {
93
93
  "key": "gemini_25_flash_lite",
94
94
  "avg_response_s": null,
95
95
  "success_count": 0,
96
- "fail_count": 6,
97
- "down_until": "2026-04-09T12:01:26.329128",
96
+ "fail_count": 7,
97
+ "down_until": "2026-04-16T05:23:42.764289",
98
98
  "last_used": null,
99
- "last_error": "No module named 'google'",
99
+ "last_error": "No module named 'google.generativeai'",
100
100
  "base_priority": 4.5
101
101
  },
102
102
  "gemini_25_pro": {
103
103
  "key": "gemini_25_pro",
104
104
  "avg_response_s": null,
105
105
  "success_count": 0,
106
- "fail_count": 6,
107
- "down_until": "2026-04-09T12:01:26.330597",
106
+ "fail_count": 7,
107
+ "down_until": "2026-04-16T05:23:42.766724",
108
108
  "last_used": null,
109
- "last_error": "No module named 'google'",
109
+ "last_error": "No module named 'google.generativeai'",
110
110
  "base_priority": 5
111
111
  },
112
112
  "gemini_3_flash": {
113
113
  "key": "gemini_3_flash",
114
114
  "avg_response_s": null,
115
115
  "success_count": 0,
116
- "fail_count": 6,
117
- "down_until": "2026-04-09T12:01:26.331597",
116
+ "fail_count": 7,
117
+ "down_until": "2026-04-16T05:23:42.767406",
118
118
  "last_used": null,
119
- "last_error": "No module named 'google'",
119
+ "last_error": "No module named 'google.generativeai'",
120
120
  "base_priority": 5.5
121
121
  },
122
122
  "gemini_3_pro": {
123
123
  "key": "gemini_3_pro",
124
124
  "avg_response_s": null,
125
125
  "success_count": 0,
126
- "fail_count": 6,
127
- "down_until": "2026-04-09T12:01:26.333600",
126
+ "fail_count": 7,
127
+ "down_until": "2026-04-16T05:23:42.769918",
128
128
  "last_used": null,
129
- "last_error": "No module named 'google'",
129
+ "last_error": "No module named 'google.generativeai'",
130
130
  "base_priority": 6
131
131
  },
132
132
  "gemini_31_pro": {
133
133
  "key": "gemini_31_pro",
134
134
  "avg_response_s": null,
135
135
  "success_count": 0,
136
- "fail_count": 6,
137
- "down_until": "2026-04-09T12:01:26.335100",
136
+ "fail_count": 7,
137
+ "down_until": "2026-04-16T05:23:42.771915",
138
138
  "last_used": null,
139
- "last_error": "No module named 'google'",
139
+ "last_error": "No module named 'google.generativeai'",
140
140
  "base_priority": 6.5
141
141
  },
142
142
  "gemini_20_flash": {
143
143
  "key": "gemini_20_flash",
144
144
  "avg_response_s": null,
145
145
  "success_count": 0,
146
- "fail_count": 6,
147
- "down_until": "2026-04-09T12:01:26.336105",
146
+ "fail_count": 7,
147
+ "down_until": "2026-04-16T05:23:42.773099",
148
148
  "last_used": null,
149
- "last_error": "No module named 'google'",
149
+ "last_error": "No module named 'google.generativeai'",
150
150
  "base_priority": 7
151
151
  },
152
152
  "gemini_20_flash_lite": {
153
153
  "key": "gemini_20_flash_lite",
154
154
  "avg_response_s": null,
155
155
  "success_count": 0,
156
- "fail_count": 6,
157
- "down_until": "2026-04-09T12:01:26.338105",
156
+ "fail_count": 7,
157
+ "down_until": "2026-04-16T05:23:43.277254",
158
158
  "last_used": null,
159
- "last_error": "No module named 'google'",
159
+ "last_error": "No module named 'google.generativeai'",
160
160
  "base_priority": 7.5
161
161
  },
162
162
  "gemini_15_flash": {
163
163
  "key": "gemini_15_flash",
164
164
  "avg_response_s": null,
165
165
  "success_count": 0,
166
- "fail_count": 6,
167
- "down_until": "2026-04-09T12:01:26.338105",
166
+ "fail_count": 7,
167
+ "down_until": "2026-04-16T05:23:43.675312",
168
168
  "last_used": null,
169
- "last_error": "No module named 'google'",
169
+ "last_error": "No module named 'google.generativeai'",
170
170
  "base_priority": 8
171
171
  },
172
172
  "openrouter_llama33_70b": {
173
173
  "key": "openrouter_llama33_70b",
174
174
  "avg_response_s": null,
175
175
  "success_count": 0,
176
- "fail_count": 1,
177
- "down_until": "2026-04-09T17:58:36.014066",
176
+ "fail_count": 2,
177
+ "down_until": "2026-04-16T05:23:43.273239",
178
178
  "last_used": null,
179
- "last_error": "429 rate limit: {\"error\":{\"message\":\"Provider returned error\",\"code\":429,\"metadata\":{\"raw\":\"meta-llama/llama-3.3-70b-instruct:free is temporarily rate-limited upstream. Please retry shortly, or add yo",
179
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
180
180
  "base_priority": 7
181
181
  },
182
182
  "openrouter_deepseek_r1_0528": {
183
183
  "key": "openrouter_deepseek_r1_0528",
184
184
  "avg_response_s": null,
185
185
  "success_count": 0,
186
- "fail_count": 2,
187
- "down_until": "2026-04-09T17:42:02.945931",
186
+ "fail_count": 3,
187
+ "down_until": "2026-04-16T05:23:43.672310",
188
188
  "last_used": null,
189
- "last_error": "404 Client Error: Not Found for url: https://openrouter.ai/api/v1/chat/completions",
189
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
190
190
  "base_priority": 7.5
191
191
  },
192
192
  "openrouter_qwen3_235b": {
193
193
  "key": "openrouter_qwen3_235b",
194
194
  "avg_response_s": null,
195
195
  "success_count": 0,
196
- "fail_count": 2,
197
- "down_until": "2026-04-09T17:42:03.575625",
196
+ "fail_count": 3,
197
+ "down_until": "2026-04-16T05:23:44.167344",
198
198
  "last_used": null,
199
- "last_error": "404 Client Error: Not Found for url: https://openrouter.ai/api/v1/chat/completions",
199
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
200
200
  "base_priority": 8
201
201
  },
202
202
  "openrouter_hermes3_405b": {
203
203
  "key": "openrouter_hermes3_405b",
204
204
  "avg_response_s": null,
205
205
  "success_count": 0,
206
- "fail_count": 1,
207
- "down_until": "2026-04-09T17:58:37.929800",
206
+ "fail_count": 2,
207
+ "down_until": "2026-04-16T05:23:44.592514",
208
208
  "last_used": null,
209
- "last_error": "429 rate limit: {\"error\":{\"message\":\"Provider returned error\",\"code\":429,\"metadata\":{\"raw\":\"nousresearch/hermes-3-llama-3.1-405b:free is temporarily rate-limited upstream. Please retry shortly, or add",
209
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
210
210
  "base_priority": 8.5
211
211
  },
212
212
  "openrouter_gpt_oss_120b": {
213
213
  "key": "openrouter_gpt_oss_120b",
214
214
  "avg_response_s": 3.9147901058197023,
215
215
  "success_count": 2,
216
- "fail_count": 0,
217
- "down_until": null,
216
+ "fail_count": 1,
217
+ "down_until": "2026-04-16T05:23:45.344548",
218
218
  "last_used": "2026-04-09T17:37:11.262276",
219
- "last_error": null,
219
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
220
220
  "base_priority": 9
221
221
  },
222
222
  "openrouter_gpt_oss_20b": {
223
223
  "key": "openrouter_gpt_oss_20b",
224
224
  "avg_response_s": null,
225
225
  "success_count": 0,
226
- "fail_count": 0,
227
- "down_until": null,
226
+ "fail_count": 1,
227
+ "down_until": "2026-04-16T05:23:44.965672",
228
228
  "last_used": null,
229
- "last_error": null,
229
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
230
230
  "base_priority": 9.5
231
231
  },
232
232
  "openrouter_stepfun_flash": {
233
233
  "key": "openrouter_stepfun_flash",
234
234
  "avg_response_s": null,
235
235
  "success_count": 0,
236
- "fail_count": 0,
237
- "down_until": null,
236
+ "fail_count": 1,
237
+ "down_until": "2026-04-16T05:23:45.751094",
238
238
  "last_used": null,
239
- "last_error": null,
239
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
240
240
  "base_priority": 10
241
241
  },
242
242
  "openrouter_nemotron_30b": {
243
243
  "key": "openrouter_nemotron_30b",
244
244
  "avg_response_s": null,
245
245
  "success_count": 0,
246
- "fail_count": 0,
247
- "down_until": null,
246
+ "fail_count": 1,
247
+ "down_until": "2026-04-16T05:23:46.171133",
248
248
  "last_used": null,
249
- "last_error": null,
249
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
250
250
  "base_priority": 10.5
251
251
  },
252
252
  "openrouter_qwen3_next_80b": {
253
253
  "key": "openrouter_qwen3_next_80b",
254
254
  "avg_response_s": null,
255
255
  "success_count": 0,
256
- "fail_count": 0,
257
- "down_until": null,
256
+ "fail_count": 1,
257
+ "down_until": "2026-04-16T05:23:48.508702",
258
258
  "last_used": null,
259
- "last_error": null,
259
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
260
260
  "base_priority": 11
261
261
  },
262
262
  "openrouter_gemma3_27b": {
263
263
  "key": "openrouter_gemma3_27b",
264
264
  "avg_response_s": null,
265
265
  "success_count": 0,
266
- "fail_count": 0,
267
- "down_until": null,
266
+ "fail_count": 1,
267
+ "down_until": "2026-04-16T05:23:48.922522",
268
268
  "last_used": null,
269
- "last_error": null,
269
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
270
270
  "base_priority": 11.5
271
271
  },
272
272
  "openrouter_mistral_small31": {
273
273
  "key": "openrouter_mistral_small31",
274
274
  "avg_response_s": null,
275
275
  "success_count": 0,
276
- "fail_count": 0,
277
- "down_until": null,
276
+ "fail_count": 1,
277
+ "down_until": "2026-04-16T05:23:49.320898",
278
278
  "last_used": null,
279
- "last_error": null,
279
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
280
280
  "base_priority": 12
281
281
  },
282
282
  "openrouter_glm45_air": {
283
283
  "key": "openrouter_glm45_air",
284
284
  "avg_response_s": null,
285
285
  "success_count": 0,
286
- "fail_count": 0,
287
- "down_until": null,
286
+ "fail_count": 1,
287
+ "down_until": "2026-04-16T05:23:49.736031",
288
288
  "last_used": null,
289
- "last_error": null,
289
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
290
290
  "base_priority": 12.5
291
291
  },
292
292
  "openrouter_dolphin_venice": {
293
293
  "key": "openrouter_dolphin_venice",
294
294
  "avg_response_s": null,
295
295
  "success_count": 0,
296
- "fail_count": 0,
297
- "down_until": null,
296
+ "fail_count": 1,
297
+ "down_until": "2026-04-16T05:23:50.146983",
298
298
  "last_used": null,
299
- "last_error": null,
299
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
300
300
  "base_priority": 13
301
301
  },
302
302
  "local_qwen3": {
303
303
  "key": "local_qwen3",
304
304
  "avg_response_s": null,
305
305
  "success_count": 0,
306
- "fail_count": 6,
307
- "down_until": "2026-04-09T12:01:26.340615",
306
+ "fail_count": 7,
307
+ "down_until": "2026-04-16T05:23:50.149988",
308
308
  "last_used": null,
309
309
  "last_error": "No module named 'ollama'",
310
310
  "base_priority": 100
@@ -313,8 +313,8 @@
313
313
  "key": "gemini_25_flash_vision",
314
314
  "avg_response_s": 15.284299373626709,
315
315
  "success_count": 1,
316
- "fail_count": 32,
317
- "down_until": "2026-04-12T09:01:55.494866",
316
+ "fail_count": 7,
317
+ "down_until": "2026-04-16T05:23:31.459883",
318
318
  "last_used": "2026-02-24T06:25:56.490384",
319
319
  "last_error": "No module named 'google.generativeai'",
320
320
  "base_priority": 1
@@ -323,8 +323,8 @@
323
323
  "key": "gemini_25_flash_lite_vision",
324
324
  "avg_response_s": 6.709401964075168,
325
325
  "success_count": 14,
326
- "fail_count": 53,
327
- "down_until": "2026-04-12T09:03:49.079253",
326
+ "fail_count": 26,
327
+ "down_until": "2026-04-16T05:23:29.452819",
328
328
  "last_used": "2026-02-24T09:45:09.831171",
329
329
  "last_error": "No module named 'google.generativeai'",
330
330
  "base_priority": 1.5
@@ -333,8 +333,8 @@
333
333
  "key": "gemini_25_pro_vision",
334
334
  "avg_response_s": null,
335
335
  "success_count": 0,
336
- "fail_count": 56,
337
- "down_until": "2026-04-12T09:03:49.075655",
336
+ "fail_count": 29,
337
+ "down_until": "2026-04-16T05:23:29.450165",
338
338
  "last_used": null,
339
339
  "last_error": "No module named 'google.generativeai'",
340
340
  "base_priority": 2
@@ -343,8 +343,8 @@
343
343
  "key": "gemini_3_flash_vision",
344
344
  "avg_response_s": 22.28640604019165,
345
345
  "success_count": 1,
346
- "fail_count": 29,
347
- "down_until": "2026-04-12T09:01:59.727849",
346
+ "fail_count": 7,
347
+ "down_until": "2026-04-16T05:23:36.561818",
348
348
  "last_used": "2026-02-24T07:50:22.906246",
349
349
  "last_error": "No module named 'google.generativeai'",
350
350
  "base_priority": 2.5
@@ -353,8 +353,8 @@
353
353
  "key": "gemini_3_pro_vision",
354
354
  "avg_response_s": null,
355
355
  "success_count": 0,
356
- "fail_count": 55,
357
- "down_until": "2026-04-12T09:03:49.077691",
356
+ "fail_count": 28,
357
+ "down_until": "2026-04-16T05:23:29.451772",
358
358
  "last_used": null,
359
359
  "last_error": "No module named 'google.generativeai'",
360
360
  "base_priority": 3
@@ -363,8 +363,8 @@
363
363
  "key": "gemini_31_pro_vision",
364
364
  "avg_response_s": null,
365
365
  "success_count": 0,
366
- "fail_count": 54,
367
- "down_until": "2026-04-12T09:03:49.080628",
366
+ "fail_count": 27,
367
+ "down_until": "2026-04-16T05:23:29.454366",
368
368
  "last_used": null,
369
369
  "last_error": "No module named 'google.generativeai'",
370
370
  "base_priority": 3.5
@@ -373,8 +373,8 @@
373
373
  "key": "gemini_20_flash_vision",
374
374
  "avg_response_s": null,
375
375
  "success_count": 0,
376
- "fail_count": 53,
377
- "down_until": "2026-04-12T09:03:49.081592",
376
+ "fail_count": 26,
377
+ "down_until": "2026-04-16T05:23:29.455373",
378
378
  "last_used": null,
379
379
  "last_error": "No module named 'google.generativeai'",
380
380
  "base_priority": 4
@@ -383,8 +383,8 @@
383
383
  "key": "gemini_20_flash_lite_vision",
384
384
  "avg_response_s": null,
385
385
  "success_count": 0,
386
- "fail_count": 53,
387
- "down_until": "2026-04-12T09:03:49.082631",
386
+ "fail_count": 26,
387
+ "down_until": "2026-04-16T05:23:29.456376",
388
388
  "last_used": null,
389
389
  "last_error": "No module named 'google.generativeai'",
390
390
  "base_priority": 4.5
@@ -393,98 +393,98 @@
393
393
  "key": "gemini_15_flash_vision",
394
394
  "avg_response_s": null,
395
395
  "success_count": 0,
396
- "fail_count": 53,
397
- "down_until": "2026-04-12T09:03:49.084633",
396
+ "fail_count": 26,
397
+ "down_until": "2026-04-16T05:23:29.457379",
398
398
  "last_used": null,
399
399
  "last_error": "No module named 'google.generativeai'",
400
400
  "base_priority": 4.8
401
401
  },
402
402
  "groq_llama4_scout_vision": {
403
403
  "key": "groq_llama4_scout_vision",
404
- "avg_response_s": 2.0650597900565066,
405
- "success_count": 217,
406
- "fail_count": 10,
407
- "down_until": "2026-04-12T09:13:17.959244",
408
- "last_used": "2026-04-12T08:42:56.472897",
409
- "last_error": "Error code: 429 - {'error': {'message': 'Rate limit reached for model `meta-llama/llama-4-scout-17b-16e-instruct` in organization `org_01jrmafjknfc8tmcz6f2zka3q2` service tier `on_demand` on tokens pe",
404
+ "avg_response_s": 1.528297446591561,
405
+ "success_count": 56,
406
+ "fail_count": 6,
407
+ "down_until": "2026-04-16T05:23:31.458378",
408
+ "last_used": "2026-04-09T18:25:54.184988",
409
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
410
410
  "base_priority": 5
411
411
  },
412
412
  "groq_vision_11b": {
413
413
  "key": "groq_vision_11b",
414
414
  "avg_response_s": null,
415
415
  "success_count": 0,
416
- "fail_count": 31,
417
- "down_until": "2026-04-12T09:01:56.480971",
416
+ "fail_count": 6,
417
+ "down_until": "2026-04-16T05:23:31.997120",
418
418
  "last_used": null,
419
- "last_error": "Error code: 400 - {'error': {'message': 'The model `llama-3.2-11b-vision-preview` has been decommissioned and is no longer supported. Please refer to https://console.groq.com/docs/deprecations for a r",
419
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
420
420
  "base_priority": 5.5
421
421
  },
422
422
  "groq_vision_90b": {
423
423
  "key": "groq_vision_90b",
424
424
  "avg_response_s": null,
425
425
  "success_count": 0,
426
- "fail_count": 31,
427
- "down_until": "2026-04-12T09:01:57.040649",
426
+ "fail_count": 6,
427
+ "down_until": "2026-04-16T05:23:32.517886",
428
428
  "last_used": null,
429
- "last_error": "Error code: 400 - {'error': {'message': 'The model `llama-3.2-90b-vision-preview` has been decommissioned and is no longer supported. Please refer to https://console.groq.com/docs/deprecations for a r",
429
+ "last_error": "Error code: 401 - {'error': {'message': 'Invalid API Key', 'type': 'invalid_request_error', 'code': 'invalid_api_key'}}",
430
430
  "base_priority": 6
431
431
  },
432
432
  "openrouter_qwen3_vl_235b": {
433
433
  "key": "openrouter_qwen3_vl_235b",
434
434
  "avg_response_s": null,
435
435
  "success_count": 0,
436
- "fail_count": 23,
437
- "down_until": "2026-04-12T09:01:58.155413",
436
+ "fail_count": 1,
437
+ "down_until": "2026-04-16T05:23:33.654285",
438
438
  "last_used": null,
439
- "last_error": "404 Client Error: Not Found for url: https://openrouter.ai/api/v1/chat/completions",
439
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
440
440
  "base_priority": 7
441
441
  },
442
442
  "openrouter_qwen3_vl_30b": {
443
443
  "key": "openrouter_qwen3_vl_30b",
444
444
  "avg_response_s": null,
445
445
  "success_count": 0,
446
- "fail_count": 23,
447
- "down_until": "2026-04-12T09:01:59.065255",
446
+ "fail_count": 1,
447
+ "down_until": "2026-04-16T05:23:34.621368",
448
448
  "last_used": null,
449
- "last_error": "404 Client Error: Not Found for url: https://openrouter.ai/api/v1/chat/completions",
449
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
450
450
  "base_priority": 7.5
451
451
  },
452
452
  "openrouter_nvidia_vl": {
453
453
  "key": "openrouter_nvidia_vl",
454
- "avg_response_s": 18.168677418730027,
455
- "success_count": 64,
456
- "fail_count": 5,
457
- "down_until": "2026-04-12T09:17:10.589764",
458
- "last_used": "2026-04-12T07:46:28.394605",
459
- "last_error": "429 rate limit: {\"error\":{\"message\":\"Rate limit exceeded: free-models-per-day. Add 10 credits to unlock 1000 free model requests per day\",\"code\":429,\"metadata\":{\"headers\":{\"X-RateLimit-Limit\":\"50\",\"X-",
454
+ "avg_response_s": null,
455
+ "success_count": 0,
456
+ "fail_count": 1,
457
+ "down_until": "2026-04-16T05:23:35.292356",
458
+ "last_used": null,
459
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
460
460
  "base_priority": 8
461
461
  },
462
462
  "openrouter_gemma3_vision": {
463
463
  "key": "openrouter_gemma3_vision",
464
- "avg_response_s": 7.945096383094787,
465
- "success_count": 3,
466
- "fail_count": 5,
467
- "down_until": "2026-04-12T09:13:59.767520",
468
- "last_used": "2026-04-12T07:43:40.487825",
469
- "last_error": "429 rate limit: {\"error\":{\"message\":\"Rate limit exceeded: free-models-per-day. Add 10 credits to unlock 1000 free model requests per day\",\"code\":429,\"metadata\":{\"headers\":{\"X-RateLimit-Limit\":\"50\",\"X-",
464
+ "avg_response_s": null,
465
+ "success_count": 0,
466
+ "fail_count": 1,
467
+ "down_until": "2026-04-16T05:23:35.918404",
468
+ "last_used": null,
469
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
470
470
  "base_priority": 8.5
471
471
  },
472
472
  "openrouter_mistral_vision": {
473
473
  "key": "openrouter_mistral_vision",
474
474
  "avg_response_s": null,
475
475
  "success_count": 0,
476
- "fail_count": 23,
477
- "down_until": "2026-04-12T09:01:59.725111",
476
+ "fail_count": 1,
477
+ "down_until": "2026-04-16T05:23:36.558806",
478
478
  "last_used": null,
479
- "last_error": "404 Client Error: Not Found for url: https://openrouter.ai/api/v1/chat/completions",
479
+ "last_error": "401 Client Error: Unauthorized for url: https://openrouter.ai/api/v1/chat/completions",
480
480
  "base_priority": 9
481
481
  },
482
482
  "local_qwen3_vl": {
483
483
  "key": "local_qwen3_vl",
484
484
  "avg_response_s": null,
485
485
  "success_count": 0,
486
- "fail_count": 23,
487
- "down_until": "2026-04-12T09:04:18.457834",
486
+ "fail_count": 7,
487
+ "down_until": "2026-04-16T05:23:36.562857",
488
488
  "last_used": null,
489
489
  "last_error": "No module named 'ollama'",
490
490
  "base_priority": 100
@@ -599,6 +599,16 @@
599
599
  "last_error": null,
600
600
  "base_priority": 25
601
601
  },
602
+ "dyn_openrouter_elephant-alpha": {
603
+ "key": "dyn_openrouter_elephant-alpha",
604
+ "avg_response_s": null,
605
+ "success_count": 0,
606
+ "fail_count": 0,
607
+ "down_until": null,
608
+ "last_used": null,
609
+ "last_error": null,
610
+ "base_priority": 28
611
+ },
602
612
  "dyn_nvidia_nemotron-nano-9b-v2_free": {
603
613
  "key": "dyn_nvidia_nemotron-nano-9b-v2_free",
604
614
  "avg_response_s": null,
@@ -607,7 +617,7 @@
607
617
  "down_until": null,
608
618
  "last_used": null,
609
619
  "last_error": null,
610
- "base_priority": 28
620
+ "base_priority": 29
611
621
  },
612
622
  "dyn_minimax_minimax-m2_5_free": {
613
623
  "key": "dyn_minimax_minimax-m2_5_free",
@@ -617,7 +627,7 @@
617
627
  "down_until": null,
618
628
  "last_used": null,
619
629
  "last_error": null,
620
- "base_priority": 29
630
+ "base_priority": 30
621
631
  },
622
632
  "dyn_qwen_qwen3-coder_free": {
623
633
  "key": "dyn_qwen_qwen3-coder_free",
@@ -627,7 +637,7 @@
627
637
  "down_until": null,
628
638
  "last_used": null,
629
639
  "last_error": null,
630
- "base_priority": 30
640
+ "base_priority": 31
631
641
  },
632
642
  "dyn_liquid_lfm-2_5-1_2b-thinking_free": {
633
643
  "key": "dyn_liquid_lfm-2_5-1_2b-thinking_free",
@@ -637,7 +647,7 @@
637
647
  "down_until": null,
638
648
  "last_used": null,
639
649
  "last_error": null,
640
- "base_priority": 31
650
+ "base_priority": 32
641
651
  },
642
652
  "dyn_liquid_lfm-2_5-1_2b-instruct_free": {
643
653
  "key": "dyn_liquid_lfm-2_5-1_2b-instruct_free",
@@ -647,7 +657,7 @@
647
657
  "down_until": null,
648
658
  "last_used": null,
649
659
  "last_error": null,
650
- "base_priority": 32
660
+ "base_priority": 33
651
661
  },
652
662
  "dyn_arcee-ai_trinity-large-preview_free": {
653
663
  "key": "dyn_arcee-ai_trinity-large-preview_free",
@@ -657,7 +667,7 @@
657
667
  "down_until": null,
658
668
  "last_used": null,
659
669
  "last_error": null,
660
- "base_priority": 34
670
+ "base_priority": 35
661
671
  },
662
672
  "dyn_meta-llama_llama-3_2-3b-instruct_free": {
663
673
  "key": "dyn_meta-llama_llama-3_2-3b-instruct_free",
@@ -667,7 +677,7 @@
667
677
  "down_until": null,
668
678
  "last_used": null,
669
679
  "last_error": null,
670
- "base_priority": 38
680
+ "base_priority": 39
671
681
  },
672
682
  "dyn_google_gemma-3n-e2b-it_free": {
673
683
  "key": "dyn_google_gemma-3n-e2b-it_free",
@@ -677,7 +687,7 @@
677
687
  "down_until": null,
678
688
  "last_used": null,
679
689
  "last_error": null,
680
- "base_priority": 42
690
+ "base_priority": 43
681
691
  },
682
692
  "dyn_google_gemma-3-4b-it_free": {
683
693
  "key": "dyn_google_gemma-3-4b-it_free",
@@ -687,7 +697,7 @@
687
697
  "down_until": null,
688
698
  "last_used": null,
689
699
  "last_error": null,
690
- "base_priority": 43
700
+ "base_priority": 44
691
701
  },
692
702
  "dyn_v_google_gemma-3-4b-it_free": {
693
703
  "key": "dyn_v_google_gemma-3-4b-it_free",
@@ -697,7 +707,27 @@
697
707
  "down_until": null,
698
708
  "last_used": null,
699
709
  "last_error": null,
700
- "base_priority": 43
710
+ "base_priority": 44
711
+ },
712
+ "dyn_meta-llama_llama-guard-4-12b_free": {
713
+ "key": "dyn_meta-llama_llama-guard-4-12b_free",
714
+ "avg_response_s": null,
715
+ "success_count": 0,
716
+ "fail_count": 0,
717
+ "down_until": null,
718
+ "last_used": null,
719
+ "last_error": null,
720
+ "base_priority": 45
721
+ },
722
+ "dyn_v_meta-llama_llama-guard-4-12b_free": {
723
+ "key": "dyn_v_meta-llama_llama-guard-4-12b_free",
724
+ "avg_response_s": null,
725
+ "success_count": 0,
726
+ "fail_count": 0,
727
+ "down_until": null,
728
+ "last_used": null,
729
+ "last_error": null,
730
+ "base_priority": 45
701
731
  },
702
732
  "dyn_google_gemma-3n-e4b-it_free": {
703
733
  "key": "dyn_google_gemma-3n-e4b-it_free",
@@ -707,7 +737,7 @@
707
737
  "down_until": null,
708
738
  "last_used": null,
709
739
  "last_error": null,
710
- "base_priority": 44
740
+ "base_priority": 46
711
741
  },
712
742
  "dyn_google_gemma-3-12b-it_free": {
713
743
  "key": "dyn_google_gemma-3-12b-it_free",
@@ -717,7 +747,7 @@
717
747
  "down_until": null,
718
748
  "last_used": null,
719
749
  "last_error": null,
720
- "base_priority": 45
750
+ "base_priority": 47
721
751
  },
722
752
  "dyn_v_google_gemma-3-12b-it_free": {
723
753
  "key": "dyn_v_google_gemma-3-12b-it_free",
@@ -727,6 +757,6 @@
727
757
  "down_until": null,
728
758
  "last_used": null,
729
759
  "last_error": null,
730
- "base_priority": 45
760
+ "base_priority": 47
731
761
  }
732
762
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "cached_at": "2026-04-12T07:04:19.623841",
2
+ "cached_at": "2026-04-16T05:18:30.220245",
3
3
  "models": [
4
4
  {
5
5
  "id": "google/lyria-3-pro-preview",
@@ -427,6 +427,58 @@
427
427
  "details": "/api/v1/models/qwen/qwen3-next-80b-a3b-instruct-2509/endpoints"
428
428
  }
429
429
  },
430
+ {
431
+ "id": "openrouter/elephant-alpha",
432
+ "canonical_slug": "openrouter/elephant-alpha",
433
+ "hugging_face_id": null,
434
+ "name": "Elephant",
435
+ "created": 1776052598,
436
+ "description": "Elephant Alpha is a 100B-parameter text model focused on intelligence efficiency, delivering strong performance while minimizing token usage. It supports a 256K context window with up to 32K output tokens,...",
437
+ "context_length": 262144,
438
+ "architecture": {
439
+ "modality": "text->text",
440
+ "input_modalities": [
441
+ "text"
442
+ ],
443
+ "output_modalities": [
444
+ "text"
445
+ ],
446
+ "tokenizer": "Other",
447
+ "instruct_type": null
448
+ },
449
+ "pricing": {
450
+ "prompt": "0",
451
+ "completion": "0"
452
+ },
453
+ "top_provider": {
454
+ "context_length": 262144,
455
+ "max_completion_tokens": 32768,
456
+ "is_moderated": false
457
+ },
458
+ "per_request_limits": null,
459
+ "supported_parameters": [
460
+ "max_tokens",
461
+ "response_format",
462
+ "structured_outputs",
463
+ "temperature",
464
+ "tool_choice",
465
+ "tools",
466
+ "top_p"
467
+ ],
468
+ "default_parameters": {
469
+ "temperature": null,
470
+ "top_p": null,
471
+ "top_k": null,
472
+ "frequency_penalty": null,
473
+ "presence_penalty": null,
474
+ "repetition_penalty": null
475
+ },
476
+ "knowledge_cutoff": null,
477
+ "expiration_date": null,
478
+ "links": {
479
+ "details": "/api/v1/models/openrouter/elephant-alpha/endpoints"
480
+ }
481
+ },
430
482
  {
431
483
  "id": "nvidia/nemotron-nano-9b-v2:free",
432
484
  "canonical_slug": "nvidia/nemotron-nano-9b-v2",
@@ -504,7 +556,7 @@
504
556
  },
505
557
  "top_provider": {
506
558
  "context_length": 196608,
507
- "max_completion_tokens": 196608,
559
+ "max_completion_tokens": 8192,
508
560
  "is_moderated": true
509
561
  },
510
562
  "per_request_limits": null,
@@ -911,7 +963,7 @@
911
963
  },
912
964
  "top_provider": {
913
965
  "context_length": 131072,
914
- "max_completion_tokens": 131072,
966
+ "max_completion_tokens": 8192,
915
967
  "is_moderated": true
916
968
  },
917
969
  "per_request_limits": null,
@@ -1214,6 +1266,49 @@
1214
1266
  "details": "/api/v1/models/google/gemma-3-4b-it/endpoints"
1215
1267
  }
1216
1268
  },
1269
+ {
1270
+ "id": "meta-llama/llama-guard-4-12b:free",
1271
+ "canonical_slug": "meta-llama/llama-guard-4-12b",
1272
+ "hugging_face_id": "meta-llama/Llama-Guard-4-12B",
1273
+ "name": "Meta: Llama Guard 4 12B (free)",
1274
+ "created": 1745975193,
1275
+ "description": "Llama Guard 4 is a Llama 4 Scout-derived multimodal pretrained model, fine-tuned for content safety classification. Similar to previous versions, it can be used to classify content in both LLM...",
1276
+ "context_length": 163840,
1277
+ "architecture": {
1278
+ "modality": "text+image->text",
1279
+ "input_modalities": [
1280
+ "image",
1281
+ "text"
1282
+ ],
1283
+ "output_modalities": [
1284
+ "text"
1285
+ ],
1286
+ "tokenizer": "Other",
1287
+ "instruct_type": null
1288
+ },
1289
+ "pricing": {
1290
+ "prompt": "0",
1291
+ "completion": "0"
1292
+ },
1293
+ "top_provider": {
1294
+ "context_length": 163840,
1295
+ "max_completion_tokens": 65000,
1296
+ "is_moderated": false
1297
+ },
1298
+ "per_request_limits": null,
1299
+ "supported_parameters": [
1300
+ "max_tokens",
1301
+ "seed",
1302
+ "temperature",
1303
+ "top_p"
1304
+ ],
1305
+ "default_parameters": {},
1306
+ "knowledge_cutoff": "2024-08-31",
1307
+ "expiration_date": null,
1308
+ "links": {
1309
+ "details": "/api/v1/models/meta-llama/llama-guard-4-12b/endpoints"
1310
+ }
1311
+ },
1217
1312
  {
1218
1313
  "id": "google/gemma-3n-e4b-it:free",
1219
1314
  "canonical_slug": "google/gemma-3n-e4b-it",
@@ -160,10 +160,27 @@ class Database:
160
160
  except sqlite3.OperationalError:
161
161
  pass
162
162
 
163
+ # Deleted-log table — tracks when posts are soft-deleted so the
164
+ # mobile app can sync deletions via /sync/deleted.
165
+ self._conn.executescript("""
166
+ CREATE TABLE IF NOT EXISTS deleted_log (
167
+ shortcode TEXT PRIMARY KEY,
168
+ deleted_at TEXT
169
+ );
170
+ CREATE INDEX IF NOT EXISTS idx_deleted_log_at ON deleted_log (deleted_at);
171
+ """)
172
+ self._conn.commit()
173
+
163
174
  # ------------------------------------------------------------------
164
175
  # Helpers
165
176
  # ------------------------------------------------------------------
166
177
 
178
+ # Columns safe to send to the mobile app (excludes heavy analysis blobs)
179
+ LIGHT_COLUMNS = (
180
+ "shortcode, url, username, content_type, analyzed_at, updated_at, "
181
+ "post_date, likes, thumbnail, title, summary, tags, music, category, is_hidden"
182
+ )
183
+
167
184
  def _row_to_dict(self, row):
168
185
  if row is None:
169
186
  return None
@@ -246,30 +263,90 @@ class Database:
246
263
  traceback.print_exc()
247
264
  return False
248
265
 
249
- def get_recent(self, limit=10000):
250
- """Return the most recently analysed posts (excludes soft-deleted). Optimized to exclude heavy text blobs."""
266
+ def get_recent(self, limit=10):
267
+ """Return the most recently analysed posts (excludes soft-deleted)."""
251
268
  if not self.is_connected():
252
269
  return []
253
270
  try:
254
271
  cur = self._conn.cursor()
255
- cols = "shortcode, url, username, content_type, analyzed_at, updated_at, post_date, likes, thumbnail, title, summary, tags, music, category, is_hidden"
256
272
  cur.execute(
257
- f"SELECT {cols} FROM analyses WHERE (is_hidden IS NULL OR is_hidden = 0) ORDER BY analyzed_at DESC LIMIT ?", (limit,)
273
+ "SELECT * FROM analyses WHERE (is_hidden IS NULL OR is_hidden = 0) ORDER BY analyzed_at DESC LIMIT ?", (limit,)
258
274
  )
259
275
  return [self._row_to_dict(r) for r in cur.fetchall()]
260
276
  except Exception as e:
261
277
  print(f"[WARNING] Error retrieving recent: {e}")
262
278
  return []
263
279
 
280
+ def get_recent_light(self, limit=50, offset=0):
281
+ """Return recent posts with only UI-essential fields (no analysis blobs)."""
282
+ if not self.is_connected():
283
+ return []
284
+ try:
285
+ cur = self._conn.cursor()
286
+ cur.execute(
287
+ f"SELECT {self.LIGHT_COLUMNS} FROM analyses "
288
+ "WHERE (is_hidden IS NULL OR is_hidden = 0) "
289
+ "ORDER BY analyzed_at DESC LIMIT ? OFFSET ?",
290
+ (limit, offset)
291
+ )
292
+ return [self._row_to_dict(r) for r in cur.fetchall()]
293
+ except Exception as e:
294
+ print(f"[WARNING] Error retrieving recent (light): {e}")
295
+ return []
296
+
297
+ def get_posts_since(self, updated_after: str, limit=1000):
298
+ """Return posts updated after the given ISO timestamp (delta sync).
299
+ Includes soft-deleted posts so the app knows to hide them."""
300
+ if not self.is_connected():
301
+ return []
302
+ try:
303
+ cur = self._conn.cursor()
304
+ cur.execute(
305
+ f"SELECT {self.LIGHT_COLUMNS} FROM analyses "
306
+ "WHERE updated_at > ? "
307
+ "ORDER BY updated_at ASC LIMIT ?",
308
+ (updated_after, limit)
309
+ )
310
+ return [self._row_to_dict(r) for r in cur.fetchall()]
311
+ except Exception as e:
312
+ print(f"[WARNING] Error getting posts since {updated_after}: {e}")
313
+ return []
314
+
315
+ def get_deleted_since(self, since: str):
316
+ """Return shortcodes of posts deleted after the given ISO timestamp."""
317
+ if not self.is_connected():
318
+ return []
319
+ try:
320
+ cur = self._conn.cursor()
321
+ cur.execute(
322
+ "SELECT shortcode, deleted_at FROM deleted_log WHERE deleted_at > ? ORDER BY deleted_at ASC",
323
+ (since,)
324
+ )
325
+ return [{"shortcode": r["shortcode"], "deleted_at": r["deleted_at"]} for r in cur.fetchall()]
326
+ except Exception as e:
327
+ print(f"[WARNING] Error getting deleted since {since}: {e}")
328
+ return []
329
+
330
+ def get_total_count(self):
331
+ """Return total number of visible (non-hidden) posts."""
332
+ if not self.is_connected():
333
+ return 0
334
+ try:
335
+ cur = self._conn.cursor()
336
+ cur.execute("SELECT COUNT(*) FROM analyses WHERE (is_hidden IS NULL OR is_hidden = 0)")
337
+ return cur.fetchone()[0]
338
+ except Exception as e:
339
+ print(f"[WARNING] Error getting total count: {e}")
340
+ return 0
341
+
264
342
  def get_by_category(self, category, limit=20):
265
343
  """Return all analyses for a given category (excludes soft-deleted)."""
266
344
  if not self.is_connected():
267
345
  return []
268
346
  try:
269
347
  cur = self._conn.cursor()
270
- cols = "shortcode, url, username, content_type, analyzed_at, updated_at, post_date, likes, thumbnail, title, summary, tags, music, category, is_hidden"
271
348
  cur.execute(
272
- f"SELECT {cols} FROM analyses WHERE category = ? AND (is_hidden IS NULL OR is_hidden = 0) ORDER BY analyzed_at DESC LIMIT ?",
349
+ "SELECT * FROM analyses WHERE category = ? AND (is_hidden IS NULL OR is_hidden = 0) ORDER BY analyzed_at DESC LIMIT ?",
273
350
  (category, limit)
274
351
  )
275
352
  return [self._row_to_dict(r) for r in cur.fetchall()]
@@ -294,9 +371,8 @@ class Database:
294
371
  cur = self._conn.cursor()
295
372
  conditions = " OR ".join(["LOWER(tags) LIKE ?" for _ in tags])
296
373
  params = [f"%{t.lower()}%" for t in tags] + [limit]
297
- cols = "shortcode, url, username, content_type, analyzed_at, updated_at, post_date, likes, thumbnail, title, summary, tags, music, category, is_hidden"
298
374
  cur.execute(
299
- f"SELECT {cols} FROM analyses WHERE ({conditions}) AND (is_hidden IS NULL OR is_hidden = 0) ORDER BY analyzed_at DESC LIMIT ?",
375
+ f"SELECT * FROM analyses WHERE ({conditions}) AND (is_hidden IS NULL OR is_hidden = 0) ORDER BY analyzed_at DESC LIMIT ?",
300
376
  params
301
377
  )
302
378
  return [self._row_to_dict(r) for r in cur.fetchall()]
@@ -637,10 +713,17 @@ class Database:
637
713
  if not self.is_connected():
638
714
  return False
639
715
  try:
716
+ now = datetime.utcnow().isoformat()
640
717
  cur = self._conn.execute(
641
718
  "UPDATE analyses SET is_hidden = 1, updated_at = ? WHERE shortcode = ?",
642
- (datetime.utcnow().isoformat(), shortcode)
719
+ (now, shortcode)
643
720
  )
721
+ if cur.rowcount > 0:
722
+ # Record in deleted_log so mobile app can sync deletions
723
+ self._conn.execute(
724
+ "INSERT OR REPLACE INTO deleted_log (shortcode, deleted_at) VALUES (?, ?)",
725
+ (shortcode, now)
726
+ )
644
727
  self._conn.commit()
645
728
  return cur.rowcount > 0
646
729
  except Exception as e: