@@ -0,0 +1,306 @@
<!DOCTYPE html>
< html lang = "pt-BR" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Ares Next — Tracker P2P< / title >
< style >
: root {
--bg : #0e1117 ; --panel : #161b24 ; --border : #262d3a ;
--text : #e8ecf3 ; --muted : #8b95a7 ; --accent : #4f8cff ;
--accent2 : #4ade80 ; --danger : #f87171 ;
}
* { box-sizing : border-box ; }
body { margin : 0 ; font-family : system-ui , - apple-system , "Segoe UI" , sans-serif ; background : var ( - - bg ) ; color : var ( - - text ) ; min-height : 100 vh ; }
header { border-bottom : 1 px solid var ( - - border ) ; background : var ( - - panel ) ; position : sticky ; top : 0 ; z-index : 10 ; }
. head-inner { max-width : 1000 px ; margin : 0 auto ; padding : 14 px 20 px ; display : flex ; align-items : center ; gap : 16 px ; flex-wrap : wrap ; }
. logo { display : flex ; align-items : center ; gap : 10 px ; font-weight : 700 ; font-size : 18 px ; }
. logo . dot { width : 12 px ; height : 12 px ; border-radius : 50 % ; background : var ( - - accent2 ) ; box-shadow : 0 0 12 px var ( - - accent2 ) ; }
. stats { margin-left : auto ; display : flex ; gap : 8 px ; flex-wrap : wrap ; }
. stat { background : var ( - - bg ) ; border : 1 px solid var ( - - border ) ; border-radius : 8 px ; padding : 4 px 10 px ; font-size : 12 px ; color : var ( - - muted ) ; }
. stat b { color : var ( - - text ) ; font-size : 14 px ; }
main { max-width : 1000 px ; margin : 0 auto ; padding : 24 px 20 px 80 px ; }
. searchbar { display : flex ; gap : 10 px ; flex-wrap : wrap ; margin-bottom : 20 px ; }
input [ type = text ] , select { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; color : var ( - - text ) ; border-radius : 8 px ; padding : 10 px 14 px ; font-size : 14 px ; outline : none ; }
input [ type = text ] : focus , select : focus { border-color : var ( - - accent ) ; }
. searchbar input [ type = text ] { flex : 1 ; min-width : 220 px ; }
button { background : var ( - - accent ) ; color : white ; border : none ; border-radius : 8 px ; padding : 10 px 18 px ; font-size : 14 px ; font-weight : 600 ; cursor : pointer ; }
button : hover { filter : brightness ( 1.1 ) ; }
button . ghost { background : transparent ; border : 1 px solid var ( - - border ) ; color : var ( - - muted ) ; }
button . ghost : hover { color : var ( - - text ) ; border-color : var ( - - muted ) ; }
. filters { display : flex ; gap : 10 px ; flex-wrap : wrap ; margin-bottom : 16 px ; }
. filters select { flex : 1 ; min-width : 140 px ; }
. count { color : var ( - - muted ) ; font-size : 13 px ; margin : 4 px 0 12 px ; }
. list { display : flex ; flex-direction : column ; gap : 10 px ; }
. file { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border-radius : 10 px ; padding : 14 px 16 px ; cursor : pointer ; transition : border-color .15 s , transform .05 s ; }
. file : hover { border-color : var ( - - accent ) ; }
. file . top { display : flex ; align-items : center ; gap : 10 px ; flex-wrap : wrap ; }
. file . name { font-weight : 600 ; font-size : 15 px ; word-break : break-all ; }
. badge { font-size : 11 px ; padding : 2 px 8 px ; border-radius : 99 px ; background : #22304a ; color : var ( - - accent ) ; text-transform : uppercase ; letter-spacing : .03 em ; }
. badge . size { background : #123c2a ; color : var ( - - accent2 ) ; }
. file . desc { color : var ( - - muted ) ; font-size : 13 px ; margin-top : 6 px ; }
. file . meta { color : var ( - - muted ) ; font-size : 12 px ; margin-top : 6 px ; font-family : ui-monospace , monospace ; word-break : break-all ; }
. empty { text-align : center ; color : var ( - - muted ) ; padding : 60 px 0 ; }
. empty b { display : block ; font-size : 18 px ; margin-bottom : 6 px ; color : var ( - - text ) ; }
. toast { position : fixed ; bottom : 24 px ; left : 50 % ; transform : translateX ( -50 % ) ; background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border-radius : 10 px ; padding : 12 px 20 px ; font-size : 14 px ; display : none ; z-index : 100 ; max-width : 90 vw ; }
. toast . show { display : block ; }
. toast . ok { border-color : var ( - - accent2 ) ; }
. toast . err { border-color : var ( - - danger ) ; }
dialog { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border-radius : 12 px ; color : var ( - - text ) ; padding : 24 px ; max-width : 560 px ; width : 90 % ; }
dialog :: backdrop { background : rgba ( 0 , 0 , 0 , .6 ) ; }
dialog h2 { margin : 0 0 16 px ; font-size : 18 px ; word-break : break-word ; }
dialog . row { display : flex ; justify-content : space-between ; gap : 12 px ; padding : 8 px 0 ; border-bottom : 1 px solid var ( - - border ) ; font-size : 13 px ; }
dialog . row span { color : var ( - - muted ) ; flex-shrink : 0 ; }
dialog . row code { font-family : ui-monospace , monospace ; word-break : break-all ; text-align : right ; }
dialog . actions { display : flex ; gap : 10 px ; margin-top : 18 px ; flex-wrap : wrap ; }
. peer { display : inline-block ; background : var ( - - bg ) ; border : 1 px solid var ( - - border ) ; border-radius : 6 px ; padding : 2 px 8 px ; font-size : 12 px ; font-family : ui-monospace , monospace ; margin : 2 px 4 px 2 px 0 ; }
. peers { margin-top : 14 px ; }
. peers . title { font-size : 13 px ; color : var ( - - muted ) ; margin-bottom : 6 px ; }
dialog . actions . copy { background : transparent ; border : 1 px solid var ( - - accent ) ; color : var ( - - accent ) ; }
dialog . actions . copy : hover { color : white ; background : var ( - - accent ) ; }
details { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border-radius : 10 px ; margin-top : 24 px ; padding : 14 px 16 px ; }
details summary { cursor : pointer ; font-weight : 600 ; font-size : 14 px ; }
. form-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 10 px ; margin-top : 14 px ; }
. form-grid label { display : flex ; flex-direction : column ; gap : 4 px ; font-size : 12 px ; color : var ( - - muted ) ; }
. form-grid label . full { grid-column : 1 / -1 ; }
. form-grid input , . form-grid select { background : var ( - - bg ) ; border : 1 px solid var ( - - border ) ; color : var ( - - text ) ; border-radius : 6 px ; padding : 8 px 10 px ; font-size : 13 px ; outline : none ; font-family : inherit ; }
. form-grid input : focus { border-color : var ( - - accent ) ; }
. form-actions { margin-top : 14 px ; display : flex ; justify-content : flex-end ; }
< / style >
< / head >
< body >
< header >
< div class = "head-inner" >
< div class = "logo" > < span class = "dot" > < / span > Ares Next< / div >
< div class = "stats" >
< div class = "stat" > Arquivos < b id = "st-files" > – < / b > < / div >
< div class = "stat" > Peers online < b id = "st-peers" > – < / b > < / div >
< div class = "stat" > Total < b id = "st-bytes" > – < / b > < / div >
< div class = "stat" > v< span id = "st-ver" > – < / span > < / div >
< / div >
< / div >
< / header >
< main >
< div class = "searchbar" >
< input type = "text" id = "q" placeholder = "Buscar por nome, descrição, hash..." autofocus >
< button id = "btn-search" > Buscar< / button >
< button class = "ghost" id = "btn-clear" > Limpar< / button >
< / div >
< div class = "filters" >
< select id = "ftype" >
< option value = "" > Todos os tipos< / option >
< option value = "audio" > Áudio< / option >
< option value = "video" > Vídeo< / option >
< option value = "document" > Documento< / option >
< option value = "software" > Software< / option >
< option value = "other" > Outro< / option >
< / select >
< select id = "sort" >
< option value = "relevance" > Mais relevantes< / option >
< option value = "newest" > Mais recentes< / option >
< option value = "oldest" > Mais antigos< / option >
< option value = "size_desc" > Maior tamanho< / option >
< option value = "size_asc" > Menor tamanho< / option >
< / select >
< select id = "limit" >
< option value = "10" > 10 resultados< / option >
< option value = "25" selected > 25 resultados< / option >
< option value = "50" > 50 resultados< / option >
< option value = "100" > 100 resultados< / option >
< / select >
< / div >
< div class = "count" id = "count" > < / div >
< div class = "list" id = "list" >
< div class = "empty" > < b > Ainda não há arquivos< / b > Busque acima ou anuncie um arquivo para começar.< / div >
< / div >
< details >
< summary > Anunciar arquivo (POST /api/v1/announce)< / summary >
< div class = "form-grid" >
< label class = "full" > Nome *< input id = "f-name" type = "text" placeholder = "ubuntu-24.04.iso" > < / label >
< label class = "full" > Descrição< input id = "f-desc" type = "text" placeholder = "O que é este arquivo?" > < / label >
< label > Tipo
< select id = "f-type" >
< option value = "software" > Software< / option >
< option value = "audio" > Áudio< / option >
< option value = "video" > Vídeo< / option >
< option value = "document" > Documento< / option >
< option value = "other" > Outro< / option >
< / select >
< / label >
< label > Extensão< input id = "f-ext" type = "text" placeholder = "iso" > < / label >
< label > Peer ID *< input id = "f-peer" type = "text" placeholder = "meu-peer-id" > < / label >
< label > Tamanho (bytes) *< input id = "f-size" type = "number" min = "0" placeholder = "734003200" > < / label >
< label class = "full" > Hash SHA-256 / BLAKE3 (64 hex) *< input id = "f-hash" type = "text" maxlength = "64" placeholder = "0123...abc" > < / label >
< label class = "full" > Licença< input id = "f-license" type = "text" placeholder = "MIT" > < / label >
< / div >
< div class = "form-actions" > < button id = "btn-announce" > Anunciar arquivo< / button > < / div >
< / details >
< / main >
< div class = "toast" id = "toast" > < / div >
< dialog id = "dlg" >
< h2 id = "dlg-name" > < / h2 >
< div id = "dlg-body" > < / div >
< div class = "actions" >
< button id = "dlg-close" > Fechar< / button >
< / div >
< / dialog >
< script >
const $ = id => document . getElementById ( id ) ;
const fmtSize = b => {
if ( ! b ) return "0 B" ;
const u = [ "B" , "KB" , "MB" , "GB" , "TB" ] ; let i = 0 ;
while ( b >= 1024 && i < u . length - 1 ) { b /= 1024 ; i ++ ; }
return b . toFixed ( i ? 1 : 0 ) + " " + u [ i ] ;
} ;
const fmtDate = ts => new Date ( ts * 1000 ) . toLocaleString ( "pt-BR" ) ;
const esc = s => String ( s ? ? "" ) . replace ( /[&<>"']/g , c => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ , "'" : "'" } [ c ] ) ) ;
let toastTimer ;
function toast ( msg , ok ) {
const t = $ ( "toast" ) ;
t . textContent = msg ;
t . className = "toast show " + ( ok ? "ok" : "err" ) ;
clearTimeout ( toastTimer ) ;
toastTimer = setTimeout ( ( ) => t . className = "toast" , 4000 ) ;
}
const api = async ( path , opts ) => {
const r = await fetch ( path , opts ) ;
if ( ! r . ok ) {
let msg = "HTTP " + r . status ;
try { const j = await r . json ( ) ; if ( j . error ) msg = j . error ; else if ( j . message ) msg = j . message ; } catch { }
throw new Error ( msg ) ;
}
return r . json ( ) ;
} ;
async function loadStats ( ) {
try {
const s = await api ( "/api/v1/stats" ) ;
$ ( "st-files" ) . textContent = s . files ;
$ ( "st-peers" ) . textContent = s . online _peers ;
$ ( "st-bytes" ) . textContent = fmtSize ( s . total _bytes ) ;
$ ( "st-ver" ) . textContent = s . version ;
} catch { }
}
async function search ( ) {
const q = $ ( "q" ) . value . trim ( ) ;
const params = new URLSearchParams ( ) ;
if ( q ) params . set ( "q" , q ) ;
if ( $ ( "ftype" ) . value ) params . set ( "ftype" , $ ( "ftype" ) . value ) ;
if ( $ ( "sort" ) . value ) params . set ( "sort" , $ ( "sort" ) . value ) ;
params . set ( "limit" , $ ( "limit" ) . value ) ;
const list = $ ( "list" ) ;
list . innerHTML = '<div class="empty"><b>Buscando...</b></div>' ;
try {
const rows = await api ( "/api/v1/search?" + params . toString ( ) ) ;
$ ( "count" ) . textContent = rows . length ? rows . length + " resultado(s)" : "" ;
if ( ! rows . length ) {
list . innerHTML = '<div class="empty"><b>Nenhum resultado</b>Tente outros termos de busca.</div>' ;
return ;
}
list . innerHTML = rows . map ( f => `
<div class="file" data-id=" ${ esc ( f . id ) } ">
<div class="top">
<span class="name"> ${ esc ( f . name ) } </span>
<span class="badge"> ${ esc ( f . ftype ) } </span>
${ f . extension ? '<span class="badge size">.' + esc ( f . extension ) + "</span>" : "" }
<span class="badge size"> ${ fmtSize ( f . size _bytes ) } </span>
</div>
${ f . description ? ` <div class="desc"> ${ esc ( f . description ) } </div> ` : "" }
<div class="meta"> ${ esc ( f . id ) } · ${ fmtDate ( f . created _at ) } </div>
</div> ` ) . join ( "" ) ;
document . querySelectorAll ( ".file" ) . forEach ( el => {
el . addEventListener ( "click" , ( ) => openFile ( el . dataset . id ) ) ;
} ) ;
} catch ( e ) {
list . innerHTML = '<div class="empty"><b>Erro na busca</b>' + esc ( e . message ) + "</div>" ;
}
}
async function openFile ( id ) {
const dlg = $ ( "dlg" ) ;
$ ( "dlg-name" ) . textContent = id ;
$ ( "dlg-body" ) . innerHTML = '<div class="empty">Carregando...</div>' ;
dlg . showModal ( ) ;
try {
const f = await api ( "/api/v1/files/" + encodeURIComponent ( id ) ) ;
$ ( "dlg-name" ) . textContent = f . name ;
let peers = [ ] ;
try { peers = ( await api ( "/api/v1/peers/" + encodeURIComponent ( id ) ) ) . peers ; } catch { }
const row = ( k , v ) => ` <div class="row"><span> ${ k } </span><code> ${ v } </code></div> ` ;
$ ( "dlg-body" ) . innerHTML =
( f . description ? ` <div style="font-size:13px;color:var(--muted);margin-bottom:10px"> ${ esc ( f . description ) } </div> ` : "" ) +
row ( "Peer" , esc ( f . peer _id ) ) +
row ( "Tipo" , esc ( f . ftype ) + ( f . extension ? " · ." + esc ( f . extension ) : "" ) ) +
row ( "Tamanho" , fmtSize ( f . size _bytes ) + " (" + f . size _bytes + " bytes)" ) +
row ( "Licença" , esc ( f . license || "—" ) ) +
row ( "Tags" , esc ( ( f . tags || [ ] ) . join ( ", " ) || "—" ) ) +
row ( "Criado em" , fmtDate ( f . created _at ) ) +
row ( "Hash" , esc ( f . hash ) ) +
` <div class="peers"><div class="title">Peers online ( ${ peers . length } )</div> ` +
( peers . length ? peers . map ( p => ` <span class="peer"> ${ esc ( p ) } </span> ` ) . join ( "" ) : "Nenhum peer online" ) +
"</div>" ;
const actions = $ ( "dlg" ) . querySelector ( ".actions" ) ;
const oldCopy = actions . querySelectorAll ( ".copy" ) ;
oldCopy . forEach ( b => b . remove ( ) ) ;
const copy = document . createElement ( "button" ) ;
copy . className = "copy" ;
copy . textContent = "Copiar hash" ;
copy . addEventListener ( "click" , ( ) => {
navigator . clipboard . writeText ( f . hash ) . then ( ( ) => toast ( "Hash copiado" , true ) ) . catch ( ( ) => toast ( "Falha ao copiar" , false ) ) ;
} ) ;
actions . insertBefore ( copy , $ ( "dlg-close" ) ) ;
} catch ( e ) {
$ ( "dlg-body" ) . innerHTML = '<div class="empty">' + esc ( e . message ) + "</div>" ;
}
}
async function announce ( ) {
const body = {
peer _id : $ ( "f-peer" ) . value . trim ( ) ,
name : $ ( "f-name" ) . value . trim ( ) ,
description : $ ( "f-desc" ) . value . trim ( ) ,
type : $ ( "f-type" ) . value ,
extension : $ ( "f-ext" ) . value . trim ( ) ,
size _bytes : Number ( $ ( "f-size" ) . value ) ,
hash : $ ( "f-hash" ) . value . trim ( ) ,
license : $ ( "f-license" ) . value . trim ( ) ,
tags : [ ]
} ;
if ( ! body . peer _id || ! body . name || ! body . size _bytes || ! body . hash ) {
toast ( "Preencha peer_id, nome, tamanho e hash" , false ) ;
return ;
}
const btn = $ ( "btn-announce" ) ;
btn . disabled = true ;
try {
const r = await api ( "/api/v1/announce" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( body )
} ) ;
toast ( "Anunciado: " + r . file _id , true ) ;
loadStats ( ) ;
search ( ) ;
$ ( "f-name" ) . value = $ ( "f-desc" ) . value = $ ( "f-peer" ) . value = $ ( "f-size" ) . value = $ ( "f-hash" ) . value = $ ( "f-license" ) . value = "" ;
} catch ( e ) {
toast ( e . message , false ) ;
} finally {
btn . disabled = false ;
}
}
$ ( "btn-search" ) . addEventListener ( "click" , search ) ;
$ ( "q" ) . addEventListener ( "keydown" , e => { if ( e . key === "Enter" ) search ( ) ; } ) ;
$ ( "btn-clear" ) . addEventListener ( "click" , ( ) => { $ ( "q" ) . value = "" ; $ ( "ftype" ) . value = "" ; $ ( "sort" ) . value = "relevance" ; search ( ) ; } ) ;
[ "ftype" , "sort" , "limit" ] . forEach ( id => $ ( id ) . addEventListener ( "change" , search ) ) ;
$ ( "btn-announce" ) . addEventListener ( "click" , announce ) ;
$ ( "dlg-close" ) . addEventListener ( "click" , ( ) => $ ( "dlg" ) . close ( ) ) ;
loadStats ( ) ;
search ( ) ;
< / script >
< / body >
< / html >