Stiglo je novo izdanje. Osvježi stranu.
Pravo na uvid u kod. Otvoren protokol omogućava nezavisne operatere i interoperabilnost. Manifest
Izvor
4d7883a894b3
- server.js
- lib/auth.js
- lib/build.js
- lib/cards.js
- lib/db.js
- lib/gradovi.js
- lib/icons.js
- lib/imena.js
- lib/jezik.js
- lib/kartapodaci.js
- lib/lekcije.js
- lib/md.js
- lib/mehanika.js
- lib/mreza.js
- lib/pages.js
- lib/registar.js
- lib/routes.js
- lib/schema2.js
- lib/views.js
- seed/content.mjs
- seed/seed.mjs
- public/app.js
- public/style.css
1 import {GRADOVI,GRAD,JEDINICE,tacka,projekcija,unutra} from './gradovi.js';
2 import {POLIGONI} from './kartapodaci.js';
3 import {esc} from './md.js';
4 import {t,jezik} from './jezik.js';
5 export function network(db,now=Date.now()){
6 const counts=Object.fromEntries(db.all('SELECT city_id,COUNT(*) n FROM users WHERE deleted=0 AND city_id IS NOT NULL GROUP BY city_id').map(r=>[r.city_id,r.n]));
7 const nodes=GRADOVI.map(g=>({id:g.id,ime:g.ime,jed:g.jed,n:counts[g.id]||0,point:tacka(g),mirror:!!db.get('SELECT 1 FROM mirrors WHERE city_id=?',g.id)}));
8 const rows=db.all(`SELECT a.city_id a,b.city_id b,p.created_at at,'odgovor' kind,a.is_seed seed FROM posts p JOIN threads t ON t.id=p.thread_id JOIN users a ON a.id=p.user_id JOIN users b ON b.id=t.user_id WHERE p.created_at>=? AND a.city_id IS NOT NULL AND b.city_id IS NOT NULL AND a.city_id!=b.city_id AND p.deleted=0 AND t.deleted=0 AND a.deleted=0 AND b.deleted=0 UNION ALL SELECT a.city_id,b.city_id,o.updated_at,'narudzba',a.is_seed FROM orders o JOIN users a ON a.id=o.buyer_id JOIN users b ON b.id=o.seller_id WHERE o.status='primljeno' AND o.updated_at>=? AND a.city_id IS NOT NULL AND b.city_id IS NOT NULL AND a.city_id!=b.city_id UNION ALL SELECT a.city_id,b.city_id,tr.decided_at,'zamjena',a.is_seed FROM trades tr JOIN users a ON a.id=tr.from_id JOIN users b ON b.id=tr.to_id WHERE tr.status='prihvaceno' AND tr.decided_at>=? AND a.city_id IS NOT NULL AND b.city_id IS NOT NULL AND a.city_id!=b.city_id`,now-30*86400000,now-30*86400000,now-30*86400000);
9 const sums=new Map();for(const r of rows){const [a,b]=[r.a,r.b].sort(),key=a+':'+b;sums.set(key,{a,b,weight:(sums.get(key)?.weight||0)+1});}
10 return {nodes,edges:[...sums.values()].sort((a,b)=>b.weight-a.weight).slice(0,40),events:rows.filter(r=>!r.seed).sort((a,b)=>b.at-a.at).slice(0,10).map(({a,b,at,kind})=>({a,b,at,kind}))};
11 }
12 const svgCache=new WeakMap();
13 export function mapSvg(data,mini=false){const key=(mini?'mini':'full')+(jezik.getStore()||'lat');if(svgCache.get(data)?.has(key))return svgCache.get(data).get(key);const result=`<svg class="map" viewBox="0 0 1230 1060" role="img" aria-label="${t('Karta mreže')}"><defs><pattern id="more" width="12" height="12" patternUnits="userSpaceOnUse"><path d="M0 12L12 0" fill="none" stroke="#003893" stroke-width=".35"/></pattern></defs><rect width="1230" height="1060" fill="#f3ecd8"/><path d="M15 250L700 1035H15Z" fill="url(#more)"/><rect class="dij" x="15" y="15" width="1200" height="1020" rx="12"/>${Object.entries(POLIGONI).map(([k,rs])=>`<a href="/karta/jedinica/${k}"><path data-jed="${k}" d="${rs.map(r=>'M'+r.map(p=>projekcija(...p).map(v=>v.toFixed(1)).join(',')).join('L')+'Z').join('')}"/><title>${t(JEDINICE[k])}</title></a>`).join('')}${!mini?Object.entries({slo:[15.25,46.35],hrv:[16.05,45.25],bih:[17.6,44.1],cg:[19.05,43.1],mak:[21.7,41.55],srb:[20.8,43.5],voj:[20.1,45.7],kos:[20.85,42.45]}).map(([key,point])=>{const [x,y]=projekcija(...point);return `<text class="unit-name" x="${x}" y="${y}" text-anchor="middle">${t(JEDINICE[key])}</text>`;}).join(''):''}${data.edges.map(e=>{const a=tacka(GRAD[e.a]),b=tacka(GRAD[e.b]);return `<path class="edge" data-edge="${e.a}:${e.b}" d="M${a}Q${(a[0]+b[0])/2-20},${(a[1]+b[1])/2-35} ${b}" stroke-width="${e.weight>8?3:e.weight>3?2:1}"/>`;}).join('')}${data.nodes.map(g=>{const [x,y]=g.point,size=g.n>10?10:g.n>5?8:g.n>1?6:4;return `<a href="/karta/grad/${g.id}">${g.jed==='dij'&&!mini?`<rect class="slot" x="${x-8}" y="${y-15}" width="119" height="29"/>`:""}${g.mirror?`<rect class="mirror-node" x="${x-size/2-3}" y="${y-size/2-3}" width="${size+6}" height="${size+6}"/>`:""}<rect data-city="${g.id}" data-x="${x}" data-y="${y}" class="${g.n?'marker':''}" x="${x-size/2}" y="${y-size/2}" width="${g.n?size:2}" height="${g.n?size:2}" fill="${g.n?'#c8102e':'none'}" stroke="#1a1714"/><title>${esc(g.ime)} · ${g.n}</title>${!mini?`<text data-city-label="${g.id}" style="${!g.n&&g.jed!=='dij'?'display:none':''}" x="${x+8}" y="${y+5}">${t(g.ime)} ${g.n}</text>`:''}</a>`;}).join('')}${!mini?`<text x="30" y="1048">${t('Stilizovana karta, granice republika i pokrajina 1974–1991. Nije kartografski tačna. Nije zahtjev za teritorijom.')}</text>`:''}</svg>`;if(!svgCache.has(data))svgCache.set(data,new Map());svgCache.get(data).set(key,result);return result;}
14 export function teleMap(data={nodes:[]}){
15 const keys=Object.keys(POLIGONI),counts=new Map();
16 for(const node of data.nodes){const g=GRAD[node.id];if(!g||g.jed==='dij'||!node.n)continue;const x=Math.round((g.lon-13.2)/10*39),y=Math.round((46.95-g.lat)/6.1*23),key=x+':'+y;counts.set(key,(counts.get(key)||0)+node.n);}
17 return Array.from({length:24},(_,y)=>Array.from({length:40},(_,x)=>{const p=[13.2+x/39*10,46.95-y/23*6.1],key=keys.find(k=>POLIGONI[k].some(r=>unutra(p,r))),n=counts.get(x+':'+y);return n?`<span class="tele-count" title="${n}">${Math.min(n,9)}</span>`:key?`<span class="tele-unit tele-${key}"> </span>`:' ';}).join('')).join('\n');
18 }
19 export function metrics(db,now=Date.now()) {
20 const scalar=(sql,...args)=>db.get(sql,...args).n,week=7*86400000;
21 const current=new Set(db.all('SELECT DISTINCT user_id FROM activities WHERE at>?',now-week).map(x=>x.user_id));
22 const previous=db.all('SELECT DISTINCT user_id FROM activities WHERE at>? AND at<=?',now-2*week,now-week).map(x=>x.user_id);
23 const times=db.all('SELECT created_at,decided_at FROM disputes WHERE decided_at IS NOT NULL').map(x=>x.decided_at-x.created_at).sort((a,b)=>a-b);
24 const median=times.length?(times[Math.floor((times.length-1)/2)]+times[Math.floor(times.length/2)])/2:0;
25 return {
26 clanovi:scalar("SELECT COUNT(*) n FROM users WHERE deleted=0 AND role!='system'"),teme:scalar('SELECT COUNT(*) n FROM threads WHERE deleted=0'),poruke:scalar('SELECT COUNT(*) n FROM posts WHERE deleted=0'),
27 imena:scalar("SELECT COUNT(*) n FROM names WHERE status='izdato'"),cvorovi:scalar('SELECT COUNT(DISTINCT city_id) n FROM users WHERE deleted=0'),stranice:scalar('SELECT COUNT(*) n FROM pages WHERE published=1'),
28 oglasi:scalar("SELECT COUNT(*) n FROM listings WHERE status='otvoreno'"),narudzbe:scalar("SELECT COUNT(*) n FROM orders WHERE status='primljeno'"),aktivni:current.size,
29 povratak_sedmica:previous.length?Math.round(previous.filter(id=>current.has(id)).length/previous.length*100):0,
30 nezavisne_usluge:scalar("SELECT (SELECT COUNT(*) FROM pages WHERE owner_id NOT IN(SELECT id FROM users WHERE role IN('admin','system'))) + (SELECT COUNT(*) FROM listings WHERE seller_id NOT IN(SELECT id FROM users WHERE role IN('admin','system'))) + (SELECT COUNT(*) FROM mirrors WHERE owner_id NOT IN(SELECT id FROM users WHERE role IN('admin','system'))) n"),
31 uspjesno:scalar("SELECT COALESCE(SUM(n),0) n FROM counters WHERE key='razrijeseno'"),odbijeno:scalar("SELECT COALESCE(SUM(n),0) n FROM counters WHERE key='odbijeno'"),
32 srednje_vrijeme_spora:Math.round(median/3600000),usvojene_zalbe:scalar("SELECT COUNT(*) n FROM disputes WHERE status='usvojena'"),odbijene_zalbe:scalar("SELECT COUNT(*) n FROM disputes WHERE status='odbijena'"),
33 izvozi:scalar("SELECT COALESCE(SUM(n),0) n FROM counters WHERE key='izvoz'"),cirilica:scalar("SELECT COUNT(*) n FROM users WHERE deleted=0 AND pismo='cir'"),latinica:scalar("SELECT COUNT(*) n FROM users WHERE deleted=0 AND pismo='lat'"),
34 zamjena_kljuca:scalar('SELECT MAX(created_at) n FROM reg_keys'),koncentracija:Math.round(100*scalar('SELECT COALESCE(SUM(n),0) n FROM(SELECT COUNT(*) n FROM names GROUP BY owner_id ORDER BY n DESC LIMIT 5)')/Math.max(1,scalar('SELECT COUNT(*) n FROM names'))),
35 uvod_prosjek:Math.round(scalar("SELECT COALESCE(SUM(n),0) n FROM counters WHERE key='uvod_sekunde'")/Math.max(1,scalar("SELECT COALESCE(SUM(n),0) n FROM counters WHERE key='uvod_zavrseno'")))
36 };
37 }
38