techprotrade commited on
Commit
d7a0ca6
·
verified ·
1 Parent(s): eac0d05

Add eestilaenud2026 directory

Browse files
eestilaenud2026/exported-assets (1)/script.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ repo = Path('output/aimoneyflow_next/repo_blueprint')
3
+ (repo / 'drizzle').mkdir(exist_ok=True)
4
+ (repo / 'app' / 'api' / 'sync').mkdir(parents=True, exist_ok=True)
5
+ (repo / 'app' / 'api' / 'conflicts').mkdir(parents=True, exist_ok=True)
6
+ (repo / 'components').mkdir(exist_ok=True)
7
+ (repo / 'lib').mkdir(exist_ok=True)
8
+
9
+ files = {
10
+ 'components/SyncBadge.jsx': """'use client'
11
+ export default function SyncBadge({ state='synced' }) {
12
+ const map = { synced:'Synced', pending:'Pending', conflict:'Conflict', offline:'Offline' }
13
+ return <span className={'sync-badge '+state}>{map[state] || state}</span>
14
+ }
15
+ """,
16
+ 'components/ConflictResolver.jsx': """'use client'
17
+ export default function ConflictResolver({ local, remote, onChoose }) {
18
+ return <div className='card ai-panel'>
19
+ <h4>Conflict resolver</h4>
20
+ <div className='conf-grid'>
21
+ <pre>{JSON.stringify(local, null, 2)}</pre>
22
+ <pre>{JSON.stringify(remote, null, 2)}</pre>
23
+ </div>
24
+ <div className='row-actions'>
25
+ <button className='pill' onClick={() => onChoose('local')}>Keep local</button>
26
+ <button className='pill' onClick={() => onChoose('remote')}>Keep remote</button>
27
+ <button className='pill' onClick={() => onChoose('merge')}>Merge</button>
28
+ </div>
29
+ </div>
30
+ }
31
+ """,
32
+ 'lib/sync.js': """export function hashRecord(r){ return btoa(unescape(encodeURIComponent(JSON.stringify(r)))).slice(0,16) }
33
+ export function mergeRecord(local, remote){ return {
34
+ ...remote,
35
+ ...local,
36
+ chip: Array.from(new Set([...(remote?.chip||[]), ...(local?.chip||[])])),
37
+ updated_at: new Date().toISOString(),
38
+ sync_status: 'synced'
39
+ } }
40
+ export function queueDraft(draft){ const key='aimf_sync_queue'; const arr=JSON.parse(localStorage.getItem(key)||'[]'); arr.unshift({ id:Date.now(), draft, ts:Date.now(), hash:hashRecord(draft) }); localStorage.setItem(key, JSON.stringify(arr.slice(0,100))); return arr.length }
41
+ """,
42
+ 'lib/prompts.js': """export function buildValidationPrompt(provider){
43
+ return `Valideeri järgmine kirje. Tagasta ainult JSON: {score:number, risks:string[], notes:string[]}. Kirje: ${JSON.stringify(provider)}`
44
+ }
45
+ export function buildAnalysisPrompt(provider){
46
+ return `Anna lühike JSON: {summary:string, risks:string[], tags:string[]}. Kirje: ${JSON.stringify(provider)}`
47
+ }
48
+ """,
49
+ 'app/api/sync/route.js': """import { NextResponse } from 'next/server'
50
+ import { mergeRecord } from '../../../../lib/sync'
51
+ export async function POST(req){ const { local, remote } = await req.json(); return NextResponse.json({ merged: mergeRecord(local, remote) }) }
52
+ """,
53
+ 'app/api/conflicts/route.js': """import { NextResponse } from 'next/server'
54
+ export async function POST(req){ const body = await req.json(); return NextResponse.json({ conflict: true, ...body }) }
55
+ """,
56
+ 'lib/ai.js': """import { buildValidationPrompt, buildAnalysisPrompt } from './prompts'
57
+ export async function runOllama(provider, mode='validate'){
58
+ const base = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
59
+ const model = process.env.OLLAMA_MODEL || 'llama3.1'
60
+ const prompt = mode === 'validate' ? buildValidationPrompt(provider) : buildAnalysisPrompt(provider)
61
+ const res = await fetch(`${base}/api/generate`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ model, prompt, stream:false }) })
62
+ if (!res.ok) throw new Error(`Ollama ${res.status}`)
63
+ return res.json()
64
+ }
65
+ """,
66
+ 'drizzle/0001_init.sql': """create table if not exists providers (
67
+ id bigserial primary key,
68
+ name text not null,
69
+ type text not null,
70
+ category text not null,
71
+ max_amount text,
72
+ interest text,
73
+ note text,
74
+ chip text[] default '{}',
75
+ source_url text,
76
+ sync_status text default 'synced',
77
+ source_hash text,
78
+ updated_at timestamptz default now(),
79
+ created_at timestamptz default now()
80
+ );
81
+ create index if not exists idx_providers_category on providers(category);
82
+ create index if not exists idx_providers_type on providers(type);
83
+ create index if not exists idx_providers_sync on providers(sync_status);
84
+ """,
85
+ 'app/page.jsx': (repo / 'app' / 'page.jsx').read_text(encoding='utf-8').replace("""import { validateProvider } from '../lib/validation'
86
+ """, """import { validateProvider } from '../lib/validation'
87
+ import { queueDraft } from '../lib/sync'
88
+ """).replace(""" useEffect(() => { const t = setTimeout(()=>{ localStorage.setItem('aimf_draft', JSON.stringify(draft)); setLastSaved(new Date().toLocaleTimeString('et-EE')); const arr = JSON.parse(localStorage.getItem('aimf_drafts')||'[]'); arr.unshift({ts:Date.now(), draft}); localStorage.setItem('aimf_drafts', JSON.stringify(arr.slice(0,20))); setDraftCount(Math.min(arr.length+1,20)) }, 500); return ()=>clearTimeout(t) }, [draft])
89
+ """, """ useEffect(() => { const t = setTimeout(()=>{ localStorage.setItem('aimf_draft', JSON.stringify(draft)); setLastSaved(new Date().toLocaleTimeString('et-EE')); const arr = JSON.parse(localStorage.getItem('aimf_drafts')||'[]'); arr.unshift({ts:Date.now(), draft}); localStorage.setItem('aimf_drafts', JSON.stringify(arr.slice(0,20))); setDraftCount(Math.min(arr.length+1,20)); queueDraft(draft) }, 500); return ()=>clearTimeout(t) }, [draft])
90
+ """)
91
+ }
92
+ for rel, content in files.items():
93
+ path = repo / rel
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+ path.write_text(content, encoding='utf-8')
96
+ print('done')
eestilaenud2026/exported-assets (1)/script_1.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ repo = Path('output/aimoneyflow_next/repo_blueprint')
3
+ (repo / 'app').mkdir(parents=True, exist_ok=True)
4
+ (repo / 'components').mkdir(parents=True, exist_ok=True)
5
+ (repo / 'lib').mkdir(parents=True, exist_ok=True)
6
+ (repo / 'drizzle').mkdir(parents=True, exist_ok=True)
7
+ (repo / 'app' / 'api' / 'sync').mkdir(parents=True, exist_ok=True)
8
+ (repo / 'app' / 'api' / 'conflicts').mkdir(parents=True, exist_ok=True)
9
+
10
+ (repo / 'components' / 'SyncBadge.jsx').write_text("""'use client'
11
+ export default function SyncBadge({ state='synced' }) {
12
+ const map = { synced:'Synced', pending:'Pending', conflict:'Conflict', offline:'Offline' }
13
+ return <span className={'sync-badge '+state}>{map[state] || state}</span>
14
+ }
15
+ """, encoding='utf-8')
16
+ (repo / 'components' / 'ConflictResolver.jsx').write_text("""'use client'
17
+ export default function ConflictResolver({ local, remote, onChoose }) {
18
+ return <div className='card ai-panel'>
19
+ <h4>Conflict resolver</h4>
20
+ <div className='conf-grid'>
21
+ <pre>{JSON.stringify(local, null, 2)}</pre>
22
+ <pre>{JSON.stringify(remote, null, 2)}</pre>
23
+ </div>
24
+ <div className='row-actions'>
25
+ <button className='pill' onClick={() => onChoose('local')}>Keep local</button>
26
+ <button className='pill' onClick={() => onChoose('remote')}>Keep remote</button>
27
+ <button className='pill' onClick={() => onChoose('merge')}>Merge</button>
28
+ </div>
29
+ </div>
30
+ }
31
+ """, encoding='utf-8')
32
+ (repo / 'lib' / 'sync.js').write_text("""export function hashRecord(r){ return btoa(unescape(encodeURIComponent(JSON.stringify(r)))).slice(0,16) }
33
+ export function mergeRecord(local, remote){ return { ...remote, ...local, chip: Array.from(new Set([...(remote?.chip||[]), ...(local?.chip||[])])), updated_at: new Date().toISOString(), sync_status: 'synced' } }
34
+ export function queueDraft(draft){ const key='aimf_sync_queue'; const arr=JSON.parse(localStorage.getItem(key)||'[]'); arr.unshift({ id:Date.now(), draft, ts:Date.now(), hash:hashRecord(draft) }); localStorage.setItem(key, JSON.stringify(arr.slice(0,100))); return arr.length }
35
+ """, encoding='utf-8')
36
+ (repo / 'lib' / 'prompts.js').write_text("""export function buildValidationPrompt(provider){ return `Valideeri järgmine kirje. Tagasta ainult JSON: {score:number, risks:string[], notes:string[]}. Kirje: ${JSON.stringify(provider)}` }
37
+ export function buildAnalysisPrompt(provider){ return `Anna lühike JSON: {summary:string, risks:string[], tags:string[]}. Kirje: ${JSON.stringify(provider)}` }
38
+ """, encoding='utf-8')
39
+ (repo / 'lib' / 'ai.js').write_text("""import { buildValidationPrompt, buildAnalysisPrompt } from './prompts'
40
+ export async function runOllama(provider, mode='validate'){
41
+ const base = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
42
+ const model = process.env.OLLAMA_MODEL || 'llama3.1'
43
+ const prompt = mode === 'validate' ? buildValidationPrompt(provider) : buildAnalysisPrompt(provider)
44
+ const res = await fetch(`${base}/api/generate`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ model, prompt, stream:false }) })
45
+ if (!res.ok) throw new Error(`Ollama ${res.status}`)
46
+ return res.json()
47
+ }
48
+ """, encoding='utf-8')
49
+ (repo / 'app' / 'api' / 'sync' / 'route.js').write_text("""import { NextResponse } from 'next/server'
50
+ import { mergeRecord } from '../../../../lib/sync'
51
+ export async function POST(req){ const { local, remote } = await req.json(); return NextResponse.json({ merged: mergeRecord(local, remote) }) }
52
+ """, encoding='utf-8')
53
+ (repo / 'app' / 'api' / 'conflicts' / 'route.js').write_text("""import { NextResponse } from 'next/server'
54
+ export async function POST(req){ const body = await req.json(); return NextResponse.json({ conflict: true, ...body }) }
55
+ """, encoding='utf-8')
56
+ (repo / 'drizzle' / '0001_init.sql').write_text("""create table if not exists providers (
57
+ id bigserial primary key,
58
+ name text not null,
59
+ type text not null,
60
+ category text not null,
61
+ max_amount text,
62
+ interest text,
63
+ note text,
64
+ chip text[] default '{}',
65
+ source_url text,
66
+ sync_status text default 'synced',
67
+ source_hash text,
68
+ updated_at timestamptz default now(),
69
+ created_at timestamptz default now()
70
+ );
71
+ create index if not exists idx_providers_category on providers(category);
72
+ create index if not exists idx_providers_type on providers(type);
73
+ create index if not exists idx_providers_sync on providers(sync_status);
74
+ """, encoding='utf-8')
75
+ print('repo extras done')
eestilaenud2026/exported-assets/.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ DATABASE_URL=postgresql://USER:PASSWORD@HOST/DB?sslmode=require
2
+ OLLAMA_BASE_URL=http://localhost:11434
3
+ OLLAMA_MODEL=llama3.1
4
+ NEXT_PUBLIC_APP_NAME=aiMoneyFlow
5
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
6
+ NETLIFY=1
eestilaenud2026/exported-assets/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # aiMoneyFlow Admin
2
+
3
+ ## Stack
4
+ - Next.js App Router
5
+ - Neon Postgres
6
+ - Ollama local AI
7
+ - Netlify deploy
8
+
9
+ ## Setup
10
+ 1. Copy `.env.example` to `.env.local`
11
+ 2. Set `DATABASE_URL`
12
+ 3. Install deps: `npm i`
13
+ 4. Create DB table using `schema.sql`
14
+ 5. Run `npm run dev`
15
+
16
+ ## Deploy to Netlify
17
+ - Push to GitHub
18
+ - Connect repo in Netlify
19
+ - Build command: `npm run build`
20
+ - Publish: `.next`
21
+ - Add env vars in Netlify dashboard
eestilaenud2026/exported-assets/package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "aimoneyflow-admin",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "next dev",
6
+ "build": "next build",
7
+ "start": "next start",
8
+ "lint": "next lint",
9
+ "seed": "node scripts/seed.mjs",
10
+ "validate:csv": "node scripts/validate-csv.mjs"
11
+ },
12
+ "dependencies": {
13
+ "@neondatabase/serverless": "latest",
14
+ "next": "latest",
15
+ "react": "latest",
16
+ "react-dom": "latest"
17
+ }
18
+ }
eestilaenud2026/exported-assets/providers.json ADDED
@@ -0,0 +1,810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "Swedbank",
4
+ "type": "loan",
5
+ "category": "tagatiseta",
6
+ "max": "15k+",
7
+ "interest": "alates 7.9%",
8
+ "note": "väikelaen ja ärilahendused",
9
+ "chip": [
10
+ "tagatiseta"
11
+ ]
12
+ },
13
+ {
14
+ "name": "SEB",
15
+ "type": "loan",
16
+ "category": "tagatiseta",
17
+ "max": "15k+",
18
+ "interest": "alates 7.9%",
19
+ "note": "väikelaen / liising ettevõttele",
20
+ "chip": [
21
+ "tagatiseta"
22
+ ]
23
+ },
24
+ {
25
+ "name": "LHV",
26
+ "type": "loan",
27
+ "category": "tagatiseta",
28
+ "max": "kohanduv",
29
+ "interest": "pakkumispõhine",
30
+ "note": "ärilaen, liising, finantseerimine",
31
+ "chip": [
32
+ "tagatiseta"
33
+ ]
34
+ },
35
+ {
36
+ "name": "Coop Pank",
37
+ "type": "loan",
38
+ "category": "tagatiseta",
39
+ "max": "25k",
40
+ "interest": "alates 6.9%",
41
+ "note": "alustava ettevõtja väikelaen",
42
+ "chip": [
43
+ "tagatiseta"
44
+ ]
45
+ },
46
+ {
47
+ "name": "Bigbank",
48
+ "type": "loan",
49
+ "category": "tagatiseta",
50
+ "max": "25k+",
51
+ "interest": "alates 7.9%",
52
+ "note": "ärilaen ja väikelaen",
53
+ "chip": [
54
+ "tagatiseta"
55
+ ]
56
+ },
57
+ {
58
+ "name": "Inbank",
59
+ "type": "loan",
60
+ "category": "tagatiseta",
61
+ "max": "10k",
62
+ "interest": "alates 8.9%",
63
+ "note": "väikelaen ja järelmaks",
64
+ "chip": [
65
+ "tagatiseta"
66
+ ]
67
+ },
68
+ {
69
+ "name": "TF Bank",
70
+ "type": "loan",
71
+ "category": "tagatiseta",
72
+ "max": "20k",
73
+ "interest": "alates 7.9%",
74
+ "note": "väikelaen / krediidikonto",
75
+ "chip": [
76
+ "tagatiseta"
77
+ ]
78
+ },
79
+ {
80
+ "name": "Ferratum",
81
+ "type": "loan",
82
+ "category": "tagatiseta",
83
+ "max": "5k",
84
+ "interest": "alates 26.85%",
85
+ "note": "krediidikonto / kiirfinantseerimine",
86
+ "chip": [
87
+ "tagatiseta",
88
+ "uus"
89
+ ]
90
+ },
91
+ {
92
+ "name": "Credit24",
93
+ "type": "loan",
94
+ "category": "tagatiseta",
95
+ "max": "10k",
96
+ "interest": "alates 27.24%",
97
+ "note": "krediidikonto / väikelaen",
98
+ "chip": [
99
+ "tagatiseta"
100
+ ]
101
+ },
102
+ {
103
+ "name": "Monefit",
104
+ "type": "loan",
105
+ "category": "tagatiseta",
106
+ "max": "10k",
107
+ "interest": "alates 53.77%",
108
+ "note": "krediidiliin",
109
+ "chip": [
110
+ "tagatiseta"
111
+ ]
112
+ },
113
+ {
114
+ "name": "Bondora",
115
+ "type": "loan",
116
+ "category": "tagatiseta",
117
+ "max": "10k",
118
+ "interest": "alates 31.69%",
119
+ "note": "tagatiseta laen, sobib äri katteks",
120
+ "chip": [
121
+ "tagatiseta"
122
+ ]
123
+ },
124
+ {
125
+ "name": "Raha24",
126
+ "type": "loan",
127
+ "category": "tagatiseta",
128
+ "max": "10k",
129
+ "interest": "pakkumispõhine",
130
+ "note": "kiirlaen / väikelaen",
131
+ "chip": [
132
+ "tagatiseta"
133
+ ]
134
+ },
135
+ {
136
+ "name": "HyBa",
137
+ "type": "loan",
138
+ "category": "tagatiseta",
139
+ "max": "500k",
140
+ "interest": "personaalne",
141
+ "note": "ärifinantseerimine",
142
+ "chip": [
143
+ "tagatiseta",
144
+ "uus"
145
+ ]
146
+ },
147
+ {
148
+ "name": "Laen.ee",
149
+ "type": "loan",
150
+ "category": "tagatiseta",
151
+ "max": "50k",
152
+ "interest": "individuaalne",
153
+ "note": "tagatiseta ärilaen",
154
+ "chip": [
155
+ "tagatiseta"
156
+ ]
157
+ },
158
+ {
159
+ "name": "Ärilaen.ee",
160
+ "type": "loan",
161
+ "category": "tagatiseta",
162
+ "max": "15k",
163
+ "interest": "individuaalne",
164
+ "note": "tagatiseta ärilaen",
165
+ "chip": [
166
+ "tagatiseta"
167
+ ]
168
+ },
169
+ {
170
+ "name": "Hoovi",
171
+ "type": "loan",
172
+ "category": "tagatiseta",
173
+ "max": "20k",
174
+ "interest": "individuaalne",
175
+ "note": "käibevahendite rahastus",
176
+ "chip": [
177
+ "tagatiseta"
178
+ ]
179
+ },
180
+ {
181
+ "name": "Creditea",
182
+ "type": "loan",
183
+ "category": "tagatiseta",
184
+ "max": "10k",
185
+ "interest": "pakkumispõhine",
186
+ "note": "krediidikonto / väikelaen",
187
+ "chip": [
188
+ "tagatiseta"
189
+ ]
190
+ },
191
+ {
192
+ "name": "ESTO",
193
+ "type": "loan",
194
+ "category": "tagatiseta",
195
+ "max": "5k",
196
+ "interest": "pakkumispõhine",
197
+ "note": "järelmaks / laen",
198
+ "chip": [
199
+ "tagatiseta"
200
+ ]
201
+ },
202
+ {
203
+ "name": "Fjord Bank",
204
+ "type": "loan",
205
+ "category": "tagatiseta",
206
+ "max": "20k",
207
+ "interest": "pakkumispõhine",
208
+ "note": "väikelaen / refinantseerimine",
209
+ "chip": [
210
+ "tagatiseta"
211
+ ]
212
+ },
213
+ {
214
+ "name": "Smsraha",
215
+ "type": "loan",
216
+ "category": "tagatiseta",
217
+ "max": "5k",
218
+ "interest": "pakkumispõhine",
219
+ "note": "kiirlaen",
220
+ "chip": [
221
+ "tagatiseta",
222
+ "uus"
223
+ ]
224
+ },
225
+ {
226
+ "name": "Primero",
227
+ "type": "loan",
228
+ "category": "tagatiseta",
229
+ "max": "15k",
230
+ "interest": "alates 1% kuus",
231
+ "note": "sõidukilaen / väikelaen",
232
+ "chip": [
233
+ "tagatiseta"
234
+ ]
235
+ },
236
+ {
237
+ "name": "Laenukompass",
238
+ "type": "loan",
239
+ "category": "vahendaja",
240
+ "max": "võrdlus",
241
+ "interest": "-",
242
+ "note": "võrdlusportaal",
243
+ "chip": [
244
+ "uus"
245
+ ]
246
+ },
247
+ {
248
+ "name": "Nordicbanks",
249
+ "type": "loan",
250
+ "category": "vahendaja",
251
+ "max": "võrdlus",
252
+ "interest": "-",
253
+ "note": "laenuvõrdlus",
254
+ "chip": [
255
+ "uus"
256
+ ]
257
+ },
258
+ {
259
+ "name": "Kreditum",
260
+ "type": "loan",
261
+ "category": "vahendaja",
262
+ "max": "võrdlus",
263
+ "interest": "-",
264
+ "note": "laenuandjate võrdlus",
265
+ "chip": [
266
+ "uus"
267
+ ]
268
+ },
269
+ {
270
+ "name": "Laenuleidja",
271
+ "type": "loan",
272
+ "category": "vahendaja",
273
+ "max": "võrdlus",
274
+ "interest": "-",
275
+ "note": "laenuvõrdlus ja blogi",
276
+ "chip": [
277
+ "uus"
278
+ ]
279
+ },
280
+ {
281
+ "name": "Krediidiandja.ee",
282
+ "type": "loan",
283
+ "category": "vahendaja",
284
+ "max": "võrdlus",
285
+ "interest": "-",
286
+ "note": "krediidiandjate kataloog",
287
+ "chip": [
288
+ "uus"
289
+ ]
290
+ },
291
+ {
292
+ "name": "Luminor",
293
+ "type": "leasing",
294
+ "category": "kapitalirent",
295
+ "max": "100k+",
296
+ "interest": "pakkumispõhine",
297
+ "note": "liising ettevõtetele",
298
+ "chip": [
299
+ "kapitalirent",
300
+ "auto"
301
+ ]
302
+ },
303
+ {
304
+ "name": "Citadele",
305
+ "type": "leasing",
306
+ "category": "kapitalirent",
307
+ "max": "90% varast",
308
+ "interest": "pakkumispõhine",
309
+ "note": "kapitali- või kasutusrent",
310
+ "chip": [
311
+ "kapitalirent",
312
+ "auto"
313
+ ]
314
+ },
315
+ {
316
+ "name": "SEB Liising",
317
+ "type": "leasing",
318
+ "category": "kapitalirent",
319
+ "max": "kohanduv",
320
+ "interest": "pakkumispõhine",
321
+ "note": "ettevõtte põhivara",
322
+ "chip": [
323
+ "kapitalirent",
324
+ "auto"
325
+ ]
326
+ },
327
+ {
328
+ "name": "Swedbank Liising",
329
+ "type": "leasing",
330
+ "category": "kapitalirent",
331
+ "max": "kohanduv",
332
+ "interest": "pakkumispõhine",
333
+ "note": "auto ja seadmete finantseerimine",
334
+ "chip": [
335
+ "kapitalirent",
336
+ "auto"
337
+ ]
338
+ },
339
+ {
340
+ "name": "LHV Liising",
341
+ "type": "leasing",
342
+ "category": "kapitalirent",
343
+ "max": "kohanduv",
344
+ "interest": "pakkumispõhine",
345
+ "note": "liising ettevõttele",
346
+ "chip": [
347
+ "kapitalirent",
348
+ "auto"
349
+ ]
350
+ },
351
+ {
352
+ "name": "Baltasar Liising",
353
+ "type": "leasing",
354
+ "category": "kapitalirent",
355
+ "max": "kohanduv",
356
+ "interest": "pakkumispõhine",
357
+ "note": "krediidiandja nimekirjas",
358
+ "chip": [
359
+ "kapitalirent",
360
+ "auto"
361
+ ]
362
+ },
363
+ {
364
+ "name": "BB Finance",
365
+ "type": "loan",
366
+ "category": "tagatiseta",
367
+ "max": "kohanduv",
368
+ "interest": "pakkumispõhine",
369
+ "note": "krediidiandja nimekirjas",
370
+ "chip": [
371
+ "tagatiseta"
372
+ ]
373
+ },
374
+ {
375
+ "name": "Best Capital",
376
+ "type": "loan",
377
+ "category": "tagatiseta",
378
+ "max": "kohanduv",
379
+ "interest": "pakkumispõhine",
380
+ "note": "krediidiandja nimekirjas",
381
+ "chip": [
382
+ "tagatiseta"
383
+ ]
384
+ },
385
+ {
386
+ "name": "Berger Financial Group",
387
+ "type": "loan",
388
+ "category": "tagatiseta",
389
+ "max": "kohanduv",
390
+ "interest": "pakkumispõhine",
391
+ "note": "krediidiandja nimekirjas",
392
+ "chip": [
393
+ "tagatiseta"
394
+ ]
395
+ },
396
+ {
397
+ "name": "Bondora AS",
398
+ "type": "loan",
399
+ "category": "tagatiseta",
400
+ "max": "10k+",
401
+ "interest": "alates 31.69%",
402
+ "note": "krediidiandja nimekirjas",
403
+ "chip": [
404
+ "tagatiseta"
405
+ ]
406
+ },
407
+ {
408
+ "name": "Clementer",
409
+ "type": "loan",
410
+ "category": "tagatiseta",
411
+ "max": "kohanduv",
412
+ "interest": "pakkumispõhine",
413
+ "note": "krediidiandja nimekirjas",
414
+ "chip": [
415
+ "tagatiseta"
416
+ ]
417
+ },
418
+ {
419
+ "name": "Creditstar",
420
+ "type": "loan",
421
+ "category": "tagatiseta",
422
+ "max": "10k",
423
+ "interest": "pakkumispõhine",
424
+ "note": "tuntud krediidipakkuja",
425
+ "chip": [
426
+ "tagatiseta"
427
+ ]
428
+ },
429
+ {
430
+ "name": "Ipizza",
431
+ "type": "loan",
432
+ "category": "tagatiseta",
433
+ "max": "kohanduv",
434
+ "interest": "pakkumispõhine",
435
+ "note": "väikelaen / krediidikonto",
436
+ "chip": [
437
+ "tagatiseta"
438
+ ]
439
+ },
440
+ {
441
+ "name": "Mikro Kapital",
442
+ "type": "loan",
443
+ "category": "tagatiseta",
444
+ "max": "kohanduv",
445
+ "interest": "pakkumispõhine",
446
+ "note": "ärifinantseerimine",
447
+ "chip": [
448
+ "tagatiseta"
449
+ ]
450
+ },
451
+ {
452
+ "name": "Placet Group",
453
+ "type": "loan",
454
+ "category": "tagatiseta",
455
+ "max": "kohanduv",
456
+ "interest": "pakkumispõhine",
457
+ "note": "krediidiandja",
458
+ "chip": [
459
+ "tagatiseta"
460
+ ]
461
+ },
462
+ {
463
+ "name": "Reval Finance",
464
+ "type": "loan",
465
+ "category": "tagatiseta",
466
+ "max": "kohanduv",
467
+ "interest": "pakkumispõhine",
468
+ "note": "krediidiandja",
469
+ "chip": [
470
+ "tagatiseta"
471
+ ]
472
+ },
473
+ {
474
+ "name": "Smsmoney",
475
+ "type": "loan",
476
+ "category": "tagatiseta",
477
+ "max": "kohanduv",
478
+ "interest": "pakkumispõhine",
479
+ "note": "kiirlaen",
480
+ "chip": [
481
+ "tagatiseta"
482
+ ]
483
+ },
484
+ {
485
+ "name": "Saare Kalur",
486
+ "type": "loan",
487
+ "category": "tagatiseta",
488
+ "max": "kohanduv",
489
+ "interest": "pakkumispõhine",
490
+ "note": "FI krediidiandjate nimekirjas",
491
+ "chip": [
492
+ "tagatiseta"
493
+ ]
494
+ },
495
+ {
496
+ "name": "Varalaen",
497
+ "type": "loan",
498
+ "category": "tagatiseta",
499
+ "max": "kohanduv",
500
+ "interest": "pakkumispõhine",
501
+ "note": "FI krediidiandjate nimekirjas",
502
+ "chip": [
503
+ "tagatiseta"
504
+ ]
505
+ },
506
+ {
507
+ "name": "Berger Financial Group",
508
+ "type": "loan",
509
+ "category": "tagatiseta",
510
+ "max": "kohanduv",
511
+ "interest": "pakkumispõhine",
512
+ "note": "FI krediidiandjate nimekirjas",
513
+ "chip": [
514
+ "tagatiseta"
515
+ ]
516
+ },
517
+ {
518
+ "name": "Mogo",
519
+ "type": "rent",
520
+ "category": "kasutusrent",
521
+ "max": "auto",
522
+ "interest": "pakkumispõhine",
523
+ "note": "autoliising / kasutusrent",
524
+ "chip": [
525
+ "kasutusrent",
526
+ "auto"
527
+ ]
528
+ },
529
+ {
530
+ "name": "Mobire",
531
+ "type": "rent",
532
+ "category": "kasutusrent",
533
+ "max": "autopark",
534
+ "interest": "pakett",
535
+ "note": "täisteenusrent",
536
+ "chip": [
537
+ "kasutusrent",
538
+ "auto"
539
+ ]
540
+ },
541
+ {
542
+ "name": "Tenor",
543
+ "type": "rent",
544
+ "category": "kasutusrent",
545
+ "max": "seadmed",
546
+ "interest": "pakett",
547
+ "note": "seadmete kasutusrent",
548
+ "chip": [
549
+ "kasutusrent",
550
+ "seadmed"
551
+ ]
552
+ },
553
+ {
554
+ "name": "Mobipunkt",
555
+ "type": "rent",
556
+ "category": "kasutusrent",
557
+ "max": "1–5 a",
558
+ "interest": "pakett",
559
+ "note": "seadmete rendilahendus",
560
+ "chip": [
561
+ "kasutusrent",
562
+ "seadmed"
563
+ ]
564
+ },
565
+ {
566
+ "name": "Telia",
567
+ "type": "rent",
568
+ "category": "kasutusrent",
569
+ "max": "seadmed",
570
+ "interest": "pakett",
571
+ "note": "IT ja side-seadmed",
572
+ "chip": [
573
+ "kasutusrent",
574
+ "seadmed"
575
+ ]
576
+ },
577
+ {
578
+ "name": "Novalnet",
579
+ "type": "rent",
580
+ "category": "kasutusrent",
581
+ "max": "seadmed",
582
+ "interest": "pakett",
583
+ "note": "seadmerendi teenus",
584
+ "chip": [
585
+ "kasutusrent",
586
+ "seadmed"
587
+ ]
588
+ },
589
+ {
590
+ "name": "Yndis",
591
+ "type": "rent",
592
+ "category": "kasutusrent",
593
+ "max": "seadmed",
594
+ "interest": "pakett",
595
+ "note": "tehnika rent",
596
+ "chip": [
597
+ "kasutusrent",
598
+ "seadmed"
599
+ ]
600
+ },
601
+ {
602
+ "name": "Kredex/EIF garantiilahendused",
603
+ "type": "loan",
604
+ "category": "vahendaja",
605
+ "max": "võrdlus",
606
+ "interest": "pakkumispõhine",
607
+ "note": "garantiiga rahastuse kanal",
608
+ "chip": [
609
+ "uus"
610
+ ]
611
+ },
612
+ {
613
+ "name": "Uus pakkuja 55",
614
+ "type": "loan",
615
+ "category": "tagatiseta",
616
+ "max": "kohanduv",
617
+ "interest": "pakkumispõhine",
618
+ "note": "uue turuletulija placeholder",
619
+ "chip": [
620
+ "uus"
621
+ ]
622
+ },
623
+ {
624
+ "name": "Uus pakkuja 56",
625
+ "type": "loan",
626
+ "category": "tagatiseta",
627
+ "max": "kohanduv",
628
+ "interest": "pakkumispõhine",
629
+ "note": "uue turuletulija placeholder",
630
+ "chip": [
631
+ "uus"
632
+ ]
633
+ },
634
+ {
635
+ "name": "Uus pakkuja 57",
636
+ "type": "loan",
637
+ "category": "tagatiseta",
638
+ "max": "kohanduv",
639
+ "interest": "pakkumispõhine",
640
+ "note": "uue turuletulija placeholder",
641
+ "chip": [
642
+ "uus"
643
+ ]
644
+ },
645
+ {
646
+ "name": "Uus pakkuja 58",
647
+ "type": "loan",
648
+ "category": "tagatiseta",
649
+ "max": "kohanduv",
650
+ "interest": "pakkumispõhine",
651
+ "note": "uue turuletulija placeholder",
652
+ "chip": [
653
+ "uus"
654
+ ]
655
+ },
656
+ {
657
+ "name": "Uus pakkuja 59",
658
+ "type": "loan",
659
+ "category": "tagatiseta",
660
+ "max": "kohanduv",
661
+ "interest": "pakkumispõhine",
662
+ "note": "uue turuletulija placeholder",
663
+ "chip": [
664
+ "uus"
665
+ ]
666
+ },
667
+ {
668
+ "name": "Uus pakkuja 60",
669
+ "type": "loan",
670
+ "category": "tagatiseta",
671
+ "max": "kohanduv",
672
+ "interest": "pakkumispõhine",
673
+ "note": "uue turuletulija placeholder",
674
+ "chip": [
675
+ "uus"
676
+ ]
677
+ },
678
+ {
679
+ "name": "Uus pakkuja 61",
680
+ "type": "loan",
681
+ "category": "tagatiseta",
682
+ "max": "kohanduv",
683
+ "interest": "pakkumispõhine",
684
+ "note": "uue turuletulija placeholder",
685
+ "chip": [
686
+ "uus"
687
+ ]
688
+ },
689
+ {
690
+ "name": "Uus pakkuja 62",
691
+ "type": "loan",
692
+ "category": "tagatiseta",
693
+ "max": "kohanduv",
694
+ "interest": "pakkumispõhine",
695
+ "note": "uue turuletulija placeholder",
696
+ "chip": [
697
+ "uus"
698
+ ]
699
+ },
700
+ {
701
+ "name": "Uus pakkuja 63",
702
+ "type": "loan",
703
+ "category": "tagatiseta",
704
+ "max": "kohanduv",
705
+ "interest": "pakkumispõhine",
706
+ "note": "uue turuletulija placeholder",
707
+ "chip": [
708
+ "uus"
709
+ ]
710
+ },
711
+ {
712
+ "name": "Uus pakkuja 64",
713
+ "type": "loan",
714
+ "category": "tagatiseta",
715
+ "max": "kohanduv",
716
+ "interest": "pakkumispõhine",
717
+ "note": "uue turuletulija placeholder",
718
+ "chip": [
719
+ "uus"
720
+ ]
721
+ },
722
+ {
723
+ "name": "Uus pakkuja 65",
724
+ "type": "loan",
725
+ "category": "tagatiseta",
726
+ "max": "kohanduv",
727
+ "interest": "pakkumispõhine",
728
+ "note": "uue turuletulija placeholder",
729
+ "chip": [
730
+ "uus"
731
+ ]
732
+ },
733
+ {
734
+ "name": "Uus pakkuja 66",
735
+ "type": "loan",
736
+ "category": "tagatiseta",
737
+ "max": "kohanduv",
738
+ "interest": "pakkumispõhine",
739
+ "note": "uue turuletulija placeholder",
740
+ "chip": [
741
+ "uus"
742
+ ]
743
+ },
744
+ {
745
+ "name": "Uus pakkuja 67",
746
+ "type": "loan",
747
+ "category": "tagatiseta",
748
+ "max": "kohanduv",
749
+ "interest": "pakkumispõhine",
750
+ "note": "uue turuletulija placeholder",
751
+ "chip": [
752
+ "uus"
753
+ ]
754
+ },
755
+ {
756
+ "name": "Uus pakkuja 68",
757
+ "type": "loan",
758
+ "category": "tagatiseta",
759
+ "max": "kohanduv",
760
+ "interest": "pakkumispõhine",
761
+ "note": "uue turuletulija placeholder",
762
+ "chip": [
763
+ "uus"
764
+ ]
765
+ },
766
+ {
767
+ "name": "Uus pakkuja 69",
768
+ "type": "loan",
769
+ "category": "tagatiseta",
770
+ "max": "kohanduv",
771
+ "interest": "pakkumispõhine",
772
+ "note": "uue turuletulija placeholder",
773
+ "chip": [
774
+ "uus"
775
+ ]
776
+ },
777
+ {
778
+ "name": "Uus pakkuja 70",
779
+ "type": "loan",
780
+ "category": "tagatiseta",
781
+ "max": "kohanduv",
782
+ "interest": "pakkumispõhine",
783
+ "note": "uue turuletulija placeholder",
784
+ "chip": [
785
+ "uus"
786
+ ]
787
+ },
788
+ {
789
+ "name": "Uus pakkuja 71",
790
+ "type": "loan",
791
+ "category": "tagatiseta",
792
+ "max": "kohanduv",
793
+ "interest": "pakkumispõhine",
794
+ "note": "uue turuletulija placeholder",
795
+ "chip": [
796
+ "uus"
797
+ ]
798
+ },
799
+ {
800
+ "name": "Uus pakkuja 72",
801
+ "type": "loan",
802
+ "category": "tagatiseta",
803
+ "max": "kohanduv",
804
+ "interest": "pakkumispõhine",
805
+ "note": "uue turuletulija placeholder",
806
+ "chip": [
807
+ "uus"
808
+ ]
809
+ }
810
+ ]
eestilaenud2026/exported-assets/schema.sql ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ create table if not exists providers (
2
+ id bigserial primary key,
3
+ name text not null,
4
+ type text not null,
5
+ category text not null,
6
+ max_amount text,
7
+ interest text,
8
+ note text,
9
+ chip text[],
10
+ source_url text,
11
+ updated_at timestamptz default now()
12
+ );
eestilaenud2026/exported-assets/script.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ base = Path('output/aimoneyflow_next')
3
+ repo = base / 'repo_blueprint'
4
+ (repo / 'app' / 'api' / 'providers' / '[id]').mkdir(parents=True, exist_ok=True)
5
+ (repo / 'app' / 'api' / 'providers' / 'ai').mkdir(parents=True, exist_ok=True)
6
+ (repo / 'app' / 'api' / 'import').mkdir(parents=True, exist_ok=True)
7
+ (repo / 'app' / 'api' / 'seed').mkdir(parents=True, exist_ok=True)
8
+ (repo / 'lib').mkdir(parents=True, exist_ok=True)
9
+ (repo / 'scripts').mkdir(parents=True, exist_ok=True)
10
+ (repo / 'components').mkdir(parents=True, exist_ok=True)
11
+ (repo / 'data').mkdir(parents=True, exist_ok=True)
12
+
13
+ # .env.example
14
+ (repo / '.env.example').write_text("""DATABASE_URL=postgresql://USER:PASSWORD@HOST/DB?sslmode=require
15
+ OLLAMA_BASE_URL=http://localhost:11434
16
+ OLLAMA_MODEL=llama3.1
17
+ NEXT_PUBLIC_APP_NAME=aiMoneyFlow
18
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
19
+ NETLIFY=1
20
+ """, encoding='utf-8')
21
+
22
+ # package.json
23
+ (repo / 'package.json').write_text("""{
24
+ "name": "aimoneyflow-admin",
25
+ "private": true,
26
+ "scripts": {
27
+ "dev": "next dev",
28
+ "build": "next build",
29
+ "start": "next start",
30
+ "lint": "next lint",
31
+ "seed": "node scripts/seed.mjs",
32
+ "validate:csv": "node scripts/validate-csv.mjs"
33
+ },
34
+ "dependencies": {
35
+ "@neondatabase/serverless": "latest",
36
+ "next": "latest",
37
+ "react": "latest",
38
+ "react-dom": "latest"
39
+ }
40
+ }
41
+ """, encoding='utf-8')
42
+
43
+ # Neon DB layer
44
+ (repo / 'lib' / 'db.js').write_text("""import { Pool } from '@neondatabase/serverless'
45
+ const conn = process.env.DATABASE_URL
46
+ const pool = conn ? new Pool({ connectionString: conn }) : null
47
+ export async function query(text, params = []) {
48
+ if (!pool) throw new Error('DATABASE_URL missing')
49
+ const client = await pool.connect()
50
+ try { return await client.query(text, params) } finally { client.release() }
51
+ }
52
+ """, encoding='utf-8')
53
+
54
+ (repo / 'lib' / 'validation.js').write_text("""export function validateProvider(p) {
55
+ const issues = []
56
+ if (!p?.name) issues.push('Puudub nimi')
57
+ if (!p?.category) issues.push('Puudub kategooria')
58
+ if (!p?.max) issues.push('Puudub max summa')
59
+ if (!p?.interest) issues.push('Puudub intress')
60
+ if (String(p?.max || '').toLowerCase().includes('kohanduv')) issues.push('Max summa on mittespetsiifiline')
61
+ if (p?.category === 'vahendaja') issues.push('Pole otsene krediidiandja')
62
+ return { ok: issues.length === 0, issues }
63
+ }
64
+ """, encoding='utf-8')
65
+
66
+ (repo / 'lib' / 'ollama.js').write_text("""export async function analyzeWithOllama(provider, mode='validate') {
67
+ const base = process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
68
+ const model = process.env.OLLAMA_MODEL || 'llama3.1'
69
+ const prompt = mode === 'validate'
70
+ ? `Sa oled andmevalideerija. Hinda kirjet ja tagasta ainult JSON: {score:number, risks:string[], notes:string[]}. Kirje: ${JSON.stringify(provider)}`
71
+ : `Sa oled fintech analüütik. Anna lühike JSON: {summary:string, risks:string[], tags:string[]}. Kirje: ${JSON.stringify(provider)}`
72
+ const res = await fetch(`${base}/api/generate`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ model, prompt, stream:false }) })
73
+ if (!res.ok) throw new Error(`Ollama error ${res.status}`)
74
+ return res.json()
75
+ }
76
+ """, encoding='utf-8')
77
+
78
+ (repo / 'lib' / 'csv.js').write_text("""export function parseCSV(text) {
79
+ const lines = text.trim().split(/\r?\n/)
80
+ const headers = lines.shift().split(',').map(s => s.trim())
81
+ return lines.filter(Boolean).map(line => {
82
+ const cols = []
83
+ let cur = '', inQ = false
84
+ for (let i = 0; i < line.length; i++) {
85
+ const ch = line[i], nxt = line[i+1]
86
+ if (ch === '"' && inQ && nxt === '"') { cur += '"'; i++; continue }
87
+ if (ch === '"') { inQ = !inQ; continue }
88
+ if (ch === ',' && !inQ) { cols.push(cur); cur = ''; continue }
89
+ cur += ch
90
+ }
91
+ cols.push(cur)
92
+ return Object.fromEntries(headers.map((h, i) => [h, (cols[i] || '').trim()]))
93
+ })
94
+ }
95
+ """, encoding='utf-8')
96
+
97
+ (repo / 'lib' / 'storage.js').write_text("""import { query } from './db'
98
+ export async function listProviders(){ const r = await query('select * from providers order by id desc'); return r.rows }
99
+ export async function getProvider(id){ const r = await query('select * from providers where id=$1', [id]); return r.rows[0] }
100
+ export async function createProvider(data){ const r = await query('insert into providers(name,type,category,max_amount,interest,note,chip,source_url) values($1,$2,$3,$4,$5,$6,$7,$8) returning *', [data.name,data.type,data.category,data.max,data.interest,data.note,data.chip||[],data.source_url||null]); return r.rows[0] }
101
+ export async function updateProvider(id, data){ const r = await query('update providers set name=$1,type=$2,category=$3,max_amount=$4,interest=$5,note=$6,chip=$7,source_url=$8,updated_at=now() where id=$9 returning *', [data.name,data.type,data.category,data.max,data.interest,data.note,data.chip||[],data.source_url||null,id]); return r.rows[0] }
102
+ export async function deleteProvider(id){ await query('delete from providers where id=$1', [id]); return { ok:true } }
103
+ export async function seedProviders(items){ await query('delete from providers'); for (const x of items) await createProvider(x); return listProviders() }
104
+ """, encoding='utf-8')
105
+
106
+ # API routes
107
+ (repo / 'app' / 'api' / 'providers' / 'route.js').write_text("""import { NextResponse } from 'next/server'
108
+ import { listProviders, createProvider } from '../../../../lib/storage'
109
+ export async function GET(){ return NextResponse.json(await listProviders()) }
110
+ export async function POST(req){ const data = await req.json(); return NextResponse.json(await createProvider(data), { status:201 }) }
111
+ """, encoding='utf-8')
112
+ (repo / 'app' / 'api' / 'providers' / '[id]' / 'route.js').write_text("""import { NextResponse } from 'next/server'
113
+ import { getProvider, updateProvider, deleteProvider } from '../../../../../lib/storage'
114
+ export async function GET(_, { params }){ const p = await getProvider(params.id); return p ? NextResponse.json(p) : NextResponse.json({ error:'Not found' }, { status:404 }) }
115
+ export async function PATCH(req, { params }){ const data = await req.json(); return NextResponse.json(await updateProvider(params.id, data)) }
116
+ export async function DELETE(_, { params }){ return NextResponse.json(await deleteProvider(params.id)) }
117
+ """, encoding='utf-8')
118
+ (repo / 'app' / 'api' / 'providers' / 'ai' / 'route.js').write_text("""import { NextResponse } from 'next/server'
119
+ import { validateProvider } from '../../../../../lib/validation'
120
+ import { analyzeWithOllama } from '../../../../../lib/ollama'
121
+ export async function POST(req){ const data = await req.json(); const validation = validateProvider(data); try { const ai = await analyzeWithOllama(data); return NextResponse.json({ validation, ai }) } catch(e){ return NextResponse.json({ validation, ai:null, error:String(e.message||e) }) } }
122
+ """, encoding='utf-8')
123
+ (repo / 'app' / 'api' / 'import' / 'route.js').write_text("""import { NextResponse } from 'next/server'
124
+ import { parseCSV } from '../../../../lib/csv'
125
+ import { createProvider } from '../../../../lib/storage'
126
+ export async function POST(req){ const form = await req.formData(); const file = form.get('file'); const text = await file.text(); const rows = parseCSV(text); const out=[]; for (const r of rows) out.push(await createProvider({ name:r.name||r.Nimi, type:r.type||'loan', category:r.category||'tagatiseta', max:r.max||r.max_amount||'', interest:r.interest||'', note:r.note||'', chip:(r.chip||'').split('|').filter(Boolean), source_url:r.source_url||'' })); return NextResponse.json({ imported:out.length, items:out }) }
127
+ """, encoding='utf-8')
128
+ (repo / 'app' / 'api' / 'seed' / 'route.js').write_text("""import { NextResponse } from 'next/server'
129
+ import seed from '../../../../providers.json'
130
+ import { seedProviders } from '../../../../lib/storage'
131
+ export async function POST(){ return NextResponse.json(await seedProviders(seed)) }
132
+ """, encoding='utf-8')
133
+
134
+ # scripts
135
+ (repo / 'scripts' / 'seed.mjs').write_text("""import fs from 'node:fs/promises'
136
+ const data = JSON.parse(await fs.readFile(new URL('../providers.json', import.meta.url), 'utf8'))
137
+ console.log(JSON.stringify({ count: data.length }, null, 2))
138
+ """, encoding='utf-8')
139
+ (repo / 'scripts' / 'validate-csv.mjs').write_text("""import fs from 'node:fs/promises'
140
+ const file = process.argv[2]
141
+ if (!file) { console.error('Usage: node scripts/validate-csv.mjs file.csv'); process.exit(1) }
142
+ const text = await fs.readFile(file, 'utf8')
143
+ const lines = text.trim().split(/\r?\n/)
144
+ const headers = lines.shift().split(',').map(s => s.trim())
145
+ const required = ['name','type','category','max','interest','note']
146
+ const missing = required.filter(x => !headers.includes(x) && !headers.includes(x[0].toUpperCase()+x.slice(1)))
147
+ const issues = []
148
+ if (missing.length) issues.push(`Missing headers: ${missing.join(', ')}`)
149
+ if (!lines.length) issues.push('CSV has no rows')
150
+ console.log(JSON.stringify({ ok: issues.length===0, issues, rows: lines.length }, null, 2))
151
+ if (issues.length) process.exit(1)
152
+ """, encoding='utf-8')
153
+
154
+ # docs files
155
+ (repo / 'README.md').write_text("""# aiMoneyFlow Admin
156
+
157
+ ## Stack
158
+ - Next.js App Router
159
+ - Neon Postgres
160
+ - Ollama local AI
161
+ - Netlify deploy
162
+
163
+ ## Setup
164
+ 1. Copy `.env.example` to `.env.local`
165
+ 2. Set `DATABASE_URL`
166
+ 3. Install deps: `npm i`
167
+ 4. Create DB table using `schema.sql`
168
+ 5. Run `npm run dev`
169
+
170
+ ## Deploy to Netlify
171
+ - Push to GitHub
172
+ - Connect repo in Netlify
173
+ - Build command: `npm run build`
174
+ - Publish: `.next`
175
+ - Add env vars in Netlify dashboard
176
+ """, encoding='utf-8')
177
+
178
+ (repo / 'schema.sql').write_text(Path(base / 'schema.sql').read_text(encoding='utf-8'), encoding='utf-8')
179
+ (repo / 'providers.json').write_text(Path(base / 'providers.json').read_text(encoding='utf-8'), encoding='utf-8')
180
+ print(repo.as_posix())
eestilaenud2026/index (1).html ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="et">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>aiMoneyFlow</title>
7
+ <style>
8
+ :root{
9
+ --bg:#07111f; --panel:#0b1728; --panel2:#0f1d33; --text:#e8eef9; --muted:#92a4c3;
10
+ --line:#20324c; --accent:#7cdb8a; --accent2:#7aa7ff; --warn:#ffd166; --danger:#ff7b7b;
11
+ --shadow: 0 18px 40px rgba(0,0,0,.25);
12
+ --radius:18px;
13
+ }
14
+ *{box-sizing:border-box}
15
+ body{margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:
16
+ radial-gradient(circle at top left, rgba(122,167,255,.13), transparent 28%),
17
+ radial-gradient(circle at top right, rgba(124,219,138,.08), transparent 25%),
18
+ var(--bg);color:var(--text)}
19
+ a{color:inherit;text-decoration:none}
20
+ .wrap{max-width:1400px;margin:0 auto;padding:20px}
21
+ .topbar{display:flex;justify-content:space-between;gap:16px;align-items:center;padding:16px 18px;background:rgba(11,23,40,.82);backdrop-filter:blur(14px);border:1px solid var(--line);border-radius:24px;box-shadow:var(--shadow);position:sticky;top:14px;z-index:10}
22
+ .brand{display:flex;align-items:center;gap:12px}
23
+ .logo{width:44px;height:44px;border-radius:14px;background:linear-gradient(135deg,var(--accent2),var(--accent));display:grid;place-items:center;font-weight:800;color:#06101c}
24
+ .title h1{margin:0;font-size:20px}
25
+ .title p{margin:3px 0 0;color:var(--muted);font-size:13px}
26
+ .controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
27
+ .search{min-width:min(480px,100%);flex:1;padding:13px 14px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);color:var(--text);outline:none}
28
+ .pill{padding:10px 14px;border:1px solid var(--line);background:var(--panel2);border-radius:999px;color:var(--text);font-size:13px;cursor:pointer}
29
+ .hero{display:grid;grid-template-columns:1.25fr .75fr;gap:18px;margin-top:18px}
30
+ .card{background:rgba(11,23,40,.9);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
31
+ .hero-main{padding:26px}
32
+ .hero-main h2{margin:0 0 10px;font-size:34px;line-height:1.08}
33
+ .hero-main p{margin:0;color:var(--muted);max-width:72ch}
34
+ .cta-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
35
+ .btn{padding:12px 16px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font-weight:600;cursor:pointer}
36
+ .btn.primary{background:linear-gradient(135deg,var(--accent2),var(--accent));color:#05111a;border:none}
37
+ .stats{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;padding:18px}
38
+ .stat{padding:16px;border-radius:16px;background:var(--panel2);border:1px solid var(--line)}
39
+ .stat b{display:block;font-size:24px;margin-bottom:4px}
40
+ .stat span{color:var(--muted);font-size:13px}
41
+ .filters{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}
42
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
43
+ .section{margin-top:22px}
44
+ .section h3{margin:0 0 10px;font-size:20px}
45
+ .provider{padding:16px}
46
+ .provider h4{margin:0 0 8px;font-size:17px}
47
+ .meta{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 12px}
48
+ .tag{font-size:12px;padding:6px 10px;border-radius:999px;border:1px solid var(--line);color:var(--muted);background:rgba(255,255,255,.02)}
49
+ .tag.good{color:var(--accent);border-color:rgba(124,219,138,.35)}
50
+ .tag.blue{color:var(--accent2);border-color:rgba(122,167,255,.35)}
51
+ .tag.warn{color:var(--warn);border-color:rgba(255,209,102,.35)}
52
+ .desc{color:var(--muted);font-size:14px;line-height:1.5;margin:0 0 12px}
53
+ .table{width:100%;border-collapse:collapse;overflow:hidden;border-radius:16px;background:rgba(11,23,40,.9);border:1px solid var(--line)}
54
+ .table th,.table td{padding:14px 12px;border-bottom:1px solid rgba(32,50,76,.7);text-align:left;font-size:13px;vertical-align:top}
55
+ .table th{color:#c8d6ee;background:rgba(15,29,51,.9)}
56
+ .table td{color:var(--text)}
57
+ .table td small{display:block;color:var(--muted);margin-top:4px}
58
+ .footer{margin:22px 0 8px;color:var(--muted);font-size:12px}
59
+ .hidden{display:none !important}
60
+ @media (max-width: 980px){.hero{grid-template-columns:1fr}.search{min-width:100%}.topbar{position:static}}
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="wrap">
65
+ <div class="topbar">
66
+ <div class="brand">
67
+ <div class="logo">AF</div>
68
+ <div class="title">
69
+ <h1>aiMoneyFlow</h1>
70
+ <p>Fintech radar • laenud, liising, rent, uued pakkujad</p>
71
+ </div>
72
+ </div>
73
+ <div class="controls">
74
+ <input id="q" class="search" placeholder="Otsi pakkujat, tüüpi või märksõna..." />
75
+ <button class="pill" data-filter="all">Kõik</button>
76
+ <button class="pill" data-filter="loan">Laen</button>
77
+ <button class="pill" data-filter="leasing">Liising</button>
78
+ <button class="pill" data-filter="rent">Rent</button>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="hero">
83
+ <div class="card hero-main">
84
+ <h2>Leia sobiv ärifinantseerimine kiiremini</h2>
85
+ <p>Sama loogika mis sinu varasemal lehel, aga puhtama UX-iga: otsing, filtrid, eraldi sektsioonid ja AI-matching. Kõik andmed käivad läbi GitHub → Neon → Netlify ning mudeline analüüs tuleb Ollamast või Hugging Face’ist.</p>
86
+ <div class="cta-row">
87
+ <button class="btn primary">AI Match</button>
88
+ <button class="btn">Turu radar</button>
89
+ <button class="btn">Uued pakkujad</button>
90
+ </div>
91
+ </div>
92
+ <div class="card stats">
93
+ <div class="stat"><b>70+</b><span>laenutüüpi / pakkujat</span></div>
94
+ <div class="stat"><b>4</b><span>peamist plokki</span></div>
95
+ <div class="stat"><b>Neon</b><span>andmebaas</span></div>
96
+ <div class="stat"><b>Netlify</b><span>front-end</span></div>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="filters">
101
+ <button class="pill" data-chip="tagatiseta">Tagatiseta</button>
102
+ <button class="pill" data-chip="kapitalirent">Kapitalirent</button>
103
+ <button class="pill" data-chip="kasutusrent">Kasutusrent</button>
104
+ <button class="pill" data-chip="auto">Auto</button>
105
+ <button class="pill" data-chip="seadmed">Seadmed</button>
106
+ <button class="pill" data-chip="uus">Uus turul</button>
107
+ </div>
108
+
109
+ <div class="section">
110
+ <h3>Tagatiseta ärilaenud</h3>
111
+ <div id="loanGrid" class="grid"></div>
112
+ </div>
113
+
114
+ <div class="section">
115
+ <h3>Kapitalirent</h3>
116
+ <div id="capitalGrid" class="grid"></div>
117
+ </div>
118
+
119
+ <div class="section">
120
+ <h3>Kasutusrent ja liising</h3>
121
+ <div id="rentGrid" class="grid"></div>
122
+ </div>
123
+
124
+ <div class="section">
125
+ <h3>Kogu nimekiri</h3>
126
+ <table class="table">
127
+ <thead><tr><th>Nimi</th><th>Tüüp</th><th>Max summa</th><th>Intress</th><th>Märkus</th></tr></thead>
128
+ <tbody id="rows"></tbody>
129
+ </table>
130
+ </div>
131
+ <div class="footer">Valmis Netlify jaoks. Andmestik on siia pandud starterina ja saab asendada Neonist tuleva API-ga.</div>
132
+ </div>
133
+
134
+ <script>
135
+ const providers = [
136
+ {name:'Coop Pank', type:'loan', category:'tagatiseta', max:'25k', interest:'alates 6.9%', note:'alustava ettevõtja väikelaen', chip:['tagatiseta']},
137
+ {name:'LHV', type:'loan', category:'tagatiseta', max:'kohanduv', interest:'pakkumispõhine', note:'ärilaen / krediidilahendus', chip:['tagatiseta']},
138
+ {name:'Bigbank', type:'loan', category:'tagatiseta', max:'25k+', interest:'alates 7.9%', note:'ärilaen ja tarbimislahendused', chip:['tagatiseta']},
139
+ {name:'Laen.ee', type:'loan', category:'tagatiseta', max:'50k', interest:'individuaalne', note:'kiire ärilaen', chip:['tagatiseta','uus']},
140
+ {name:'Ärilaen.ee', type:'loan', category:'tagatiseta', max:'15k', interest:'individuaalne', note:'tagatiseta ärilaen', chip:['tagatiseta']},
141
+ {name:'Hoovi', type:'loan', category:'tagatiseta', max:'20k', interest:'individuaalne', note:'käibevahendite rahastus', chip:['tagatiseta']},
142
+ {name:'Bondora', type:'loan', category:'tagatiseta', max:'kuni 10k', interest:'alates 9.9%', note:'tagatiseta väikelaen sobib äri katteks', chip:['tagatiseta']},
143
+ {name:'HyBa', type:'loan', category:'tagatiseta', max:'500k', interest:'personaalne', note:'ärifinantseerimine', chip:['tagatiseta','uus']},
144
+ {name:'Citadele', type:'leasing', category:'kapitalirent', max:'90% varast', interest:'pakkumispõhine', note:'kapitali- või kasutusrent', chip:['kapitalirent','auto']},
145
+ {name:'SEB Liising', type:'leasing', category:'kapitalirent', max:'kohanduv', interest:'pakkumispõhine', note:'ettevõtte põhivara', chip:['kapitalirent','auto']},
146
+ {name:'Telia', type:'leasing', category:'kapitalirent', max:'seadmed', interest:'pakett', note:'IT ja side-seadmed', chip:['kapitalirent','seadmed']},
147
+ {name:'Mobire', type:'rent', category:'kasutusrent', max:'autopark', interest:'pakett', note:'täisteenusrent', chip:['kasutusrent','auto']},
148
+ {name:'Tenor', type:'rent', category:'kasutusrent', max:'seadmed', interest:'pakett', note:'seadmete kasutusrent', chip:['kasutusrent','seadmed']},
149
+ {name:'Mobipunkt', type:'rent', category:'kasutusrent', max:'1–5 a', interest:'pakett', note:'seadmete rendilahendus', chip:['kasutusrent','seadmed']},
150
+ {name:'Mogo', type:'leasing', category:'kasutusrent', max:'auto', interest:'pakkumispõhine', note:'autoliising / kasutusrent', chip:['kasutusrent','auto']}
151
+ ];
152
+ const loanGrid=document.getElementById('loanGrid');
153
+ const capitalGrid=document.getElementById('capitalGrid');
154
+ const rentGrid=document.getElementById('rentGrid');
155
+ const rows=document.getElementById('rows');
156
+ const q=document.getElementById('q');
157
+ let active='all', chip='';
158
+ function card(p){return `<article class="card provider"><h4>${p.name}</h4><div class="meta"><span class="tag blue">${p.type}</span><span class="tag good">${p.category}</span>${p.chip.includes('uus')?'<span class="tag warn">uus turul</span>':''}</div><p class="desc">${p.note}</p><div class="meta"><span class="tag">Max: ${p.max}</span><span class="tag">Intress: ${p.interest}</span></div></article>`}
159
+ function render(){const term=q.value.toLowerCase();const list=providers.filter(p=> (active==='all' || p.type===active) && (!chip || p.chip.includes(chip)) && (p.name.toLowerCase().includes(term)||p.note.toLowerCase().includes(term)||p.category.includes(term)) );loanGrid.innerHTML=list.filter(p=>p.type==='loan').map(card).join('')||'<div class="card provider">Tulemust ei leitud.</div>';capitalGrid.innerHTML=list.filter(p=>p.category==='kapitalirent').map(card).join('')||'<div class="card provider">Tulemust ei leitud.</div>';rentGrid.innerHTML=list.filter(p=>p.type!=='loan' && p.category!=='kapitalirent').map(card).join('')||'<div class="card provider">Tulemust ei leitud.</div>';rows.innerHTML=list.map(p=>`<tr><td>${p.name}</td><td>${p.category}</td><td>${p.max}</td><td>${p.interest}</td><td><small>${p.note}</small></td></tr>`).join('')||'<tr><td colspan="5">Tulemust ei leitud.</td></tr>'}
160
+ document.querySelectorAll('[data-filter]').forEach(b=>b.onclick=()=>{active=b.dataset.filter;render()});document.querySelectorAll('[data-chip]').forEach(b=>b.onclick=()=>{chip=chip===b.dataset.chip?'':b.dataset.chip;render()});q.oninput=render;render();
161
+ </script>
162
+ </body>
163
+ </html>
eestilaenud2026/index.html ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="et">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>aiMoneyFlow</title>
7
+ <style>
8
+ :root{
9
+ --bg:#07111f; --panel:#0b1728; --panel2:#0f1d33; --text:#e8eef9; --muted:#92a4c3;
10
+ --line:#20324c; --accent:#7cdb8a; --accent2:#7aa7ff; --warn:#ffd166; --danger:#ff7b7b;
11
+ --shadow: 0 18px 40px rgba(0,0,0,.25);
12
+ --radius:18px;
13
+ }
14
+ *{box-sizing:border-box}
15
+ body{margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:
16
+ radial-gradient(circle at top left, rgba(122,167,255,.13), transparent 28%),
17
+ radial-gradient(circle at top right, rgba(124,219,138,.08), transparent 25%),
18
+ var(--bg);color:var(--text)}
19
+ a{color:inherit;text-decoration:none}
20
+ .wrap{max-width:1400px;margin:0 auto;padding:20px}
21
+ .topbar{display:flex;justify-content:space-between;gap:16px;align-items:center;padding:16px 18px;background:rgba(11,23,40,.82);backdrop-filter:blur(14px);border:1px solid var(--line);border-radius:24px;box-shadow:var(--shadow);position:sticky;top:14px;z-index:10}
22
+ .brand{display:flex;align-items:center;gap:12px}
23
+ .logo{width:44px;height:44px;border-radius:14px;background:linear-gradient(135deg,var(--accent2),var(--accent));display:grid;place-items:center;font-weight:800;color:#06101c}
24
+ .title h1{margin:0;font-size:20px}
25
+ .title p{margin:3px 0 0;color:var(--muted);font-size:13px}
26
+ .controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
27
+ .search{min-width:min(480px,100%);flex:1;padding:13px 14px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);color:var(--text);outline:none}
28
+ .pill{padding:10px 14px;border:1px solid var(--line);background:var(--panel2);border-radius:999px;color:var(--text);font-size:13px;cursor:pointer}
29
+ .hero{display:grid;grid-template-columns:1.25fr .75fr;gap:18px;margin-top:18px}
30
+ .card{background:rgba(11,23,40,.9);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
31
+ .hero-main{padding:26px}
32
+ .hero-main h2{margin:0 0 10px;font-size:34px;line-height:1.08}
33
+ .hero-main p{margin:0;color:var(--muted);max-width:72ch}
34
+ .cta-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}
35
+ .btn{padding:12px 16px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);color:var(--text);font-weight:600;cursor:pointer}
36
+ .btn.primary{background:linear-gradient(135deg,var(--accent2),var(--accent));color:#05111a;border:none}
37
+ .stats{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;padding:18px}
38
+ .stat{padding:16px;border-radius:16px;background:var(--panel2);border:1px solid var(--line)}
39
+ .stat b{display:block;font-size:24px;margin-bottom:4px}
40
+ .stat span{color:var(--muted);font-size:13px}
41
+ .filters{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}
42
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
43
+ .section{margin-top:22px}
44
+ .section h3{margin:0 0 10px;font-size:20px}
45
+ .provider{padding:16px}
46
+ .provider h4{margin:0 0 8px;font-size:17px}
47
+ .meta{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 12px}
48
+ .tag{font-size:12px;padding:6px 10px;border-radius:999px;border:1px solid var(--line);color:var(--muted);background:rgba(255,255,255,.02)}
49
+ .tag.good{color:var(--accent);border-color:rgba(124,219,138,.35)}
50
+ .tag.blue{color:var(--accent2);border-color:rgba(122,167,255,.35)}
51
+ .tag.warn{color:var(--warn);border-color:rgba(255,209,102,.35)}
52
+ .desc{color:var(--muted);font-size:14px;line-height:1.5;margin:0 0 12px}
53
+ .table{width:100%;border-collapse:collapse;overflow:hidden;border-radius:16px;background:rgba(11,23,40,.9);border:1px solid var(--line)}
54
+ .table th,.table td{padding:14px 12px;border-bottom:1px solid rgba(32,50,76,.7);text-align:left;font-size:13px;vertical-align:top}
55
+ .table th{color:#c8d6ee;background:rgba(15,29,51,.9)}
56
+ .table td{color:var(--text)}
57
+ .table td small{display:block;color:var(--muted);margin-top:4px}
58
+ .footer{margin:22px 0 8px;color:var(--muted);font-size:12px}
59
+ .hidden{display:none !important}
60
+ @media (max-width: 980px){.hero{grid-template-columns:1fr}.search{min-width:100%}.topbar{position:static}}
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="wrap">
65
+ <div class="topbar">
66
+ <div class="brand">
67
+ <div class="logo">AF</div>
68
+ <div class="title">
69
+ <h1>aiMoneyFlow</h1>
70
+ <p>Fintech radar • laenud, liising, rent, uued pakkujad</p>
71
+ </div>
72
+ </div>
73
+ <div class="controls">
74
+ <input id="q" class="search" placeholder="Otsi pakkujat, tüüpi või märksõna..." />
75
+ <button class="pill" data-filter="all">Kõik</button>
76
+ <button class="pill" data-filter="loan">Laen</button>
77
+ <button class="pill" data-filter="leasing">Liising</button>
78
+ <button class="pill" data-filter="rent">Rent</button>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="hero">
83
+ <div class="card hero-main">
84
+ <h2>Leia sobiv ärifinantseerimine kiiremini</h2>
85
+ <p>Sama loogika mis sinu varasemal lehel, aga puhtama UX-iga: otsing, filtrid, eraldi sektsioonid ja AI-matching. Kõik andmed käivad läbi GitHub → Neon → Netlify ning mudeline analüüs tuleb Ollamast või Hugging Face’ist.</p>
86
+ <div class="cta-row">
87
+ <button class="btn primary">AI Match</button>
88
+ <button class="btn">Turu radar</button>
89
+ <button class="btn">Uued pakkujad</button>
90
+ </div>
91
+ </div>
92
+ <div class="card stats">
93
+ <div class="stat"><b>70+</b><span>laenutüüpi / pakkujat</span></div>
94
+ <div class="stat"><b>4</b><span>peamist plokki</span></div>
95
+ <div class="stat"><b>Neon</b><span>andmebaas</span></div>
96
+ <div class="stat"><b>Netlify</b><span>front-end</span></div>
97
+ </div>
98
+ </div>
99
+
100
+ <div class="filters">
101
+ <button class="pill" data-chip="tagatiseta">Tagatiseta</button>
102
+ <button class="pill" data-chip="kapitalirent">Kapitalirent</button>
103
+ <button class="pill" data-chip="kasutusrent">Kasutusrent</button>
104
+ <button class="pill" data-chip="auto">Auto</button>
105
+ <button class="pill" data-chip="seadmed">Seadmed</button>
106
+ <button class="pill" data-chip="uus">Uus turul</button>
107
+ </div>
108
+
109
+ <div class="section">
110
+ <h3>Tagatiseta ärilaenud</h3>
111
+ <div id="loanGrid" class="grid"></div>
112
+ </div>
113
+
114
+ <div class="section">
115
+ <h3>Kapitalirent</h3>
116
+ <div id="capitalGrid" class="grid"></div>
117
+ </div>
118
+
119
+ <div class="section">
120
+ <h3>Kasutusrent ja liising</h3>
121
+ <div id="rentGrid" class="grid"></div>
122
+ </div>
123
+
124
+ <div class="section">
125
+ <h3>Kogu nimekiri</h3>
126
+ <table class="table">
127
+ <thead><tr><th>Nimi</th><th>Tüüp</th><th>Max summa</th><th>Intress</th><th>Märkus</th></tr></thead>
128
+ <tbody id="rows"></tbody>
129
+ </table>
130
+ </div>
131
+ <div class="footer">Valmis Netlify jaoks. Andmestik on siia pandud starterina ja saab asendada Neonist tuleva API-ga.</div>
132
+ </div>
133
+
134
+ <script>
135
+ const providers = [
136
+ {name:'Coop Pank', type:'loan', category:'tagatiseta', max:'25k', interest:'alates 6.9%', note:'alustava ettevõtja väikelaen', chip:['tagatiseta']},
137
+ {name:'LHV', type:'loan', category:'tagatiseta', max:'kohanduv', interest:'pakkumispõhine', note:'ärilaen / krediidilahendus', chip:['tagatiseta']},
138
+ {name:'Bigbank', type:'loan', category:'tagatiseta', max:'25k+', interest:'alates 7.9%', note:'ärilaen ja tarbimislahendused', chip:['tagatiseta']},
139
+ {name:'Laen.ee', type:'loan', category:'tagatiseta', max:'50k', interest:'individuaalne', note:'kiire ärilaen', chip:['tagatiseta','uus']},
140
+ {name:'Ärilaen.ee', type:'loan', category:'tagatiseta', max:'15k', interest:'individuaalne', note:'tagatiseta ärilaen', chip:['tagatiseta']},
141
+ {name:'Hoovi', type:'loan', category:'tagatiseta', max:'20k', interest:'individuaalne', note:'käibevahendite rahastus', chip:['tagatiseta']},
142
+ {name:'Bondora', type:'loan', category:'tagatiseta', max:'kuni 10k', interest:'alates 9.9%', note:'tagatiseta väikelaen sobib äri katteks', chip:['tagatiseta']},
143
+ {name:'HyBa', type:'loan', category:'tagatiseta', max:'500k', interest:'personaalne', note:'ärifinantseerimine', chip:['tagatiseta','uus']},
144
+ {name:'Citadele', type:'leasing', category:'kapitalirent', max:'90% varast', interest:'pakkumispõhine', note:'kapitali- või kasutusrent', chip:['kapitalirent','auto']},
145
+ {name:'SEB Liising', type:'leasing', category:'kapitalirent', max:'kohanduv', interest:'pakkumispõhine', note:'ettevõtte põhivara', chip:['kapitalirent','auto']},
146
+ {name:'Telia', type:'leasing', category:'kapitalirent', max:'seadmed', interest:'pakett', note:'IT ja side-seadmed', chip:['kapitalirent','seadmed']},
147
+ {name:'Mobire', type:'rent', category:'kasutusrent', max:'autopark', interest:'pakett', note:'täisteenusrent', chip:['kasutusrent','auto']},
148
+ {name:'Tenor', type:'rent', category:'kasutusrent', max:'seadmed', interest:'pakett', note:'seadmete kasutusrent', chip:['kasutusrent','seadmed']},
149
+ {name:'Mobipunkt', type:'rent', category:'kasutusrent', max:'1–5 a', interest:'pakett', note:'seadmete rendilahendus', chip:['kasutusrent','seadmed']},
150
+ {name:'Mogo', type:'leasing', category:'kasutusrent', max:'auto', interest:'pakkumispõhine', note:'autoliising / kasutusrent', chip:['kasutusrent','auto']}
151
+ ];
152
+ const loanGrid=document.getElementById('loanGrid');
153
+ const capitalGrid=document.getElementById('capitalGrid');
154
+ const rentGrid=document.getElementById('rentGrid');
155
+ const rows=document.getElementById('rows');
156
+ const q=document.getElementById('q');
157
+ let active='all', chip='';
158
+ function card(p){return `<article class="card provider"><h4>${p.name}</h4><div class="meta"><span class="tag blue">${p.type}</span><span class="tag good">${p.category}</span>${p.chip.includes('uus')?'<span class="tag warn">uus turul</span>':''}</div><p class="desc">${p.note}</p><div class="meta"><span class="tag">Max: ${p.max}</span><span class="tag">Intress: ${p.interest}</span></div></article>`}
159
+ function render(){const term=q.value.toLowerCase();const list=providers.filter(p=> (active==='all' || p.type===active) && (!chip || p.chip.includes(chip)) && (p.name.toLowerCase().includes(term)||p.note.toLowerCase().includes(term)||p.category.includes(term)) );loanGrid.innerHTML=list.filter(p=>p.type==='loan').map(card).join('')||'<div class="card provider">Tulemust ei leitud.</div>';capitalGrid.innerHTML=list.filter(p=>p.category==='kapitalirent').map(card).join('')||'<div class="card provider">Tulemust ei leitud.</div>';rentGrid.innerHTML=list.filter(p=>p.type!=='loan' && p.category!=='kapitalirent').map(card).join('')||'<div class="card provider">Tulemust ei leitud.</div>';rows.innerHTML=list.map(p=>`<tr><td>${p.name}</td><td>${p.category}</td><td>${p.max}</td><td>${p.interest}</td><td><small>${p.note}</small></td></tr>`).join('')||'<tr><td colspan="5">Tulemust ei leitud.</td></tr>'}
160
+ document.querySelectorAll('[data-filter]').forEach(b=>b.onclick=()=>{active=b.dataset.filter;render()});document.querySelectorAll('[data-chip]').forEach(b=>b.onclick=()=>{chip=chip===b.dataset.chip?'':b.dataset.chip;render()});q.oninput=render;render();
161
+ </script>
162
+ </body>
163
+ </html>
eestilaenud2026/package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "aimoneyflow-admin",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "next dev",
6
+ "build": "next build",
7
+ "start": "next start"
8
+ },
9
+ "dependencies": {
10
+ "next": "latest",
11
+ "react": "latest",
12
+ "react-dom": "latest"
13
+ }
14
+ }
eestilaenud2026/providers.json ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "Swedbank",
4
+ "type": "loan",
5
+ "category": "tagatiseta",
6
+ "max": "15k+",
7
+ "interest": "alates 7.9%",
8
+ "note": "väikelaen ja ärilahendused",
9
+ "chip": [
10
+ "tagatiseta"
11
+ ]
12
+ },
13
+ {
14
+ "name": "SEB",
15
+ "type": "loan",
16
+ "category": "tagatiseta",
17
+ "max": "15k+",
18
+ "interest": "alates 7.9%",
19
+ "note": "väikelaen / liising ettevõttele",
20
+ "chip": [
21
+ "tagatiseta"
22
+ ]
23
+ },
24
+ {
25
+ "name": "LHV",
26
+ "type": "loan",
27
+ "category": "tagatiseta",
28
+ "max": "kohanduv",
29
+ "interest": "pakkumispõhine",
30
+ "note": "ärilaen, liising, finantseerimine",
31
+ "chip": [
32
+ "tagatiseta"
33
+ ]
34
+ },
35
+ {
36
+ "name": "Coop Pank",
37
+ "type": "loan",
38
+ "category": "tagatiseta",
39
+ "max": "25k",
40
+ "interest": "alates 6.9%",
41
+ "note": "alustava ettevõtja väikelaen",
42
+ "chip": [
43
+ "tagatiseta"
44
+ ]
45
+ },
46
+ {
47
+ "name": "Bigbank",
48
+ "type": "loan",
49
+ "category": "tagatiseta",
50
+ "max": "25k+",
51
+ "interest": "alates 7.9%",
52
+ "note": "ärilaen ja väikelaen",
53
+ "chip": [
54
+ "tagatiseta"
55
+ ]
56
+ },
57
+ {
58
+ "name": "Inbank",
59
+ "type": "loan",
60
+ "category": "tagatiseta",
61
+ "max": "10k",
62
+ "interest": "alates 8.9%",
63
+ "note": "väikelaen ja järelmaks",
64
+ "chip": [
65
+ "tagatiseta"
66
+ ]
67
+ },
68
+ {
69
+ "name": "TF Bank",
70
+ "type": "loan",
71
+ "category": "tagatiseta",
72
+ "max": "20k",
73
+ "interest": "alates 7.9%",
74
+ "note": "väikelaen / krediidikonto",
75
+ "chip": [
76
+ "tagatiseta"
77
+ ]
78
+ },
79
+ {
80
+ "name": "Ferratum",
81
+ "type": "loan",
82
+ "category": "tagatiseta",
83
+ "max": "5k",
84
+ "interest": "alates 26.85%",
85
+ "note": "krediidikonto / kiirfinantseerimine",
86
+ "chip": [
87
+ "tagatiseta",
88
+ "uus"
89
+ ]
90
+ },
91
+ {
92
+ "name": "Credit24",
93
+ "type": "loan",
94
+ "category": "tagatiseta",
95
+ "max": "10k",
96
+ "interest": "alates 27.24%",
97
+ "note": "krediidikonto / väikelaen",
98
+ "chip": [
99
+ "tagatiseta"
100
+ ]
101
+ },
102
+ {
103
+ "name": "Monefit",
104
+ "type": "loan",
105
+ "category": "tagatiseta",
106
+ "max": "10k",
107
+ "interest": "alates 53.77%",
108
+ "note": "krediidiliin",
109
+ "chip": [
110
+ "tagatiseta"
111
+ ]
112
+ },
113
+ {
114
+ "name": "Bondora",
115
+ "type": "loan",
116
+ "category": "tagatiseta",
117
+ "max": "10k",
118
+ "interest": "alates 31.69%",
119
+ "note": "tagatiseta laen, sobib äri katteks",
120
+ "chip": [
121
+ "tagatiseta"
122
+ ]
123
+ },
124
+ {
125
+ "name": "Raha24",
126
+ "type": "loan",
127
+ "category": "tagatiseta",
128
+ "max": "10k",
129
+ "interest": "pakkumispõhine",
130
+ "note": "kiirlaen / väikelaen",
131
+ "chip": [
132
+ "tagatiseta"
133
+ ]
134
+ },
135
+ {
136
+ "name": "HyBa",
137
+ "type": "loan",
138
+ "category": "tagatiseta",
139
+ "max": "500k",
140
+ "interest": "personaalne",
141
+ "note": "ärifinantseerimine",
142
+ "chip": [
143
+ "tagatiseta",
144
+ "uus"
145
+ ]
146
+ },
147
+ {
148
+ "name": "Laen.ee",
149
+ "type": "loan",
150
+ "category": "tagatiseta",
151
+ "max": "50k",
152
+ "interest": "individuaalne",
153
+ "note": "tagatiseta ärilaen",
154
+ "chip": [
155
+ "tagatiseta"
156
+ ]
157
+ },
158
+ {
159
+ "name": "Ärilaen.ee",
160
+ "type": "loan",
161
+ "category": "tagatiseta",
162
+ "max": "15k",
163
+ "interest": "individuaalne",
164
+ "note": "tagatiseta ärilaen",
165
+ "chip": [
166
+ "tagatiseta"
167
+ ]
168
+ },
169
+ {
170
+ "name": "Hoovi",
171
+ "type": "loan",
172
+ "category": "tagatiseta",
173
+ "max": "20k",
174
+ "interest": "individuaalne",
175
+ "note": "käibevahendite rahastus",
176
+ "chip": [
177
+ "tagatiseta"
178
+ ]
179
+ },
180
+ {
181
+ "name": "Creditea",
182
+ "type": "loan",
183
+ "category": "tagatiseta",
184
+ "max": "10k",
185
+ "interest": "pakkumispõhine",
186
+ "note": "krediidikonto / väikelaen",
187
+ "chip": [
188
+ "tagatiseta"
189
+ ]
190
+ },
191
+ {
192
+ "name": "ESTO",
193
+ "type": "loan",
194
+ "category": "tagatiseta",
195
+ "max": "5k",
196
+ "interest": "pakkumispõhine",
197
+ "note": "järelmaks / laen",
198
+ "chip": [
199
+ "tagatiseta"
200
+ ]
201
+ },
202
+ {
203
+ "name": "Fjord Bank",
204
+ "type": "loan",
205
+ "category": "tagatiseta",
206
+ "max": "20k",
207
+ "interest": "pakkumispõhine",
208
+ "note": "väikelaen / refinantseerimine",
209
+ "chip": [
210
+ "tagatiseta"
211
+ ]
212
+ },
213
+ {
214
+ "name": "Laenukompass",
215
+ "type": "loan",
216
+ "category": "vahendaja",
217
+ "max": "võrdlus",
218
+ "interest": "-",
219
+ "note": "võrdlusportaal, mitte otsene krediidiandja",
220
+ "chip": [
221
+ "uus"
222
+ ]
223
+ },
224
+ {
225
+ "name": "Nordicbanks",
226
+ "type": "loan",
227
+ "category": "vahendaja",
228
+ "max": "võrdlus",
229
+ "interest": "-",
230
+ "note": "laenuvõrdlus ja turuülevaade",
231
+ "chip": [
232
+ "uus"
233
+ ]
234
+ },
235
+ {
236
+ "name": "Kreditum",
237
+ "type": "loan",
238
+ "category": "vahendaja",
239
+ "max": "võrdlus",
240
+ "interest": "-",
241
+ "note": "laenuandjate võrdlus",
242
+ "chip": [
243
+ "uus"
244
+ ]
245
+ },
246
+ {
247
+ "name": "Luminor",
248
+ "type": "leasing",
249
+ "category": "kapitalirent",
250
+ "max": "100k+",
251
+ "interest": "pakkumispõhine",
252
+ "note": "liising ettevõtetele",
253
+ "chip": [
254
+ "kapitalirent",
255
+ "auto"
256
+ ]
257
+ },
258
+ {
259
+ "name": "Citadele",
260
+ "type": "leasing",
261
+ "category": "kapitalirent",
262
+ "max": "90% varast",
263
+ "interest": "pakkumispõhine",
264
+ "note": "kapitali- või kasutusrent",
265
+ "chip": [
266
+ "kapitalirent",
267
+ "auto"
268
+ ]
269
+ },
270
+ {
271
+ "name": "SEB Liising",
272
+ "type": "leasing",
273
+ "category": "kapitalirent",
274
+ "max": "kohanduv",
275
+ "interest": "pakkumispõhine",
276
+ "note": "ettevõtte põhivara",
277
+ "chip": [
278
+ "kapitalirent",
279
+ "auto"
280
+ ]
281
+ },
282
+ {
283
+ "name": "Swedbank Liising",
284
+ "type": "leasing",
285
+ "category": "kapitalirent",
286
+ "max": "kohanduv",
287
+ "interest": "pakkumispõhine",
288
+ "note": "auto ja seadmete finantseerimine",
289
+ "chip": [
290
+ "kapitalirent",
291
+ "auto"
292
+ ]
293
+ },
294
+ {
295
+ "name": "LHV Liising",
296
+ "type": "leasing",
297
+ "category": "kapitalirent",
298
+ "max": "kohanduv",
299
+ "interest": "pakkumispõhine",
300
+ "note": "liising ettevõttele",
301
+ "chip": [
302
+ "kapitalirent",
303
+ "auto"
304
+ ]
305
+ },
306
+ {
307
+ "name": "Telia",
308
+ "type": "rent",
309
+ "category": "kasutusrent",
310
+ "max": "seadmed",
311
+ "interest": "pakett",
312
+ "note": "IT ja side-seadmed",
313
+ "chip": [
314
+ "kasutusrent",
315
+ "seadmed"
316
+ ]
317
+ },
318
+ {
319
+ "name": "Mobire",
320
+ "type": "rent",
321
+ "category": "kasutusrent",
322
+ "max": "autopark",
323
+ "interest": "pakett",
324
+ "note": "täisteenusrent",
325
+ "chip": [
326
+ "kasutusrent",
327
+ "auto"
328
+ ]
329
+ },
330
+ {
331
+ "name": "Tenor",
332
+ "type": "rent",
333
+ "category": "kasutusrent",
334
+ "max": "seadmed",
335
+ "interest": "pakett",
336
+ "note": "seadmete kasutusrent",
337
+ "chip": [
338
+ "kasutusrent",
339
+ "seadmed"
340
+ ]
341
+ },
342
+ {
343
+ "name": "Mobipunkt",
344
+ "type": "rent",
345
+ "category": "kasutusrent",
346
+ "max": "1–5 a",
347
+ "interest": "pakett",
348
+ "note": "seadmete rendilahendus",
349
+ "chip": [
350
+ "kasutusrent",
351
+ "seadmed"
352
+ ]
353
+ },
354
+ {
355
+ "name": "Mogo",
356
+ "type": "rent",
357
+ "category": "kasutusrent",
358
+ "max": "auto",
359
+ "interest": "pakkumispõhine",
360
+ "note": "autoliising / kasutusrent",
361
+ "chip": [
362
+ "kasutusrent",
363
+ "auto"
364
+ ]
365
+ }
366
+ ]
eestilaenud2026/schema.sql ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ create table if not exists providers (
2
+ id bigserial primary key,
3
+ name text not null,
4
+ type text not null,
5
+ category text not null,
6
+ max_amount text,
7
+ interest text,
8
+ note text,
9
+ chip text[],
10
+ source_url text,
11
+ updated_at timestamptz default now()
12
+ );
eestilaenud2026/seed.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "Coop Pank",
4
+ "type": "loan",
5
+ "category": "tagatiseta",
6
+ "max_amount": "25k",
7
+ "interest": "alates 6.9%",
8
+ "note": "alustava ettevõtja väikelaen",
9
+ "chip": [
10
+ "tagatiseta"
11
+ ]
12
+ },
13
+ {
14
+ "name": "Laen.ee",
15
+ "type": "loan",
16
+ "category": "tagatiseta",
17
+ "max_amount": "50k",
18
+ "interest": "individuaalne",
19
+ "note": "kiire ärilaen",
20
+ "chip": [
21
+ "tagatiseta",
22
+ "uus"
23
+ ]
24
+ },
25
+ {
26
+ "name": "Citadele",
27
+ "type": "leasing",
28
+ "category": "kapitalirent",
29
+ "max_amount": "90% varast",
30
+ "interest": "pakkumispõhine",
31
+ "note": "kapitali- või kasutusrent",
32
+ "chip": [
33
+ "kapitalirent",
34
+ "auto"
35
+ ]
36
+ }
37
+ ]
eestilaenud2026/validate-kit-6.mjs ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const registryPath = path.join(root, "registry", "skills.index.json");
8
+ const schemaPath = path.join(root, "schemas", "universal-skill.schema.json");
9
+
10
+ const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, "utf8"));
11
+
12
+ const errors = [];
13
+ const warnings = [];
14
+
15
+ function exists(relativePath) {
16
+ return fs.existsSync(path.join(root, relativePath));
17
+ }
18
+
19
+ function validateRegistry() {
20
+ const registry = readJson(registryPath);
21
+ if (!Array.isArray(registry.skills)) {
22
+ errors.push("registry/skills.index.json: `skills` must be an array");
23
+ return;
24
+ }
25
+
26
+ const seen = new Set();
27
+ for (const entry of registry.skills) {
28
+ if (!entry.id) errors.push("registry entry missing `id`");
29
+ if (seen.has(entry.id)) errors.push(`duplicate registry id: ${entry.id}`);
30
+ seen.add(entry.id);
31
+
32
+ if (!entry.path) {
33
+ errors.push(`${entry.id}: missing path`);
34
+ } else if (!exists(entry.path)) {
35
+ errors.push(`${entry.id}: path does not exist: ${entry.path}`);
36
+ }
37
+ }
38
+ }
39
+
40
+ function validateUcaFile(relativePath, schema) {
41
+ const data = readJson(path.join(root, relativePath));
42
+ for (const field of schema.required || []) {
43
+ if (!(field in data)) {
44
+ errors.push(`${relativePath}: missing required field: ${field}`);
45
+ }
46
+ }
47
+
48
+ const statusEnum = schema.properties?.status?.enum || [];
49
+ if (data.status && !statusEnum.includes(data.status)) {
50
+ errors.push(`${relativePath}: invalid status: ${data.status}`);
51
+ }
52
+
53
+ const queueEnum = schema.properties?.queue_state?.enum || [];
54
+ if (data.queue_state && !queueEnum.includes(data.queue_state)) {
55
+ errors.push(`${relativePath}: invalid queue_state: ${data.queue_state}`);
56
+ }
57
+
58
+ if (data.steps && !Array.isArray(data.steps)) {
59
+ errors.push(`${relativePath}: steps must be an array`);
60
+ }
61
+ }
62
+
63
+ function validateUcaFiles() {
64
+ const schema = readJson(schemaPath);
65
+ const candidates = [
66
+ "skills/create-skill/skill.json",
67
+ "skills/luuna-capability-radar/skill.json",
68
+ "examples/luuna-capability-radar.example.json",
69
+ ];
70
+
71
+ for (const relativePath of candidates) {
72
+ if (!exists(relativePath)) {
73
+ warnings.push(`skipped missing UCA file: ${relativePath}`);
74
+ continue;
75
+ }
76
+ validateUcaFile(relativePath, schema);
77
+ }
78
+ }
79
+
80
+ function validateLegacyNames() {
81
+ for (const legacy of ["arhidecture", "capibility-radar", "files"]) {
82
+ if (exists(legacy)) errors.push(`legacy folder still exists: ${legacy}`);
83
+ }
84
+ }
85
+
86
+ validateRegistry();
87
+ validateUcaFiles();
88
+ validateLegacyNames();
89
+
90
+ if (warnings.length) {
91
+ console.log("Warnings:");
92
+ for (const warning of warnings) console.log(`- ${warning}`);
93
+ }
94
+
95
+ if (errors.length) {
96
+ console.error("Validation failed:");
97
+ for (const error of errors) console.error(`- ${error}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ console.log("SKILLGENERATOR validation OK");