From 7751db2792379e883d51e0b220beeb71e3bd3b22 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Wed, 13 May 2026 08:22:37 +0300 Subject: [PATCH] cleanup + docs: remove marketing leftovers, full PROMPTS.md, CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deleted (live in gov-agreg repo): - src/lib/auth-queries.ts, supabase.ts, platform-queries.ts (marketing only) - src/pages/api/auth/, platform/, traducator.ts Added: - PROMPTS.md: §0 vreau.digital context + full historic (sections §0a-§0e from gov-agreg pre-split — 1367 lines preserved as "Istoric sesiuni") - CLAUDE.md: vreau.digital scope, 17 data sources table, deploy, sister project link Memory dir bootstrapped at ~/Code/claude-memory/projects/vreau-digital/memory/ with 23 .md files (all relevant context from gov-agreg). Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 99 ++++++++++--- PROMPTS.md | 54 +++++++ src/lib/auth-queries.ts | 121 ---------------- src/lib/platform-queries.ts | 209 --------------------------- src/lib/supabase.ts | 15 -- src/pages/api/auth/[...route].ts | 133 ----------------- src/pages/api/platform/[...route].ts | 155 -------------------- src/pages/api/traducator.ts | 116 --------------- 8 files changed, 131 insertions(+), 771 deletions(-) delete mode 100644 src/lib/auth-queries.ts delete mode 100644 src/lib/platform-queries.ts delete mode 100644 src/lib/supabase.ts delete mode 100644 src/pages/api/auth/[...route].ts delete mode 100644 src/pages/api/platform/[...route].ts delete mode 100644 src/pages/api/traducator.ts diff --git a/CLAUDE.md b/CLAUDE.md index 8525b91..5ca64d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,35 +1,90 @@ # vreau.digital — Platformă de transparență achiziții publice -## Context +## Ce e proiectul -Spin-off din `gov-agreg` (vreaudigital.ro). Acest repo conține platforma standalone de transparență achiziții publice România. +Platforma standalone de transparență pentru achizițiile publice din România. Agregator cross-source via CUI pe 17 surse publice (SEAP, ANAF, ANRE, ANCOM, ASF, AAAS, AEP, CNSC, Curtea Conturi, RegAS, bugetar, ONRC, AFIR, fonduri-ue, CNAS, APIA, GNM). -- **Domain**: https://vreau.digital -- **Hub marketing**: https://vreaudigital.ro (rămâne gov-agreg) -- **Backend partajat**: PostgreSQL satra, Photon, Martin (același cluster ca gov-agreg) +**Spin-off din `gov-agreg` (vreaudigital.ro)** — vreaudigital.ro rămâne hub-ul de marketing/discoverability. Vreau.digital e produsul real, live, cu DB conectat. ## Stack -- Astro 5 + React 19 + Tailwind 3 + Node @astrojs/node standalone -- PostgreSQL @ satra:5432 (schemas: seap, firms, anaf, anre, ancom, asf, aaas, aep, cnsc, curteacont, bugetar, regas, fonduri, cnas, apia, gnm, public_kpi) +- Astro 5 + React 19 + Tailwind 3.4 (NU v4) + Node @astrojs/node standalone +- PostgreSQL 17 @ satra:5432 (17+ schemas + public_kpi snapshots) - MapLibre 5 + Martin tile server @ 10.10.10.166:3010 -- Docker container @ satra:5096 → Traefik @ proxy 10.10.10.199 → vreau.digital (Cloudflare proxied) +- Photon geocoder daemon @ satra:2322 +- Docker container @ satra:5096 → Traefik @ proxy 10.10.10.199 → Cloudflare → vreau.digital -## Routes (toate la rădăcină, nu /achizitii/) +## Routes (toate la rădăcină, NU sub /achizitii/) -- `/` — landing page -- `/cauta` — search SEAP -- `/retete/[slug]` — investigative recipes (49+) -- `/investigation/[slug]` — narrative leads (15+) -- `/firma/[cui]`, `/autoritate/[cui]` — profile pages -- `/red-flags` — cross-source signals -- `/top-contracte`, `/top-firme`, `/fonduri-ue` — leaderboards -- `/api/*` — endpoints (og, cpv, profil, etc.) +| Route | Scop | +|---|---| +| `/` | landing — KPI naționale + secțiuni principale | +| `/cauta` | search SEAP cu MapLibre map, filtre cross-source, CPV autocomplete | +| `/retete` + `/retete/[slug]` | rețete investigative (49+) | +| `/investigation` + `/investigation/[slug]` | pagini narative cu cifre live (15+) | +| `/firma/[cui]`, `/autoritate/[cui]` | profile cu timeline 10 ani financiar + badges cross-source | +| `/red-flags` | landing cross-source signals (snapshot table — 207ms) | +| `/top-contracte`, `/top-firme`, `/fonduri-ue` | leaderboards | +| `/azi` | activitate zilnică SEAP | +| `/risc/*` | risk analysis (single-bidder, economii-suspect, termen-scurt, overprice) | +| `/explorer` | data browser | +| `/harta` | Civic OS map — bani publici per județ | +| `/api/og/firma/[cui].png` etc. | OG image generation | +| `/api/cpv/search` | CPV nomenclature autocomplete | +| `/api/profil/[side]/[cui]` | JSON profile API | +| `/api/ocds/*` | Open Contracting Data Standard endpoints | +| `/api/harta/*` | Civic OS map data | +| `/api/version` | deploy verification | + +## Data sources & freshness + +| Schema | Rows | Cadence | Live? | +|---|---:|---|---| +| seap.announcements | 781K+ | daily WSP scrapers (T+0) | ✓ | +| firms.entities (ONRC) | 4.005M | monthly fresh import | ✓ | +| firms.financials | 7.77M (10 ani, 2015-2024) | annual data.gov.ro | ✓ | +| anaf.datornici | 47K T1 2026 LIVE | **quarterly via 2captcha** | ✓ | +| anaf.lista_alba | 648K T1 2026 LIVE | **quarterly via 2captcha** | ✓ | +| anre.licente | 29K | weekly | ✓ | +| ancom.operatori | 518 | weekly | ✓ | +| asf.entitati | 849 | weekly | ✓ | +| aaas.firme | 11 (limit sursă) | weekly | ✓ | +| aep.donatii_pj/pf/rvc | 380K | monthly | ✓ | +| cnsc.decizii | 29K | weekly | ✓ | +| curteacont.rapoarte | 1133 | weekly | ✓ | +| regas.ajutoare | 78K | monthly | ✓ | +| bugetar.entitate | 18K | quarterly | ✓ | +| cnas.furnizori | 36K (50+ PDFs parse) | monthly | ✓ | +| apia.fermieri | 191 (snapshot test) | quarterly | ✓ | +| fonduri.beneficiar_anunt | 41K | weekly | ✓ | +| fonduri.afir_plati | 5.33M | monthly | ✓ | + +## Deploy + +- CI/CD: Gitea webhook → `satra:9867/hooks/vreau-digital-deploy` → `/opt/vreau-digital/deploy.sh` +- Build args: BUILD_SHA, BUILD_REF, BUILD_TIME → expose via /api/version +- Secrete: Infisical Machine Identity `/vreaudigital` (DATABASE_URL, TWOCAPTCHA_KEY) +- Verifică `/api/version` post-push ## Reguli operaționale -- Backend secrets în Infisical, path `/vreaudigital` (shared cu gov-agreg deocamdată — DATABASE_URL e identic) -- TWOCAPTCHA_KEY pentru ANAF datornici/lista_alba scrapers -- `npx astro check` + `npm run build` ÎNAINTE de commit -- Push triggers webhook → satra:9867 → /opt/vreau-digital/deploy.sh -- Verify deploy via `/api/version` +- `npx astro check` + `npm run build` PASS înainte de commit +- DACĂ scp pe satra → reset ÎNAINTE de push (deploy.sh git pull --ff-only fails) +- Pre-create `/var/log/vreau-digital-*.log` cu chown bulibasa pentru scrapere noi +- NU echo niciun secret. `$VAR` unexpanded. `--env-file` (NU `-e`) +- DB queries: `/tmp/govq.sh` reads SQL stdin → architools_db pe satra +- Memory: scrie DIRECT în `~/Code/claude-memory/projects/vreau-digital/memory/` + git commit + push (sync cron clobbers `~/.claude/.../memory/` every 60s) +- systemd OnCalendar: `Sun *-*-*` (cu SPACE după ziua), NU `Sun*-*-*` +- Comit-uri mici, focusate. NU amend. + +## Sister project: gov-agreg (vreaudigital.ro) + +Hub-ul de marketing/discoverability — listează produsele, idei, propuneri. Conține REDIRECTS de la /achizitii/* la vreau.digital. + +- **Repo:** `gitadmin/gov-agreg` +- **Domain:** `https://vreaudigital.ro` +- **Local path:** `/home/orchestrator/Code/gov-agreg/` + +NU atinge marketing pages (idei, despre, contribuie, produse) — alea sunt în gov-agreg. + +Vezi `PROMPTS.md` pentru context tehnic complet + istoric sesiuni. diff --git a/PROMPTS.md b/PROMPTS.md index 0e6a833..8c1a6d0 100644 --- a/PROMPTS.md +++ b/PROMPTS.md @@ -1,3 +1,57 @@ +# Prompts pentru sesiuni Claude — vreau.digital + +## 0. Context comun (citește PRIMUL) + +``` +Lucrăm la vreau.digital — platforma de transparență achiziții publice +România. Repo: git.beletage.ro/gitadmin/vreau-digital. + +SPLIT MAJOR 2026-05-13: spin-off din `gov-agreg` (vreaudigital.ro). Acest +repo conține platforma standalone — toate scrapers, recipes, investigation +pages, SQL migrations, Postgres queries cross-source. + +vreaudigital.ro = hub marketing/discoverability (gov-agreg repo) +vreau.digital = platforma achiziții (acest repo) + +Stack: +- Astro 5 + React 19 + Tailwind 3.4 (NU v4) + Node @astrojs/node standalone +- PostgreSQL 17 @ satra:5432 (schema `seap`, `firms`, `anaf`, ... 17 schemas) +- MapLibre 5 + Martin tile server @ 10.10.10.166:3010 (gis_uats_z0/z5/z8/z12) +- Photon geocoder daemon @ satra:2322 +- Container Docker @ satra:5096, Traefik @ proxy 10.10.10.199, Cloudflare proxied → vreau.digital + +Date prod (post 2026-05-13): +- 4.005M firme ONRC (100% geocoded) +- 7.77M financials (10 ani, 2015-2024) +- 781K+ anunțuri SEAP/TED +- 47K datornici ANAF T1 2026 + 648K lista albă (live via 2captcha) +- 17 systemd timers scrapere (daily/weekly/monthly/quarterly) +- 49 retete + 15 investigation pages + +Infra: +- CI/CD: Gitea webhook → satra:9867/hooks/vreau-digital-deploy → /opt/vreau-digital/deploy.sh +- Secrete: Infisical Machine Identity `/vreaudigital` (DATABASE_URL, TWOCAPTCHA_KEY, etc.) +- DB queries dev: `/tmp/govq.sh` reads SQL stdin → architools_db pe satra +- Verifică `/api/version` post-push + +Routes (toate la rădăcină, NU /achizitii/): +- /, /cauta, /retete/[slug], /investigation/[slug], /firma/[cui], /autoritate/[cui] +- /red-flags, /top-contracte, /top-firme, /fonduri-ue, /azi, /risc/*, /explorer +- /harta (Civic OS), /api/* (og, cpv, profil, ocds, harta, version) + +Reguli operaționale: +- `npx astro check` + `npm run build` PASS înainte de commit +- DACĂ scp pe satra → reset ÎNAINTE de push (deploy.sh git pull --ff-only fails) +- Pre-create /var/log/vreau-digital-*.log cu chown bulibasa pentru scrapere noi +- NU echo niciun secret. $VAR unexpanded, --env-file (NU -e) +- Memory persistat: scrie DIRECT în ~/Code/claude-memory/projects/vreau-digital/memory/ + git commit + push (sync cron clobbers ~/.claude/.../memory/ every 60s) +- systemd OnCalendar: `Sun *-*-*` (cu space după ziua), NU `Sun*-*-*` +``` + +--- + +## Istoric sesiuni (din gov-agreg, păstrat aici după split 2026-05-13) + # Prompts pentru sesiuni Claude — vreaudigital.ro Fișier de copy-paste pentru sesiunile viitoare Claude Code, ca să nu reexpli o diff --git a/src/lib/auth-queries.ts b/src/lib/auth-queries.ts deleted file mode 100644 index ce318b2..0000000 --- a/src/lib/auth-queries.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { query } from './db.js'; - -export interface UserProfile { - id: string; - email: string; - name: string | null; - city: string | null; - role: 'user' | 'developer' | 'admin'; - bio: string | null; - github_url: string | null; - website_url: string | null; - created_at: string; -} - -export async function ensureAuthTables() { - await query(` - CREATE SCHEMA IF NOT EXISTS platform; - - CREATE TABLE IF NOT EXISTS platform.user_profiles ( - id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, - name TEXT, - city TEXT, - role TEXT DEFAULT 'user' CHECK (role IN ('user', 'developer', 'admin')), - bio TEXT, - github_url TEXT, - website_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS platform.product_submissions ( - id SERIAL PRIMARY KEY, - user_id TEXT REFERENCES platform.user_profiles(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - category TEXT NOT NULL, - demo_url TEXT, - source_url TEXT, - screenshot_url TEXT, - status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')), - solves_ideas INTEGER[] DEFAULT '{}', - created_at TIMESTAMPTZ DEFAULT NOW() - ); - - -- Add solves_product column to ideas if not exists - DO $$ BEGIN - ALTER TABLE platform.ideas ADD COLUMN IF NOT EXISTS solved_by_product TEXT; - ALTER TABLE platform.ideas ADD COLUMN IF NOT EXISTS vote_threshold_reached BOOLEAN DEFAULT FALSE; - EXCEPTION WHEN OTHERS THEN NULL; - END $$; - `); -} - -export async function getOrCreateProfile(id: string, email: string): Promise { - const existing = await query( - 'SELECT * FROM platform.user_profiles WHERE id = $1', [id] - ); - if (existing.rows[0]) return existing.rows[0]; - - const result = await query( - `INSERT INTO platform.user_profiles (id, email) VALUES ($1, $2) RETURNING *`, - [id, email] - ); - return result.rows[0]; -} - -export async function updateProfile(id: string, data: { - name?: string; - city?: string; - bio?: string; - github_url?: string; - website_url?: string; -}): Promise { - const result = await query( - `UPDATE platform.user_profiles - SET name = COALESCE($2, name), - city = COALESCE($3, city), - bio = COALESCE($4, bio), - github_url = COALESCE($5, github_url), - website_url = COALESCE($6, website_url) - WHERE id = $1 RETURNING *`, - [id, data.name, data.city, data.bio, data.github_url, data.website_url] - ); - return result.rows[0]; -} - -export async function submitProduct(data: { - user_id: string; - title: string; - description: string; - category: string; - demo_url?: string; - source_url?: string; - screenshot_url?: string; - solves_ideas?: number[]; -}) { - const result = await query( - `INSERT INTO platform.product_submissions - (user_id, title, description, category, demo_url, source_url, screenshot_url, solves_ideas) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, - [data.user_id, data.title, data.description, data.category, - data.demo_url || null, data.source_url || null, data.screenshot_url || null, - data.solves_ideas || []] - ); - return result.rows[0].id; -} - -export async function getUserSubmissions(userId: string) { - const result = await query( - 'SELECT * FROM platform.product_submissions WHERE user_id = $1 ORDER BY created_at DESC', - [userId] - ); - return result.rows; -} - -export async function getProfile(id: string): Promise { - const result = await query( - 'SELECT * FROM platform.user_profiles WHERE id = $1', [id] - ); - return result.rows[0] || null; -} diff --git a/src/lib/platform-queries.ts b/src/lib/platform-queries.ts deleted file mode 100644 index 6f3786a..0000000 --- a/src/lib/platform-queries.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { query } from './db.js'; - -export interface Idea { - id: number; - title: string; - problem: string; - solution: string | null; - category: string; - author_name: string | null; - author_city: string | null; - status: string; - votes: number; - comment_count?: number; - solved_by_product?: string | null; - created_at: string; -} - -export interface ProductSubmission { - id: number; - title: string; - description: string; - category: string; - demo_url: string; - source_url: string | null; - screenshot_url: string | null; - solves_ideas: string | null; - author_name: string; - author_email: string; - author_location: string | null; - author_github: string | null; - status: string; - created_at: string; -} - -export async function getIdeas(sort: string = 'votes', category?: string): Promise { - const orderBy = sort === 'new' ? 'i.created_at DESC' : 'i.votes DESC, i.created_at DESC'; - const categoryFilter = category && category !== 'toate' ? 'AND i.category = $1' : ''; - const params = category && category !== 'toate' ? [category] : []; - - const result = await query(` - SELECT i.*, COALESCE(c.cnt, 0)::int AS comment_count - FROM platform.ideas i - LEFT JOIN (SELECT idea_id, COUNT(*) AS cnt FROM platform.comments GROUP BY idea_id) c - ON c.idea_id = i.id - WHERE 1=1 ${categoryFilter} - ORDER BY ${orderBy} - `, params); - return result.rows; -} - -export async function getIdea(id: number): Promise { - const result = await query('SELECT * FROM platform.ideas WHERE id = $1', [id]); - return result.rows[0] || null; -} - -export async function getIdeaComments(ideaId: number) { - const result = await query( - 'SELECT * FROM platform.comments WHERE idea_id = $1 ORDER BY created_at ASC', - [ideaId] - ); - return result.rows; -} - -export async function createIdea(data: { - title: string; - problem: string; - solution?: string; - category?: string; - author_name?: string; - author_email?: string; - author_city?: string; -}): Promise { - const result = await query( - `INSERT INTO platform.ideas (title, problem, solution, category, author_name, author_email, author_city) - VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, - [data.title, data.problem, data.solution || null, data.category || 'general', - data.author_name || null, data.author_email || null, data.author_city || null] - ); - return result.rows[0].id; -} - -export async function voteIdea(ideaId: number, fingerprint: string): Promise<{ votes: number; alreadyVoted: boolean }> { - // Try insert vote - try { - await query( - 'INSERT INTO platform.votes (idea_id, fingerprint) VALUES ($1, $2)', - [ideaId, fingerprint] - ); - // Increment counter - const result = await query( - 'UPDATE platform.ideas SET votes = votes + 1 WHERE id = $1 RETURNING votes', - [ideaId] - ); - return { votes: result.rows[0].votes, alreadyVoted: false }; - } catch { - // Already voted (unique constraint) - const result = await query('SELECT votes FROM platform.ideas WHERE id = $1', [ideaId]); - return { votes: result.rows[0]?.votes || 0, alreadyVoted: true }; - } -} - -export async function addComment(ideaId: number, authorName: string, content: string) { - await query( - 'INSERT INTO platform.comments (idea_id, author_name, content) VALUES ($1, $2, $3)', - [ideaId, authorName || 'Anonim', content] - ); -} - -export async function getStats() { - const result = await query(` - SELECT - (SELECT COUNT(*) FROM platform.ideas)::int AS total_ideas, - (SELECT COUNT(*) FROM platform.ideas WHERE status = 'in lucru')::int AS in_progress, - (SELECT COUNT(*) FROM platform.ideas WHERE status = 'live')::int AS live, - (SELECT SUM(votes) FROM platform.ideas)::int AS total_votes - `); - return result.rows[0]; -} - -export async function getIdeasByVoteThreshold(threshold: number): Promise { - const result = await query(` - SELECT i.*, COALESCE(c.cnt, 0)::int AS comment_count - FROM platform.ideas i - LEFT JOIN (SELECT idea_id, COUNT(*) AS cnt FROM platform.comments GROUP BY idea_id) c - ON c.idea_id = i.id - WHERE i.votes >= $1 - ORDER BY i.votes DESC - `, [threshold]); - return result.rows; -} - -export async function getTopIdeas(limit: number): Promise { - const result = await query(` - SELECT i.*, COALESCE(c.cnt, 0)::int AS comment_count - FROM platform.ideas i - LEFT JOIN (SELECT idea_id, COUNT(*) AS cnt FROM platform.comments GROUP BY idea_id) c - ON c.idea_id = i.id - ORDER BY i.votes DESC, i.created_at DESC - LIMIT $1 - `, [limit]); - return result.rows; -} - -export async function getIdeasForProduct(productSlug: string): Promise { - const result = await query(` - SELECT i.*, COALESCE(c.cnt, 0)::int AS comment_count - FROM platform.ideas i - LEFT JOIN (SELECT idea_id, COUNT(*) AS cnt FROM platform.comments GROUP BY idea_id) c - ON c.idea_id = i.id - WHERE i.solved_by_product = $1 - ORDER BY i.votes DESC - `, [productSlug]); - return result.rows; -} - -export async function submitProductIdea(data: { - title: string; - description: string; - category: string; - demo_url: string; - source_url?: string | null; - screenshot_url?: string | null; - solves_ideas?: string | null; - author_name: string; - author_email: string; - author_location?: string | null; - author_github?: string | null; -}): Promise { - // Create table if not exists (idempotent) - await query(` - CREATE TABLE IF NOT EXISTS platform.product_submissions ( - id SERIAL PRIMARY KEY, - title VARCHAR(200) NOT NULL, - description TEXT NOT NULL, - category VARCHAR(50) NOT NULL, - demo_url TEXT NOT NULL, - source_url TEXT, - screenshot_url TEXT, - solves_ideas TEXT, - author_name VARCHAR(200) NOT NULL, - author_email VARCHAR(200) NOT NULL, - author_location VARCHAR(200), - author_github VARCHAR(200), - status VARCHAR(50) DEFAULT 'pending', - created_at TIMESTAMPTZ DEFAULT NOW() - ) - `); - - const result = await query( - `INSERT INTO platform.product_submissions - (title, description, category, demo_url, source_url, screenshot_url, solves_ideas, author_name, author_email, author_location, author_github) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) - RETURNING id`, - [ - data.title, - data.description, - data.category, - data.demo_url, - data.source_url || null, - data.screenshot_url || null, - data.solves_ideas || null, - data.author_name, - data.author_email, - data.author_location || null, - data.author_github || null, - ] - ); - return result.rows[0].id; -} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts deleted file mode 100644 index b68ac17..0000000 --- a/src/lib/supabase.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL || process.env.PUBLIC_SUPABASE_URL || ''; -const supabaseAnonKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY || process.env.PUBLIC_SUPABASE_ANON_KEY || ''; - -export const supabaseEnabled = !!(supabaseUrl && supabaseAnonKey); - -export const supabase = supabaseEnabled - ? createClient(supabaseUrl, supabaseAnonKey) - : null; - -export function getSupabaseClient() { - if (!supabase) throw new Error('Supabase not configured'); - return supabase; -} diff --git a/src/pages/api/auth/[...route].ts b/src/pages/api/auth/[...route].ts deleted file mode 100644 index c9b9bf7..0000000 --- a/src/pages/api/auth/[...route].ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { APIRoute } from 'astro'; -import { supabase, supabaseEnabled } from '../../../lib/supabase.js'; -import { getOrCreateProfile, updateProfile, getUserSubmissions, getProfile } from '../../../lib/auth-queries.js'; - -export const prerender = false; - -const json = (data: any, status = 200) => - new Response(JSON.stringify(data), { - status, - headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' }, - }); - -async function getAuthUser(request: Request) { - if (!supabaseEnabled || !supabase) { - return { user: null, error: 'Supabase not configured' }; - } - - const token = request.headers.get('Authorization')?.replace('Bearer ', ''); - if (!token) { - return { user: null, error: 'No token provided' }; - } - - const { data: { user }, error } = await supabase!.auth.getUser(token); - if (error || !user) { - return { user: null, error: error?.message || 'Invalid token' }; - } - - return { user, error: null }; -} - -export const GET: APIRoute = async ({ params, request }) => { - const route = params.route || ''; - - try { - if (route === 'profile') { - const { user, error } = await getAuthUser(request); - if (!user) return json({ error: error || 'Unauthorized' }, 401); - - const profile = await getOrCreateProfile(user.id, user.email!); - const submissions = await getUserSubmissions(user.id); - - return json({ profile, submissions }); - } - - return json({ error: 'Not found' }, 404); - } catch (err: any) { - console.error('[auth GET]', err); - return json({ error: 'Server error' }, 500); - } -}; - -export const POST: APIRoute = async ({ params, request }) => { - const route = params.route || ''; - - try { - if (route === 'profile') { - const { user, error } = await getAuthUser(request); - if (!user) return json({ error: error || 'Unauthorized' }, 401); - - // Ensure profile exists - await getOrCreateProfile(user.id, user.email!); - - // If body has update data, apply it - let body: any = {}; - try { - body = await request.json(); - } catch { - // Empty body is fine — just ensure profile exists - } - - const hasUpdates = body.name || body.city || body.bio || body.github_url || body.website_url; - - if (hasUpdates) { - // Validate URLs if provided - if (body.github_url && !isValidUrl(body.github_url)) { - return json({ error: 'URL GitHub invalid' }, 400); - } - if (body.website_url && !isValidUrl(body.website_url)) { - return json({ error: 'URL website invalid' }, 400); - } - - // Sanitize text fields - const updateData = { - name: sanitize(body.name, 100), - city: sanitize(body.city, 100), - bio: sanitize(body.bio, 500), - github_url: body.github_url?.trim().slice(0, 200) || undefined, - website_url: body.website_url?.trim().slice(0, 200) || undefined, - }; - - const profile = await updateProfile(user.id, updateData); - return json({ profile }); - } - - const profile = await getProfile(user.id); - return json({ profile }); - } - - if (route === 'logout') { - // Server-side logout — clear any server session if needed - return new Response(JSON.stringify({ message: 'Logged out' }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache', - 'Set-Cookie': [ - 'sb-token=; path=/; max-age=0; SameSite=Lax', - 'sb-refresh-token=; path=/; max-age=0; SameSite=Lax', - ].join(', '), - }, - }); - } - - return json({ error: 'Not found' }, 404); - } catch (err: any) { - console.error('[auth POST]', err); - return json({ error: 'Server error' }, 500); - } -}; - -function isValidUrl(str: string): boolean { - try { - const url = new URL(str); - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch { - return false; - } -} - -function sanitize(value: string | undefined, maxLen: number): string | undefined { - if (!value) return undefined; - return value.trim().slice(0, maxLen).replace(/<[^>]*>/g, '') || undefined; -} diff --git a/src/pages/api/platform/[...route].ts b/src/pages/api/platform/[...route].ts deleted file mode 100644 index 8508edd..0000000 --- a/src/pages/api/platform/[...route].ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { APIRoute } from 'astro'; -import { - getIdeas, - getIdea, - getIdeaComments, - createIdea, - voteIdea, - addComment, - getStats, - getTopIdeas, - getIdeasByVoteThreshold, - submitProductIdea, -} from '../../../lib/platform-queries.js'; -import { createHash } from 'crypto'; - -export const prerender = false; - -const json = (data: any, status = 200) => - new Response(JSON.stringify(data), { - status, - headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' }, - }); - -function fingerprint(request: Request): string { - const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; - const ua = request.headers.get('user-agent') || ''; - return createHash('sha256').update(`${ip}:${ua}`).digest('hex').slice(0, 16); -} - -export const GET: APIRoute = async ({ params, url }) => { - const route = params.route || ''; - const parts = route.split('/'); - - try { - if (route === 'ideas') { - const ideas = await getIdeas('votes'); - return json(ideas); - } - - if (route === 'ideas/new') { - const ideas = await getIdeas('new'); - return json(ideas); - } - - if (route === 'ideas/top') { - const limit = parseInt(url.searchParams.get('limit') || '10'); - const ideas = await getTopIdeas(limit); - return json(ideas); - } - - if (route === 'ideas/threshold') { - const threshold = parseInt(url.searchParams.get('min') || '25'); - const ideas = await getIdeasByVoteThreshold(threshold); - return json(ideas); - } - - if (parts[0] === 'idea' && parts[1]) { - const idea = await getIdea(parseInt(parts[1])); - if (!idea) return json({ error: 'Not found' }, 404); - const comments = await getIdeaComments(idea.id); - return json({ idea, comments }); - } - - if (route === 'stats') { - return json(await getStats()); - } - - return json({ error: 'Not found' }, 404); - } catch (err: any) { - console.error('[platform]', err); - return json({ error: 'Server error' }, 500); - } -}; - -export const POST: APIRoute = async ({ params, request }) => { - const route = params.route || ''; - - try { - if (route === 'ideas') { - const body = await request.json(); - if (!body.title?.trim() || !body.problem?.trim()) { - return json({ error: 'Titlul si problema sunt obligatorii.' }, 400); - } - if (body.title.length > 200 || body.problem.length > 2000) { - return json({ error: 'Text prea lung.' }, 400); - } - const id = await createIdea({ - title: body.title.trim(), - problem: body.problem.trim(), - solution: body.solution?.trim(), - category: body.category, - author_name: body.author_name?.trim(), - author_email: body.author_email?.trim(), - author_city: body.author_city?.trim(), - }); - return json({ id, message: 'Ideea ta a fost adaugata!' }, 201); - } - - if (route === 'vote') { - const body = await request.json(); - if (!body.idea_id) return json({ error: 'Missing idea_id' }, 400); - const fp = fingerprint(request); - const result = await voteIdea(body.idea_id, fp); - return json(result); - } - - if (route === 'comment') { - const body = await request.json(); - if (!body.idea_id || !body.content?.trim()) { - return json({ error: 'Continutul e obligatoriu.' }, 400); - } - if (body.content.length > 1000) { - return json({ error: 'Comentariu prea lung.' }, 400); - } - await addComment(body.idea_id, body.author_name, body.content.trim()); - return json({ message: 'Comentariu adaugat.' }, 201); - } - - if (route === 'submit-product') { - const body = await request.json(); - if (!body.title?.trim() || !body.description?.trim() || !body.category?.trim() || !body.demo_url?.trim()) { - return json({ error: 'Titlul, descrierea, categoria si URL-ul demo sunt obligatorii.' }, 400); - } - if (!body.author_name?.trim() || !body.author_email?.trim()) { - return json({ error: 'Numele si email-ul autorului sunt obligatorii.' }, 400); - } - if (body.title.length > 200) { - return json({ error: 'Titlul e prea lung (max 200 caractere).' }, 400); - } - if (body.description.length > 2000) { - return json({ error: 'Descrierea e prea lunga (max 2000 caractere).' }, 400); - } - - const id = await submitProductIdea({ - title: body.title.trim(), - description: body.description.trim(), - category: body.category.trim(), - demo_url: body.demo_url.trim(), - source_url: body.source_url?.trim() || null, - screenshot_url: body.screenshot_url?.trim() || null, - solves_ideas: body.solves_ideas?.trim() || null, - author_name: body.author_name.trim(), - author_email: body.author_email.trim(), - author_location: body.author_location?.trim() || null, - author_github: body.author_github?.trim() || null, - }); - return json({ id, message: 'Produsul a fost trimis pentru revizuire.' }, 201); - } - - return json({ error: 'Not found' }, 404); - } catch (err: any) { - console.error('[platform POST]', err); - return json({ error: 'Server error' }, 500); - } -}; diff --git a/src/pages/api/traducator.ts b/src/pages/api/traducator.ts deleted file mode 100644 index 43aa390..0000000 --- a/src/pages/api/traducator.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { APIRoute } from 'astro'; - -export const prerender = false; - -const SYSTEM_PROMPT = `Ești un traducător din limbaj birocratic/juridic românesc în limbaj simplu, pe înțelesul oricui. - -Reguli: -1. Primești un text oficial (lege, hotărâre, adresă, contract, etc.) și îl explici în limba română normală. -2. Folosește propoziții scurte și clare. Evită jargonul. -3. Dacă textul cere o acțiune din partea cititorului, spune clar CE trebuie să facă. -4. Evidențiază termenii cheie și explică-i pe scurt între paranteze dacă e necesar. -5. Păstrează structura logică: dacă textul original are mai multe puncte, menține-le. -6. NU inventa informații. Dacă ceva e ambiguu în text, spune-o. -7. Răspunde DOAR cu traducerea/explicația. Fără introduceri de genul "Iată traducerea:" sau "Textul original spune:". -8. Tonul: prietenos, direct, ca și cum ai explica unui prieten. -9. Dacă textul conține referințe la legi (ex: "art. 7 alin. (2) din Legea nr. 50/1991"), menționează-le pe scurt dar explică ce înseamnă practic.`; - -// Ollama (local Gemma 3 12B) — primary -// Claude API — fallback if Ollama unavailable -const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; -const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'gemma3:12b'; - -async function translateWithOllama(text: string): Promise { - const res = await fetch(`${OLLAMA_URL}/api/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: OLLAMA_MODEL, - messages: [ - { role: 'system', content: SYSTEM_PROMPT }, - { role: 'user', content: `Traduce următorul text oficial în limbaj simplu:\n\n${text}` }, - ], - stream: false, - }), - }); - - if (!res.ok) throw new Error(`Ollama error: ${res.status}`); - const data = await res.json(); - return data.message?.content || ''; -} - -async function translateWithClaude(text: string, apiKey: string): Promise { - const { default: Anthropic } = await import('@anthropic-ai/sdk'); - const client = new Anthropic({ apiKey }); - - const message = await client.messages.create({ - model: 'claude-haiku-4-5-20251001', - max_tokens: 2048, - system: SYSTEM_PROMPT, - messages: [ - { role: 'user', content: `Traduce următorul text oficial în limbaj simplu:\n\n${text}` }, - ], - }); - - const content = message.content[0]; - return content.type === 'text' ? content.text : ''; -} - -export const POST: APIRoute = async ({ request }) => { - let body: { text?: string }; - try { - body = await request.json(); - } catch { - return new Response( - JSON.stringify({ error: 'Request invalid.' }), - { status: 400, headers: { 'Content-Type': 'application/json' } } - ); - } - - const text = body.text?.trim(); - if (!text || text.length < 10) { - return new Response( - JSON.stringify({ error: 'Textul trebuie să aibă cel puțin 10 caractere.' }), - { status: 400, headers: { 'Content-Type': 'application/json' } } - ); - } - - if (text.length > 5000) { - return new Response( - JSON.stringify({ error: 'Textul nu poate depăși 5000 de caractere.' }), - { status: 400, headers: { 'Content-Type': 'application/json' } } - ); - } - - // Try Ollama first (local, free), fall back to Claude API - try { - const translation = await translateWithOllama(text); - if (translation) { - return new Response( - JSON.stringify({ translation, provider: 'local' }), - { status: 200, headers: { 'Content-Type': 'application/json' } } - ); - } - } catch (err) { - console.warn('Ollama unavailable, trying Claude fallback:', (err as Error).message); - } - - // Fallback: Claude API - const apiKey = import.meta.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY; - if (apiKey) { - try { - const translation = await translateWithClaude(text, apiKey); - return new Response( - JSON.stringify({ translation, provider: 'cloud' }), - { status: 200, headers: { 'Content-Type': 'application/json' } } - ); - } catch (err) { - console.error('Claude API error:', err); - } - } - - return new Response( - JSON.stringify({ error: 'Serviciul de traducere nu este disponibil momentan. Încearcă din nou.' }), - { status: 503, headers: { 'Content-Type': 'application/json' } } - ); -};