srijeda, 11. novembar 2026. · Godina I · Broj 15 · Cijena: 0 dinara
JUGOSLOVENSKI FORUM

Glasilo YU mreže · NEMA GRANICA ZA ZNANJE

Prijava Upis
Pravo na uvid u kod. Otvoren protokol omogućava nezavisne operatere i interoperabilnost. Manifest

Izvor

4d7883a894b3

   1 'use strict';
   2 const S=JSON.parse(document.getElementById('str')?.textContent||'{}'), t=(key,vars={})=>(S[key]||key).replace(/\{(\w+)\}/g,(_,k)=>vars[k]??'');
   3 const $=(s,root=document)=>root.querySelector(s),$$=(s,root=document)=>[...root.querySelectorAll(s)];
   4 const reduced=matchMedia('(prefers-reduced-motion: reduce)'),touch=matchMedia('(pointer: coarse)');
   5 const csrf=()=>$('input[name=csrf]')?.value;
   6 const canonical=v=>v===null||typeof v!=='object'?JSON.stringify(v):Array.isArray(v)?'['+v.map(canonical).join(',')+']':'{'+Object.keys(v).sort().map(k=>JSON.stringify(k)+':'+canonical(v[k])).join(',')+'}';
   7 const bytes=s=>Uint8Array.from(atob(s.replaceAll('-','+').replaceAll('_','/')),c=>c.charCodeAt(0));
   8 const hash=async s=>[...new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(s)))].map(x=>x.toString(16).padStart(2,'0')).join('');
   9 async function verify(payload,sig,pem){const key=await crypto.subtle.importKey('spki',bytes(pem.replace(/-----[^-]+-----|\s/g,'')),{name:'Ed25519'},false,['verify']);return crypto.subtle.verify('Ed25519',key,bytes(sig),new TextEncoder().encode(canonical(payload)));}
  10 const keys=()=>fetch('/registar/kljucevi.json').then(r=>r.json());
  11 function show(text){const el=$('[data-verification]');if(el)el.textContent=text;}
  12 $$('[data-clock]').forEach(el=>{const tick=()=>{el.textContent=new Date().toLocaleString('hr-HR',{timeZone:'UTC'});};tick();setInterval(tick,1000);});
  13 $('[data-reload]')?.addEventListener('click',()=>location.reload());
  14 setInterval(async()=>{try{const r=await fetch('/build.json',{cache:'no-store'}),j=await r.json();if(j.build!==$('meta[name=yugoslavia-forum-build]')?.content)$('#update').hidden=false;}catch{}},60000);
  15 $$('[data-quote]').forEach(a=>a.addEventListener('click',()=>{const box=$('#odgovor textarea');if(box)box.value='> '+a.dataset.quote.replaceAll('\n','\n> ')+'\n\n';}));
  16 const cards=$$('.karta'),state=new Map(cards.map(c=>[c,{x:0,y:0,tx:0,ty:0,visible:true,active:false}]));
  17 const clamp=(n,max)=>Math.max(-max,Math.min(max,n));
  18 let frame=0,baseline=null,orientation=false;
  19 const idleOff=()=>cards.forEach(c=>$('.karta-tijelo',c).classList.remove('idle'));
  20 function animate(){frame=0;let moving=false;for(const [c,s]of state){if(!s.visible||reduced.matches)continue;s.x+=(s.tx-s.x)*.15;s.y+=(s.ty-s.y)*.15;c.style.setProperty('--rx',s.x+'deg');c.style.setProperty('--ry',s.y+'deg');c.style.setProperty('--mx',50+s.y*2+'%');c.style.setProperty('--my',50-s.x*2+'%');c.style.setProperty('--hyp',Math.min(1,(Math.abs(s.x)+Math.abs(s.y))/24)+.2);const active=Math.abs(s.x-s.tx)+Math.abs(s.y-s.ty)>.02;$('.karta-tijelo',c).style.willChange=active?'transform':'';moving||=active;}if(moving)frame=requestAnimationFrame(animate);}
  21 const schedule=()=>{if(!frame&&!reduced.matches)frame=requestAnimationFrame(animate);};
  22 const recenter=()=>{baseline=null;for(const s of state.values()){s.tx=s.ty=0;}schedule();};
  23 const observer=new IntersectionObserver(entries=>entries.forEach(e=>{state.get(e.target).visible=e.isIntersecting;$('.karta-tijelo',e.target).style.animationPlayState=e.isIntersecting?'running':'paused';}));
  24 for(const c of cards){observer.observe(c);c.addEventListener('pointermove',e=>{if(reduced.matches)return;idleOff();const r=c.getBoundingClientRect(),s=state.get(c);s.tx=clamp(-((e.clientY-r.top)/r.height-.5)*32,16);s.ty=clamp(((e.clientX-r.left)/r.width-.5)*32,16);s.active=true;c.setAttribute('data-active','');schedule();});c.addEventListener('pointerleave',()=>{const s=state.get(c);s.active=false;c.removeAttribute('data-active');s.tx=s.ty=0;schedule();});c.addEventListener('dblclick',recenter);c.addEventListener('pointercancel',recenter);let lastTap=0;c.addEventListener('pointerup',()=>{if(performance.now()-lastTap<350)recenter();lastTap=performance.now();});$('[data-flip]',c)?.addEventListener('click',e=>{idleOff();const on=c.dataset.flipped!=='true';c.dataset.flipped=String(on);e.currentTarget.setAttribute('aria-pressed',String(on));});$('[data-png]',c)?.addEventListener('click',()=>saveCard(c));}
  25 function sensor(e){if(e.beta==null||e.gamma==null||reduced.matches)return;baseline??={b:e.beta,g:e.gamma};for(const s of state.values()){const factor=s.active?1:.7;s.tx+=(clamp(e.beta-baseline.b,22)*factor-s.tx)*.12;s.ty+=(clamp(e.gamma-baseline.g,22)*factor-s.ty)*.12;}schedule();}
  26 $$('[data-tilt]').forEach(button=>{button.hidden=!touch.matches;button.addEventListener('click',async()=>{idleOff();try{if(typeof DeviceOrientationEvent==='undefined')throw Error();if(typeof DeviceOrientationEvent.requestPermission==='function'&&await DeviceOrientationEvent.requestPermission()!=='granted')throw Error();if(!orientation)window.addEventListener('deviceorientation',sensor);orientation=true;sessionStorage.setItem('yu-tilt','1');recenter();button.disabled=true;}catch{$('[data-tilt-status]').textContent=t('odbijenNagib');}});});
  27 if(sessionStorage.getItem('yu-tilt')==='1'&&typeof DeviceOrientationEvent!=='undefined'&&typeof DeviceOrientationEvent.requestPermission!=='function'){window.addEventListener('deviceorientation',sensor);orientation=true;}
  28 document.addEventListener('visibilitychange',recenter);
  29 async function saveCard(c){const portrait=c.classList.contains('slicica'),canvas=document.createElement('canvas');canvas.width=1200;canvas.height=portrait?1680:756;const ctx=canvas.getContext('2d'),g=ctx.createLinearGradient(0,0,1200,canvas.height);g.addColorStop(0,'#003893');g.addColorStop(.45,'#14110f');g.addColorStop(.75,'#62488d');g.addColorStop(1,'#c5a348');ctx.fillStyle=g;ctx.fillRect(0,0,canvas.width,canvas.height);ctx.strokeStyle='#e7dec3';ctx.lineWidth=4;ctx.strokeRect(20,20,1160,canvas.height-40);const artwork=portrait?$('.sloj-podloga svg',c):$('.av',c);if(artwork){const svg=artwork.tagName.toLowerCase()==='svg'?new XMLSerializer().serializeToString(artwork):null;const url=svg?URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'})):artwork.src;try{const img=new Image();img.src=url;await img.decode();ctx.globalAlpha=.7;ctx.drawImage(img,portrait?100:1010,portrait?760:40,portrait?1000:100,portrait?650:100);ctx.globalAlpha=1;}catch{}finally{if(svg)URL.revokeObjectURL(url);}}ctx.fillStyle='#f3ecd8';const lines=JSON.parse(c.dataset.lines||'[]');let y=100;for(const line of lines){ctx.font=(y===100?'bold 40px Georgia':'34px Georgia');const words=String(line).split(' ');let row='';for(const word of words){if(ctx.measureText(row+word).width>1040&&row){ctx.fillText(row,70,y);y+=46;row='';}row+=word+' ';}ctx.fillText(row,70,y);y+=72;}const blob=await new Promise(r=>canvas.toBlob(r,'image/png'));if(!blob)return;const file=new File([blob],'yu-karta.png',{type:'image/png'});if(navigator.canShare?.({files:[file]})){try{await navigator.share({files:[file]});return;}catch{}}const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=file.name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),30000);}
  30 $$('[data-verify]').forEach(button=>button.addEventListener('click',async()=>{try{const result=await (await fetch('/api/v1/razrijesi?ime='+encodeURIComponent(button.dataset.verify))).json(),ks=await keys(),pub=ks.find(k=>k.key_id===result.key_id);let ok;try{ok=await verify(result.record,result.signature,pub.public_key);}catch{ok=(await (await fetch('/api/v1/provjera?ime='+encodeURIComponent(button.dataset.verify))).json()).ok;}show(t(ok?'Potpis je ispravan.':'Potpis nije ispravan.'));}catch{show(t('Potpis nije ispravan.'));}}));
  31 $('[data-chain]')?.addEventListener('click',async()=>{try{const [rows,ks]=await Promise.all([fetch('/registar/dnevnik.json').then(r=>r.json()),keys()]);let prev='',fallback=false;for(const r of rows){const entry={seq:r.seq,at:r.at,action:r.action,name:r.name,actor_id:r.actor_id,payload:JSON.parse(r.payload)},digest=await hash(prev+canonical(entry));if(r.prev_hash!==prev||digest!==r.hash)throw Error();let valid=true;try{valid=await verify({hash:digest},r.sig,ks.find(k=>k.key_id===r.key_id).public_key);}catch{fallback=true;}if(!valid)throw Error();prev=digest;}if(fallback&&!(await(await fetch('/api/v1/provjera?tip=dnevnik&hash='+await hash(canonical(rows)))).json()).ok)throw Error();show(t('Lanac je ispravan.'));}catch{show(t('Lanac nije ispravan.'));}});
  32 $('#export-verify')?.addEventListener('submit',async e=>{e.preventDefault();try{const j=JSON.parse(new FormData(e.target).get('document')),ks=await keys(),key=ks.find(k=>k.key_id===j.key_id);show(t(await verify(j.payload,j.signature,key.public_key)?'Potpis je ispravan.':'Potpis nije ispravan.'));}catch{show(t('Potpis nije ispravan.'));}});
  33 async function connect(e){e?.preventDefault();const form=$('#connect-form'),data=new FormData(form),name=String(data.get('ime')).normalize('NFKC').trim().toLowerCase(),log=$('#connect-log'),out=$('#connect-result');out.replaceChildren();const line=text=>{const p=document.createElement('p');p.textContent=text;log.append(p);};log.replaceChildren();if(!name.endsWith('.yu')){line(t('internet'));return;}try{line(t('Normalizacija imena')+': '+name.normalize('NFKC'));line(t('Učitavanje potpisanog snimka'));const r=await (await fetch('/api/v1/razrijesi?ime='+encodeURIComponent(name)+(data.get('proba')?'&mreza=proba':''))).json(),ks=await keys(),snap=r.snapshot;if(!snap){line(t(r.state));return;}const {signature,...payload}=snap;let valid;try{valid=await verify(payload,signature,ks.find(k=>k.key_id===snap.key_id).public_key);}catch{valid=(await (await fetch('/api/v1/provjera?tip=snimak&hash='+await hash(canonical(payload)))).json()).ok;}if(!valid||snap.expires_at<Date.now()){line(t('Potpis nije ispravan.'));return;}const highest=Number(localStorage.getItem('yu-sequence')||0);if(snap.sequence<highest){line(t('rollback'));return;}localStorage.setItem('yu-sequence',String(snap.sequence));line(t('Čitanje zapisa')+': '+r.record.sequence);if(!r.ok){line(t(r.state));return;}let recordOK;try{recordOK=await verify(r.record,r.signature,ks.find(k=>k.key_id===r.key_id).public_key);}catch{recordOK=(await(await fetch('/api/v1/provjera?ime='+encodeURIComponent(name)+'&hash='+await hash(canonical(r.record)))).json()).ok;}if(!recordOK){line(t('Potpis nije ispravan.'));return;}line(t('Porijeklo')+': '+r.record.origin);line(t('Potpis je ispravan.'));const bar=document.createElement('div');bar.className='origin';bar.textContent=name+' · '+r.record.origin+' · '+t(r.trust)+' · '+t('Zapis br.')+' '+r.record.sequence;out.append(bar);line(t('Alias ostaje uz pravo porijeklo.'));if(r.net==='proba'){const tag=document.createElement('p');tag.className='tag';tag.textContent=t('PROBNO');out.append(tag);}const target=new URL(r.record.origin),production='https://www.yugoslavia.forum';if(target.origin===production||target.origin===location.origin){const iframe=document.createElement('iframe');iframe.src=target.pathname+target.search;iframe.title=t('Stranica u YU mreži');iframe.sandbox='allow-same-origin';out.append(iframe);}else{const trust=document.createElement('p');trust.textContent=t('Spoljno porijeklo. Otvorite samo ako vjerujete izdavaču.');const a=document.createElement('a');a.href=target.href;a.textContent=target.href;a.rel='noopener noreferrer nofollow ugc';out.append(trust,a);}sessionStorage.setItem('yu-explorer','1');}catch{line(t('Provjera nije uspjela. Ime nije otvoreno.'));}}
  34 $('#connect-form')?.addEventListener('submit',connect);if($('#connect-form')&&new URLSearchParams(location.search).has('ime'))connect();
  35 const started=()=>Number(sessionStorage.getItem('yu-start')||0);$$('[data-step]').forEach(a=>a.addEventListener('click',()=>{if(!started())sessionStorage.setItem('yu-start',String(Date.now()));const done=JSON.parse(sessionStorage.getItem('yu-steps')||'[]');if(!done.includes(a.dataset.step))done.push(a.dataset.step);sessionStorage.setItem('yu-steps',JSON.stringify(done));}));
  36 if($('[data-countdown]')){const update=()=>{if(!started())return;const elapsed=Math.floor((Date.now()-started())/1000),done=JSON.parse(sessionStorage.getItem('yu-steps')||'[]');$('[data-countdown]').textContent=done.length===6?t('Stigli ste za {n} s',{n:elapsed}):t('Preostalo: {n} s',{n:Math.max(0,60-elapsed)});if(done.length===6&&csrf()&&!sessionStorage.getItem('yu-finished')){sessionStorage.setItem('yu-finished','1');fetch('/dobrodosli/vrijeme',{method:'POST',body:new URLSearchParams({csrf:csrf(),seconds:String(Math.min(3600,elapsed))})});}};update();setInterval(update,1000);}
  37 if($('[data-network]')){
  38  const seen=new Set(),svg=$('.map'),ns='http://www.w3.org/2000/svg';let initialized=false;
  39  const poll=async()=>{try{
  40   const data=await (await fetch('/api/v1/mreza.json')).json(),list=$('#network-events'),names=new Map(data.nodes.map(n=>[n.id,n]));list.replaceChildren();
  41   const last=data.events[0]?.at,quiet=$('[data-network-quiet]');
  42   quiet.hidden=!!last&&Date.now()-last<3600000;quiet.textContent=last?t('Mirno je. Posljednji signal: prije {n} h.',{n:Math.max(1,Math.floor((Date.now()-last)/3600000))}):t('Mirno je. Još nema signala članova.');
  43   for(const event of data.events){const li=document.createElement('li');li.textContent=new Date(event.at).toLocaleTimeString('hr-HR')+' '+(names.get(event.a)?.ime||event.a)+' · '+(names.get(event.b)?.ime||event.b)+': '+t(event.kind);list.append(li);
  44    const key=[event.at,event.a,event.b,event.kind].join(':');if(initialized&&!seen.has(key)&&!reduced.matches){for(const city of [event.a,event.b]){const marker=$(`[data-city="${city}"]`);if(marker){marker.classList.remove('pulse');void marker.getBoundingClientRect();marker.classList.add('pulse');marker.addEventListener('animationend',()=>marker.classList.remove('pulse'),{once:true});}}}seen.add(key);
  45   }
  46   for(const node of data.nodes){const marker=$(`[data-city="${node.id}"]`);if(!marker)continue;const size=node.n>10?10:node.n>5?8:node.n>1?6:node.n?4:2,x=Number(marker.dataset.x),y=Number(marker.dataset.y);marker.classList.toggle('marker',node.n>0);for(const [attr,value]of Object.entries({x:x-size/2,y:y-size/2,width:size,height:size,fill:node.n?'#c8102e':'none'}))marker.setAttribute(attr,value);const label=$(`[data-city-label="${node.id}"]`);if(label){label.textContent=t(node.ime)+' '+node.n;label.hidden=!node.n&&node.jed!=='dij';label.style.display=label.hidden?'none':'';}}
  47   const active=new Set();for(const edge of data.edges){const key=edge.a+':'+edge.b;active.add(key);let path=$(`[data-edge="${key}"]`);if(!path){path=document.createElementNS(ns,'path');path.classList.add('edge');path.dataset.edge=key;svg.insertBefore(path,svg.querySelector('[data-city]')?.parentElement||null);}const a=names.get(edge.a).point,b=names.get(edge.b).point;path.setAttribute('d',`M${a}Q${(a[0]+b[0])/2-20},${(a[1]+b[1])/2-35} ${b}`);path.setAttribute('stroke-width',edge.weight>8?3:edge.weight>3?2:1);}
  48   $$('[data-edge]',svg).forEach(path=>{if(!active.has(path.dataset.edge))path.remove();});initialized=true;$('[data-network]').dataset.ready='true';
  49  }catch{}};
  50  poll();setInterval(poll,20000);document.addEventListener('visibilitychange',()=>{if(!document.hidden)poll();});
  51 }
  52 if($('[data-spy]')){const observer=new IntersectionObserver(entries=>entries.forEach(e=>{if(e.isIntersecting){$$('a[href^="#"]').forEach(a=>a.removeAttribute('aria-current'));$(`a[href="#${e.target.id}"]`)?.setAttribute('aria-current','location');}}),{rootMargin:'-10% 0px -65%'});$$('section[id]').forEach(el=>observer.observe(el));}
  53