From 95ceed0de015c4aa863ce61f77f4ade552861571 Mon Sep 17 00:00:00 2001 From: mae Date: Wed, 4 Mar 2026 22:50:58 -0600 Subject: [PATCH 01/34] cmd/pkgserver: basic web ui --- cmd/pkgserver/main.go | 55 ++++++++++++++++++++ cmd/pkgserver/ui/index.html | 23 +++++++++ cmd/pkgserver/ui/static/_common.scss | 0 cmd/pkgserver/ui/static/dark.css | 6 +++ cmd/pkgserver/ui/static/dark.css.map | 7 +++ cmd/pkgserver/ui/static/dark.scss | 6 +++ cmd/pkgserver/ui/static/favicon.ico | Bin 0 -> 15086 bytes cmd/pkgserver/ui/static/index.js | 4 ++ cmd/pkgserver/ui/static/index.ts | 6 +++ cmd/pkgserver/ui/static/light.css | 6 +++ cmd/pkgserver/ui/static/light.css.map | 7 +++ cmd/pkgserver/ui/static/light.scss | 6 +++ internal/azalea/azalea.bnf | 0 internal/azalea/azalea.go | 69 ++++++++++++++++++++++++++ internal/azalea/generator.go | 36 ++++++++++++++ 15 files changed, 231 insertions(+) create mode 100644 cmd/pkgserver/main.go create mode 100644 cmd/pkgserver/ui/index.html create mode 100644 cmd/pkgserver/ui/static/_common.scss create mode 100644 cmd/pkgserver/ui/static/dark.css create mode 100644 cmd/pkgserver/ui/static/dark.css.map create mode 100644 cmd/pkgserver/ui/static/dark.scss create mode 100644 cmd/pkgserver/ui/static/favicon.ico create mode 100644 cmd/pkgserver/ui/static/index.js create mode 100644 cmd/pkgserver/ui/static/index.ts create mode 100644 cmd/pkgserver/ui/static/light.css create mode 100644 cmd/pkgserver/ui/static/light.css.map create mode 100644 cmd/pkgserver/ui/static/light.scss create mode 100644 internal/azalea/azalea.bnf create mode 100644 internal/azalea/azalea.go create mode 100644 internal/azalea/generator.go diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go new file mode 100644 index 00000000..1cb0738c --- /dev/null +++ b/cmd/pkgserver/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "embed" + "fmt" + "net/http" +) + +//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" +//go:embed ui/* +var content embed.FS + +func serveWebUI(w http.ResponseWriter, r *http.Request) { + fmt.Printf("serveWebUI: %s\n", r.URL.Path) + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-XSS-Protection", "1") + w.Header().Set("X-Frame-Options", "DENY") + + http.ServeFileFS(w, r, content, "ui/index.html") +} +func serveStaticContent(w http.ResponseWriter, r *http.Request) { + fmt.Printf("serveStaticContent: %s\n", r.URL.Path) + switch r.URL.Path { + case "/static/style.css": + darkTheme := r.CookiesNamed("dark_theme") + if len(darkTheme) > 0 && darkTheme[0].Value == "true" { + http.ServeFileFS(w, r, content, "ui/static/dark.css") + } else { + http.ServeFileFS(w, r, content, "ui/static/light.css") + } + break + case "/favicon.ico": + http.ServeFileFS(w, r, content, "ui/static/favicon.ico") + break + case "/static/index.js": + http.ServeFileFS(w, r, content, "ui/static/index.js") + break + default: + http.NotFound(w, r) + + } +} +func serveAPI(w http.ResponseWriter, r *http.Request) { + +} +func main() { + http.HandleFunc("GET /{$}", serveWebUI) + http.HandleFunc("GET /favicon.ico", serveStaticContent) + http.HandleFunc("GET /static/", serveStaticContent) + http.HandleFunc("GET /api/", serveAPI) + http.ListenAndServe(":8067", nil) +} diff --git a/cmd/pkgserver/ui/index.html b/cmd/pkgserver/ui/index.html new file mode 100644 index 00000000..6a832cb1 --- /dev/null +++ b/cmd/pkgserver/ui/index.html @@ -0,0 +1,23 @@ + + + + + + Hakurei PkgServer + + + +

Hakurei PkgServer

+ + + +
StatusNameVersion
+

Showing entries .

+« PreviousNext » + + \ No newline at end of file diff --git a/cmd/pkgserver/ui/static/_common.scss b/cmd/pkgserver/ui/static/_common.scss new file mode 100644 index 00000000..e69de29b diff --git a/cmd/pkgserver/ui/static/dark.css b/cmd/pkgserver/ui/static/dark.css new file mode 100644 index 00000000..8a6ac8ab --- /dev/null +++ b/cmd/pkgserver/ui/static/dark.css @@ -0,0 +1,6 @@ +@use 'common'; +html { + background-color: #2c2c2c; + color: ghostwhite; } + +/*# sourceMappingURL=dark.css.map */ diff --git a/cmd/pkgserver/ui/static/dark.css.map b/cmd/pkgserver/ui/static/dark.css.map new file mode 100644 index 00000000..9db27579 --- /dev/null +++ b/cmd/pkgserver/ui/static/dark.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,aAAa;AAEb,IAAK;EACH,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,UAAU", +"sources": ["dark.scss"], +"names": [], +"file": "dark.css" +} diff --git a/cmd/pkgserver/ui/static/dark.scss b/cmd/pkgserver/ui/static/dark.scss new file mode 100644 index 00000000..8d7ea847 --- /dev/null +++ b/cmd/pkgserver/ui/static/dark.scss @@ -0,0 +1,6 @@ +@use 'common'; + +html { + background-color: #2c2c2c; + color: ghostwhite; +} \ No newline at end of file diff --git a/cmd/pkgserver/ui/static/favicon.ico b/cmd/pkgserver/ui/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cc6750305fea9d207c16502e82256d0e931e09d9 GIT binary patch literal 15086 zcmZQzU}RusFfaho3Jfb$85qnM7#I{3pnL%ahI^_E3<3fWeg+EzLz@``g9ZZwg8>5r zLjnUtoM9ADEd=W88T9Jw862P(6x38Rn^p$Z*E7h}*E7`3n84 zHBa*8%d_?vZbtUU=PNG%r%vEGNp1TnF|VnKLH*QL?cd1m#K*pM$P^R@9+cQgO}!U) z>mR`9UTktJ=1SbArW+_R>%%3-er#^UC-&f&<^P5ThC)j0B}VV6RSe9~GBBaOo}r_@ zo?%&iJ;T2GdWHk_^$c6;>lvDwni#bIU2zloe#0&E!zHI{_}q;w2P%^q8W>WEaR;$# zy1N;;8yguy8yXmwyA>Dv{4V|f&CQH0_4N!q$Yzkj?(AgXt*>W@>1<=&wRVBjACQ@Z$`O!z zo}9Ek`Twp9Kgcdr*D&tL=+`+n8k<IG6?uk17W|MjXXO4#DE^X&!4Z*R^y`g#Vo@7LWNKU{LYfy+FY_|<(z|NFW*J~T8i6obqrgs1iK zc7W`I=_iJMa>C}y|A#h$_4Nz_8yCwQes=*iJt3QM=djuTWwXWJ*Vi-nfZX!^x@!lr zJTCU3jq0GheXzcsK?I}^8=gN!FqN1zf^5gbW0tG_|7T#F+Rq(+c8A^%WI2576I-?Z zx3w~Nf!qN~Q(vyS609rk9$jXf4S=V3t1i;``Ia*|6Ludr$F{1<3k%& zBeAK+CieQA!{bj^>}~4n8CbTiP}oCk9>HeTi!*lrXH4R~+}g?@`u)1Q12#42VxV%b zzMi1~+5K1d8Sg|_hs^(U#pTz_v-W$xUULnsuV-LtZDp}PuwLyMvKn&PA1^!qpE;TL zB*@&a*IcuRF=xd*iOLGJfhr4l9uQEz9I*?^~<-3sk=0QcsRJ$h^*W z)^3m;A1*qrA!G)qjDeNOAU)4d+u;n$Z`a)ZcXhCpfz-V@?|@SsAv+1lAKIw?2bzu) z2&o0-zxsLxczf^V89SWe4>D`nT=AVC_kX`vV@n0r9< zlan^p_|%`;s(lZn4u=2#4`u#w!`1WqHP`NM*WK2Bzvi~``*qg_Z20RH*Y0;0opyY@ z?EC_s9U!?ohs{C#7^#2Pt$0Cvbo}YE%l}6wt#V=RfYG-Pnl_@VMdqK{s=W%P78^Zr zA_F@}yuO|xqQ0J?0fZYH8T>(Q3XnJq|G4F(_3^UH24u65*)Px7{cmbw&;_Y~f58zW zogUk)i4u+=bs&6Yui<87bCB7f`sv0&Jw=c@Y#7vjShq;}#_9!<;5Id=Yy;t&2TlG@ zo4|7q)RqP5tFLEJYiMAI2C=_icS}UJ1I7mB;ie`ARgjt&XY3xq#9=h3Jy&1PUF%8;ay+PpaL@E+)jgdbUiRWsP1TKVqO4JckhVBm23Nr|4;1Y zIt*%Cf4byS4^s!DLE+!l#vlw*|Ln94*7oV586t~7^2qqzMaP#gGmz;|mtFonKW?r7 z3S&^;uBnMZAJqO`zewi6=B4ugKz)qchfKF3>p^GlU#AL6(-v>fJFEi9fzt4znIcao zPGsPFf6)o0KDmF?@)yiqk4{*h2dPKLJ60-Sgy-k0&hydLf%x~2T2_F@aOO=FI`Z(i z)&B_-*i`qdQ2~eBt8@1M8ygwSAD*zf1X7C&gVNu`9`3W>ue&RJx#IcVfex>{f993-#==36(){M?^~;S38n@{|GViV{q?FFiaTDMwmS;IbvO+SLv-#B0jPnRt%%-&B=S>Hr99~=AJPCbxc1HWH)hnEwFH){M@yI5N0 z>s7aZFg?dNYr)gT(~~w&VB*O1zBMWs;SX}x|Nq9UAoIUob^ZGBlJm)1C(QU3&lElL z1=Ss^ke+n}5^JN!sKjGsg=l{*kOyMuj z+Fb|9gW3cKH_E}wi5I8s-h;%^@vhZM+d=Lngx4*SS`CVOnEs7RWV>!3GWCG*LG+x- z{6$Ys+N=PvVffQ!m)~Emy4L)-=I;INx?A7Zt8RZma<>nef#M+a!zDyoT{c^E&Z85S zi$U^t4qL$MNRZp!UU0(Nr`fnfZYoF~A6`0BSmX6M2b4a^ys3iWHx8J9(>};P5MDG> zxc|ci$4n3#hA;0ij6gQ~$8{G!nB1&M{7b*xbhm}^w=I|7_2iUQJBWX6pHVhUAE-YH z62pd<&yk3Qsl!E|+NOOFSudy!3d*Z@kC@Fw7GE`Aa?`i#ZdNd{2gfY`fy%0-vqkqI zoBj5@1Gs$Kx?JHLD4l-3;tGzFZOavQzc_85@#Lh<2W0hE_829=%tWR^^+SC<0~fMd zZ0y%(?ZIgjrWaJ^)z>pvJv?qT7bXUxw=S2v{{Ox`$LA}Mz8|Q3zjxHq{nBp3-=H!c zX7>G~mf-g7!S(7_Kw|GMIDH4{-@aUN;rHtqOQeb>xw*k1U1*t*CHx8IQ z1uMWq{Cn=ojjRX6hK9fYvs1Px;{}H|sQ(6)QE$&X?g6O-)jv~udD1^!cKJ4Eiol)v zdWNvJHfE0(r|rOH%*8$W6G7v5A1*qB`D+$Rg@c>|iZfWf1>)a5Vh%6kK<2{mneDnA zAbEWF|9yJ_n0jP-=4Af%Z`WK<`pNf>T7c>%uOHXlf?;asOci{8_po{6w`*?y?j5!G z4jcD>f6?gyC~Vd(l34^&`{20MH<&w?&Jvs3-N^!<$9Q+a37oz_>R|ZwIfoxG_4w%j z{~4HIdXVW2i)7CKyY3=|ECymv>Eqc33cs&c-N1e5*XJGn&zi*lYuPOEnP0BCuK#x3 zZRfY^?p6CYsfkRTz%w5dM<9J?cIbe|FXl`V^aj<1OXi4z)6n@HIsqVgbo~CjT_8Sp zBg=id?D7*`|ApQ9ps?0>f6?(IvfRP-Y9MhtP}qLH>Usep=l1{PR_*^YC-VJjX=b|A z(#&*dwsmf)$RY(ex5zQZaAAF*O{RDZPp~d5B2p78X!M>yXxBi^{U%9WH-Rr z?=CpLdwJd#9L}IQjx*bJ!TI>n39G-L@feufzFl{V0`(=nU3V)1#W_rzSo+6x_k5V0 z$n^bVmj7E?n6kcIaVHi~}H2&{dqx^sG8kPT`Hr%VT zb|~%!sl9j90^a5%#$BYS`S-|z^V@Z|=x^8DW_-Kuy6f9DH&8j>2=W`q98g;Ma>bS8 zxgwCeLHOlayLTTh*$9Hn8b}N(A3k4k{Qzx506`&`*6VtrHuzOW3bTQZ@4Og+Rxvwx#fJj>RR*ds$2Na8y=_t-FmNz1$TKi}04+aaXJB~1z`)SJ45Awt7?>Ft4uG7_h(rr8fOIe%5CBOq zfN2&6C=F640ba<&a6pCuqF??(0TVj|!vXmQ6(*2*HVq13nt_3V4aA4h91uRpevtS- z1_lN`W{^9X8+gDp;{h2k&2U799pr8X1|KMWh9AQ3;0LK^VC0Yo(@Z?}U>Y1Qe;63R zG{{_VxG^#?fa(7S7$D*Kfbl;leE$DohtQw^2Z!^25FfO7NP&TY!2#?&5DVh};Yilk zGl;dfvgGetrF8DVdi8sArwSgbuV+xAwO>JXVM6jF8l!0{)VZ? zMT7bc3#JKexU|RcKgjJcGmz;Imz;k+KWQI2eIj4auGLCEw=Gxrvv{V+<+c{)35U06 z7(Y90a|~HMj13y!p4iK^8`m89iaBBd*Y}$srC~@P3#K1Lzc_98;LK)q_mf+-Um}m6 zf#g7Ae_NKye`{!9i2ZQM$@R@SM~t=#NX_OYa-cNfh8zZ<{?YA2X7D}-$UJoX;*9O8 zgKL%2U!QZp8rLxOHxHWrpU};@;^7fPsn_Qmp2NhE>C@YFK;w1(FgG0CsD*P*?A2NO zt3R)~8|_-Dm;=gF$mZZ;gVM>&iTo#D95)w#cfs)^Hg&sKD}RB_!Cc#KgfZ`OZJ+Ur zZ&w}Vw=9!uJ+e{b!iP)9hu&0>I5NI*z~ll*4jF^S2rle20EYu8FM#Ia zW=!PU22!(XzC>$h8*AjVQ?|j#ZaBYFA3XN`@~l04&JdJ8nwy#7Wx$=o<|ut-kl(K# zF;Q=BW{?Gq9d2E!kiT$-@SLmr4N>M~K<)*tkLc}WD}8m&{sD+RxsPk&lan@UL2@8` zZl~ULko%sUvUv^S!|>V#lHfH)Ai2+%T*6@DAo|uJ)5(iw3THk%ZuM{e6b1>(~~wRebDVI6la0NVHngG z2hAftKW%$p@odp}P`&ZsnB|v?yA8nWRxa-`{C4x8DMmbj>;#3^ls@jpr>AW$gZT5O z3C(zQ7BWY&XN4SiecqdMsN>c_(>NOy-n&e8L($#(8$T?wM=*j5mSIynV>@ z&%>jLd~#`z;bzeKtPdBRmVo3Bu2=u~{G=^-Z2iDmwe$CnT3&j1%o05I4w_R3&EbI5 z!SJRf(pcvTPH)$N&kyWdtGfB_5wo)(J)rebPfl7HfY{GZ+y39SRK~u(ocAbnw%{x*=Apfz+KE;;=A6~ES3(~u5wPGl!>|Q)uECyu$)qO@N<86mGsON*k(J?5VK>A_$ z_!g}%&raIH=dh1&(RzPlzp==xv-bC%owEJEZjto$^Sku7U*B)CVEG)0l}}IF{Jp%_ z;4sMCb364;?q9FE62yLe*8U5~OiH`2W#y>wh4#ukJNk4JxA!tW^WAS=qWw9=ta5(-jw+ zdq*w%UZ1rx!Dc72*z0o+lR$RE@S%oFf5vwXdOzdfj%O~@*ecT{{sW# z{~rpB|Nnnr{{R1vJlJYb!U7ervItn8@X^W}(!Kjt&O!I(1Z8+`NS| z1?5g`(S8VO6NB>3!S(6{*AYP1`GDGAcBi*$mx20aAiW?A>Z5F4B(0BbBdAPXHdn0p z&<6EucaNC={eIo;|MSzf@3$?Now#v{bmPMl=1X91czw>{?}7Dd%Ry}*m^r7n>43`? zkR2d=`>^SWqnkC(gUkW(U!8OKcX5yYj_0TCo`U!we0i_YkM3@E#Z#L#bU=I%estXG z<(VBi%dYMwn?scW0^&40A>B0L{ zSN0k{x_Qvp3}hAvpWLc_0JM$()V9BS#C+k61IB9}9=Ag2gX~-(R|6{3LH(G`%jF#| z?=b||Z6N&z*Q+M<^>ImVUM%i#`;gf`(7flSC9>;|Z`R!WnC%2n?!I)Ud8 z=T8%|0M)H8&e%P@y2o(uwSC6#&+gDYaC)204v={u3|doh{ea2w^E-7ffb8A5RL=PA z1xIi@Xx&07_#ESn{U%Ru9yEFL;HcHxliRd*f#U1l-l8qn#mw{-8y6jp92EbZ6N(mPTKsteaLhdXifg%jjB24cIvJA> zk8DzJI Date: Thu, 5 Mar 2026 00:32:25 -0600 Subject: [PATCH 02/34] cmd/pkgserver: pagination --- cmd/pkgserver/ui/index.html | 13 ++++--- cmd/pkgserver/ui/static/index.js | 63 ++++++++++++++++++++++++++++++ cmd/pkgserver/ui/static/index.ts | 66 ++++++++++++++++++++++++++++++-- 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/cmd/pkgserver/ui/index.html b/cmd/pkgserver/ui/index.html index 6a832cb1..3ca3fc24 100644 --- a/cmd/pkgserver/ui/index.html +++ b/cmd/pkgserver/ui/index.html @@ -4,20 +4,23 @@ Hakurei PkgServer +

Hakurei PkgServer

+ + + +
StatusNameVersion
+

Showing entries .

+« Previous 1 Next » - - -
StatusNameVersion
-

Showing entries .

-« PreviousNext » +
© Hakurei. Licensed under the MIT license.
\ No newline at end of file diff --git a/cmd/pkgserver/ui/static/index.js b/cmd/pkgserver/ui/static/index.js index 26cdcb85..759c2c27 100644 --- a/cmd/pkgserver/ui/static/index.js +++ b/cmd/pkgserver/ui/static/index.js @@ -1,4 +1,67 @@ +"use strict"; +var PackageEntry = /** @class */ (function () { + function PackageEntry() { + } + return PackageEntry; +}()); +var State = /** @class */ (function () { + function State() { + this.entriesPerPage = 10; + this.currentPage = 1; + this.entryIndex = 0; + this.loadedEntries = []; + } + State.prototype.getEntriesPerPage = function () { + return this.entriesPerPage; + }; + State.prototype.setEntriesPerPage = function (entriesPerPage) { + this.entriesPerPage = entriesPerPage; + this.updateRange(); + }; + State.prototype.getCurrentPage = function () { + return this.currentPage; + }; + State.prototype.setCurrentPage = function (page) { + this.currentPage = page; + document.getElementById("page-number").innerText = String(this.currentPage); + this.updateRange(); + }; + State.prototype.getEntryIndex = function () { + return this.entryIndex; + }; + State.prototype.setEntryIndex = function (entryIndex) { + this.entryIndex = entryIndex; + this.updateRange(); + }; + State.prototype.getLoadedEntries = function () { + return this.loadedEntries; + }; + State.prototype.getMaxPage = function () { + return this.loadedEntries.length / this.entriesPerPage; + }; + State.prototype.updateRange = function () { + var max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length); + document.getElementById("entry-counter").innerText = "".concat(this.entryIndex, "-").concat(max, " of ").concat(this.loadedEntries.length); + }; + return State; +}()); +var STATE; function prevPage() { + var current = STATE.getCurrentPage(); + if (current > 1) { + STATE.setCurrentPage(STATE.getCurrentPage() - 1); + } } function nextPage() { + var current = STATE.getCurrentPage(); + if (current < STATE.getMaxPage()) { + STATE.setCurrentPage(STATE.getCurrentPage() + 1); + } } +document.addEventListener("DOMContentLoaded", function () { + STATE = new State(); + STATE.updateRange(); + document.getElementById("count").addEventListener("change", function (event) { + STATE.setEntriesPerPage(parseInt(event.target.value)); + }); +}); diff --git a/cmd/pkgserver/ui/static/index.ts b/cmd/pkgserver/ui/static/index.ts index 1253812c..0301df01 100644 --- a/cmd/pkgserver/ui/static/index.ts +++ b/cmd/pkgserver/ui/static/index.ts @@ -1,6 +1,66 @@ -function prevPage() { +"use strict" + +class PackageEntry { } -function nextPage() { +class State { + entriesPerPage: number = 10 + currentPage: number = 1 + entryIndex: number = 0 + loadedEntries: PackageEntry[] = [] + getEntriesPerPage(): number { + return this.entriesPerPage + } + setEntriesPerPage(entriesPerPage: number) { + this.entriesPerPage = entriesPerPage + this.updateRange() + } + getCurrentPage(): number { + return this.currentPage + } + setCurrentPage(page: number) { + this.currentPage = page + document.getElementById("page-number").innerText = String(this.currentPage) + this.updateRange() + } + getEntryIndex(): number { + return this.entryIndex + } + setEntryIndex(entryIndex: number) { + this.entryIndex = entryIndex + this.updateRange() + } + getLoadedEntries(): PackageEntry[] { + return this.loadedEntries + } + getMaxPage(): number { + return this.loadedEntries.length / this.entriesPerPage + } + updateRange() { + let max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length) + document.getElementById("entry-counter").innerText = `${this.entryIndex}-${max} of ${this.loadedEntries.length}` + } +} -} \ No newline at end of file +let STATE: State + +function prevPage() { + let current = STATE.getCurrentPage() + if (current > 1) { + STATE.setCurrentPage(STATE.getCurrentPage() - 1) + } +} +function nextPage() { + let current = STATE.getCurrentPage() + if (current < STATE.getMaxPage()) { + STATE.setCurrentPage(STATE.getCurrentPage() + 1) + } +} + +document.addEventListener("DOMContentLoaded", () => { + STATE = new State() + STATE.updateRange() + document.getElementById("count").addEventListener("change", (event) => { + STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value)) + }) +}) \ No newline at end of file -- 2.51.2 From d6af8edb4a2faa452c88caa6a25144e7d8dc6a72 Mon Sep 17 00:00:00 2001 From: mae Date: Thu, 5 Mar 2026 01:12:17 -0600 Subject: [PATCH 03/34] cmd/pkgserver: replace favicon --- cmd/pkgserver/ui/static/favicon.ico | Bin 15086 -> 16958 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cmd/pkgserver/ui/static/favicon.ico b/cmd/pkgserver/ui/static/favicon.ico index cc6750305fea9d207c16502e82256d0e931e09d9..2fefdfd7efe2321bbb1cb85a6e7c308522ba088f 100644 GIT binary patch literal 16958 zcmZQzU}RuqaBu+83Je-f3=Con3=A3!3=9qo3=9nn5OIc4Fla(RQc{vZT3VV*N=k}R zN=k}ZLPCOR&^V3`4v~_QS}!Ff^&bg;kdl%DC0RPyKJZpZNlEQN@;fXX{0H7;^l^cd zlvEj-|6fZ=O0v+$_JOxWN=nKG&Ht+g-c|H-fs~XKADaJX(9ib4VUv`U)HkFsm@qh8 zML$_ ze&Ah2dlyJaNrBqDaZ*xJlhOQtoA$QT*9IvmDQ;*xcdnF_RFIUEl&+MN)CDwugVH_- z|C5rE@&}a*^mQd|Y=frny*T^|@-Hs@SV~Ijpp=wUwUm^UIc;1=dsC&Pq{7MYJ9?P> zkdl(Bl9G~groD@)YlD=Ol(3YPl(LkRR2mh+0hBicsOv)dnImI3gt_RUjoL z#YsPxQ^zJLDXBmz_#c)xqNSvyWU1pa+M6OJC8a1OC3TyM{s)D_S7`gmUP?+z666ce zcoK*nOc)eTQc_Y+sO)!;|B>-CDJiK-Qc_Z%rKF^qq@<)~Nl8hW4u)`$l9JjDe~|^+BYhqz(;0Iw+Nrl41wN0|?8=$binL zNlBfeFbtkbNl7h`l9DO}&5t1GZ@PJmNQw&xF*1#??V<2tZ+4I#7PZXRnx;7=x6QRK1jx6sX<@ zm0N`TM2fs8be!RZl#~?R`gQpHi!LW6C1nRqr=<7`m)e(TW@SoANi{&lK>1?O&521# zNlk~k50{@v5$}PT1&VKwKF}DW1GG*B^=Evc^P`fWwkCR934gUpwblB$K~2~Q{= zR3C!WPtQQtmVxS4P}%^EYuOOxAUZ38#ucaygzP6` z*`R$PpfymS@mtW`K4@Lo4rp5tW(Fuslv3@WQYX_Q#MgYq&czCmk6LFpURj15DNsA^yp)vGIVmZrby8AN zl~PhtAUPjU-+@|oj+!$X0=PopKLY%*hp>OhF&D`Tu{Ay&(EO$lUY)|Nn1b0CWEzfVcxB4hkWVdZ_!Dp)}Y- zAaRg~Ks3mMAoD;phz}D7dGHSd0~3^HbN>&vmjy(F%HE*XU;#1* l#0N?LKfv4wrXPSp<^TWx2heDk_ye%>{{R00l?Q8r5dcr}!IuC4 literal 15086 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 Date: Sun, 8 Mar 2026 22:28:08 -0500 Subject: [PATCH 04/34] cmd/pkgserver: add command handler --- cmd/pkgserver/main.go | 80 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index 1cb0738c..afbeaca2 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -1,9 +1,20 @@ package main import ( + "context" "embed" "fmt" + "log" "net/http" + "os" + "os/signal" + "syscall" + + "hakurei.app/command" + "hakurei.app/container/check" + "hakurei.app/internal/pkg" + "hakurei.app/internal/rosa" + "hakurei.app/message" ) //go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" @@ -43,13 +54,70 @@ func serveStaticContent(w http.ResponseWriter, r *http.Request) { } } -func serveAPI(w http.ResponseWriter, r *http.Request) { +func serveAPI(pi *PackageIndex) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} +type SortOrders int + +const ( + NameAscending SortOrders = iota + NameDescending +) + +type PackageIndex struct { + sorts [][]*pkg.Artifact +} + +func createPackageIndex(cache *pkg.Cache, report *rosa.Report) *PackageIndex { + return &PackageIndex{} } func main() { - http.HandleFunc("GET /{$}", serveWebUI) - http.HandleFunc("GET /favicon.ico", serveStaticContent) - http.HandleFunc("GET /static/", serveStaticContent) - http.HandleFunc("GET /api/", serveAPI) - http.ListenAndServe(":8067", nil) + log.SetFlags(0) + log.SetPrefix("pkgserver: ") + + var ( + flagBaseDir string + flagPort uint16 + ) + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + defer stop() + msg := message.New(log.Default()) + + c := command.New(os.Stderr, log.Printf, "pkgserver", func(args []string) error { + reportPath := args[0] + baseDir, err := check.NewAbs(flagBaseDir) + if err != nil { + return err + } + cache, err := pkg.Open(ctx, msg, 0, baseDir) + if err != nil { + return err + } + report, err := rosa.OpenReport(reportPath) + if err != nil { + return err + } + pi := createPackageIndex(cache, report) + + http.HandleFunc("GET /{$}", serveWebUI) + http.HandleFunc("GET /favicon.ico", serveStaticContent) + http.HandleFunc("GET /static/", serveStaticContent) + http.HandleFunc("GET /api/", serveAPI(pi)) + http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) + return nil + }).Flag( + &flagBaseDir, + "b", command.StringFlag(""), + "base directory for cache", + ).Flag( + &flagPort, + "p", command.IntFlag(8067), + "http listen port", + ) + c.MustParse(os.Args[1:], func(e error) { + log.Fatal(e) + }) + } -- 2.51.2 From 75133e0234196bedd99dfd5999889505fea6ea1c Mon Sep 17 00:00:00 2001 From: mae Date: Mon, 9 Mar 2026 01:27:46 -0500 Subject: [PATCH 05/34] cmd/pkgserver: add createPackageIndex --- cmd/pkgserver/main.go | 67 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index afbeaca2..de9734f2 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -1,6 +1,7 @@ package main import ( + "cmp" "context" "embed" "fmt" @@ -8,7 +9,9 @@ import ( "net/http" "os" "os/signal" + "slices" "syscall" + "unique" "hakurei.app/command" "hakurei.app/container/check" @@ -61,16 +64,61 @@ func serveAPI(pi *PackageIndex) func(w http.ResponseWriter, r *http.Request) { type SortOrders int const ( - NameAscending SortOrders = iota + DeclarationDescending SortOrders = iota + DeclarationAscending + NameAscending NameDescending + limitSortOrders ) -type PackageIndex struct { - sorts [][]*pkg.Artifact +type PackageIndex [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry + +type PackageIndexEntry struct { + id unique.Handle[pkg.ID] + name string + description string + website string + version string + status []byte } func createPackageIndex(cache *pkg.Cache, report *rosa.Report) *PackageIndex { - return &PackageIndex{} + var index PackageIndex + var work []PackageIndexEntry + for p := range rosa.PresetUnexportedStart { + m := rosa.GetMetadata(p) + v := rosa.Std.Version(p) + a := rosa.Std.Load(p) + id := cache.Ident(a) + status, n := report.ArtifactOf(id) + work[p] = PackageIndexEntry{ + id: id, + name: m.Name, + description: m.Description, + website: m.Website, + version: v, + status: status[:n], + } + } + for i, p := range work { + index[DeclarationAscending][i] = &p + } + slices.Reverse(work) + for i, p := range work { + index[DeclarationDescending][i] = &p + } + slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int { + return cmp.Compare(a.name, b.name) + }) + for i, p := range work { + index[NameAscending][i] = &p + } + slices.Reverse(work) + for i, p := range work { + index[NameDescending][i] = &p + } + + return &index } func main() { log.SetFlags(0) @@ -99,13 +147,16 @@ func main() { if err != nil { return err } - pi := createPackageIndex(cache, report) - + defer report.HandleAccess(&err)() + index := createPackageIndex(cache, report) http.HandleFunc("GET /{$}", serveWebUI) http.HandleFunc("GET /favicon.ico", serveStaticContent) http.HandleFunc("GET /static/", serveStaticContent) - http.HandleFunc("GET /api/", serveAPI(pi)) - http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) + http.HandleFunc("GET /api/", serveAPI(index)) + err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) + if err != nil { + return err + } return nil }).Flag( &flagBaseDir, -- 2.51.2 From 2e5ac56bdf6e36e41a958fe93b6d8cf8f7d1ea92 Mon Sep 17 00:00:00 2001 From: mae Date: Mon, 9 Mar 2026 04:09:18 -0500 Subject: [PATCH 06/34] cmd/pkgserver: add status endpoint --- cmd/pkgserver/main.go | 117 +++++++++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 31 deletions(-) diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index de9734f2..8ae00c9d 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -1,17 +1,20 @@ package main import ( + "bytes" "cmp" "context" "embed" "fmt" + "io" "log" "net/http" "os" "os/signal" + "path" "slices" + "strings" "syscall" - "unique" "hakurei.app/command" "hakurei.app/container/check" @@ -57,68 +60,112 @@ func serveStaticContent(w http.ResponseWriter, r *http.Request) { } } -func serveAPI(pi *PackageIndex) func(w http.ResponseWriter, r *http.Request) { +func serveAPI(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {} } +func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if index == nil { + http.Error(w, "index is nil", http.StatusInternalServerError) + return + } + base := path.Base(r.URL.Path) + name := strings.TrimSuffix(base, ".log") + p, ok := rosa.ResolveName(name) + if !ok { + http.NotFound(w, r) + return + } + m := rosa.GetMetadata(p) + pk, ok := index.names[m.Name] + if !ok { + http.NotFound(w, r) + return + } + if len(pk.status) > 0 { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.WriteHeader(http.StatusOK) + _, err := io.Copy(w, bytes.NewReader(pk.status)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.NotFound(w, r) + } + } +} + type SortOrders int const ( - DeclarationDescending SortOrders = iota - DeclarationAscending + DeclarationAscending SortOrders = iota + DeclarationDescending NameAscending NameDescending limitSortOrders ) -type PackageIndex [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry +type PackageIndex struct { + sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry + names map[string]*PackageIndexEntry +} type PackageIndexEntry struct { - id unique.Handle[pkg.ID] - name string - description string - website string - version string + Name string `json:"name"` + Description string `json:"description"` + Website string `json:"website"` + Version string `json:"version"` status []byte } -func createPackageIndex(cache *pkg.Cache, report *rosa.Report) *PackageIndex { - var index PackageIndex - var work []PackageIndexEntry +func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { + index := new(PackageIndex) + index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart) + work := make([]PackageIndexEntry, rosa.PresetUnexportedStart) + defer report.HandleAccess(&err)() for p := range rosa.PresetUnexportedStart { m := rosa.GetMetadata(p) v := rosa.Std.Version(p) a := rosa.Std.Load(p) id := cache.Ident(a) - status, n := report.ArtifactOf(id) - work[p] = PackageIndexEntry{ - id: id, - name: m.Name, - description: m.Description, - website: m.Website, - version: v, - status: status[:n], + st, n := report.ArtifactOf(id) + var status []byte + if n < 1 { + status = nil + } else { + status = st } + log.Printf("Processing package %s...\n", m.Name) + entry := PackageIndexEntry{ + Name: m.Name, + Description: m.Description, + Website: m.Website, + Version: v, + status: status, + } + work[p] = entry + index.names[m.Name] = &entry } for i, p := range work { - index[DeclarationAscending][i] = &p + index.sorts[DeclarationAscending][i] = &p } slices.Reverse(work) for i, p := range work { - index[DeclarationDescending][i] = &p + index.sorts[DeclarationDescending][i] = &p } slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int { - return cmp.Compare(a.name, b.name) + return cmp.Compare(a.Name, b.Name) }) for i, p := range work { - index[NameAscending][i] = &p + index.sorts[NameAscending][i] = &p } slices.Reverse(work) for i, p := range work { - index[NameDescending][i] = &p + index.sorts[NameDescending][i] = &p } - - return &index + return index, err } func main() { log.SetFlags(0) @@ -126,7 +173,7 @@ func main() { var ( flagBaseDir string - flagPort uint16 + flagPort int ) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) @@ -139,6 +186,7 @@ func main() { if err != nil { return err } + log.Println("baseDir:", baseDir) cache, err := pkg.Open(ctx, msg, 0, baseDir) if err != nil { return err @@ -147,12 +195,19 @@ func main() { if err != nil { return err } - defer report.HandleAccess(&err)() - index := createPackageIndex(cache, report) + log.Println("reportPath:", reportPath) + log.Println("indexing packages...") + index, err := createPackageIndex(cache, report) + if err != nil { + return err + } + log.Println("created package index") http.HandleFunc("GET /{$}", serveWebUI) http.HandleFunc("GET /favicon.ico", serveStaticContent) http.HandleFunc("GET /static/", serveStaticContent) http.HandleFunc("GET /api/", serveAPI(index)) + http.HandleFunc("GET /api/status/", serveStatus(index)) + log.Println("listening on", flagPort) err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) if err != nil { return err -- 2.51.2 From 92a90582bbad52be3e415e6094d9b69c31cbcc46 Mon Sep 17 00:00:00 2001 From: mae Date: Mon, 9 Mar 2026 15:41:21 -0500 Subject: [PATCH 07/34] cmd/pkgserver: add count endpoint and restructure --- cmd/pkgserver/api.go | 59 +++++++++++++++ cmd/pkgserver/index.go | 79 ++++++++++++++++++++ cmd/pkgserver/main.go | 163 +---------------------------------------- cmd/pkgserver/ui.go | 48 ++++++++++++ 4 files changed, 188 insertions(+), 161 deletions(-) create mode 100644 cmd/pkgserver/api.go create mode 100644 cmd/pkgserver/index.go create mode 100644 cmd/pkgserver/ui.go diff --git a/cmd/pkgserver/api.go b/cmd/pkgserver/api.go new file mode 100644 index 00000000..30496046 --- /dev/null +++ b/cmd/pkgserver/api.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "io" + "net/http" + "path" + "strconv" + "strings" + + "hakurei.app/internal/rosa" +) + +func serveCount(index *PackageIndex) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + count := len(index.names) + w.Write([]byte(strconv.Itoa(count))) + } +} + +func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if index == nil { + http.Error(w, "index is nil", http.StatusInternalServerError) + return + } + base := path.Base(r.URL.Path) + name := strings.TrimSuffix(base, ".log") + p, ok := rosa.ResolveName(name) + if !ok { + http.NotFound(w, r) + return + } + m := rosa.GetMetadata(p) + pk, ok := index.names[m.Name] + if !ok { + http.NotFound(w, r) + return + } + if len(pk.status) > 0 { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.WriteHeader(http.StatusOK) + _, err := io.Copy(w, bytes.NewReader(pk.status)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + http.NotFound(w, r) + } + } +} + +func apiRoutes(index *PackageIndex) { + http.HandleFunc("GET /api/count", serveCount(index)) + http.HandleFunc("GET /api/status/", serveStatus(index)) +} diff --git a/cmd/pkgserver/index.go b/cmd/pkgserver/index.go new file mode 100644 index 00000000..46ee7137 --- /dev/null +++ b/cmd/pkgserver/index.go @@ -0,0 +1,79 @@ +package main + +import ( + "cmp" + "slices" + + "hakurei.app/internal/pkg" + "hakurei.app/internal/rosa" +) + +type SortOrders int + +const ( + DeclarationAscending SortOrders = iota + DeclarationDescending + NameAscending + NameDescending + limitSortOrders +) + +type PackageIndex struct { + sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry + names map[string]*PackageIndexEntry +} + +type PackageIndexEntry struct { + Name string `json:"name"` + Description string `json:"description"` + Website string `json:"website"` + Version string `json:"version"` + status []byte +} + +func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { + index := new(PackageIndex) + index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart) + work := make([]PackageIndexEntry, rosa.PresetUnexportedStart) + defer report.HandleAccess(&err)() + for p := range rosa.PresetUnexportedStart { + m := rosa.GetMetadata(p) + v := rosa.Std.Version(p) + a := rosa.Std.Load(p) + id := cache.Ident(a) + st, n := report.ArtifactOf(id) + var status []byte + if n < 1 { + status = nil + } else { + status = st + } + entry := PackageIndexEntry{ + Name: m.Name, + Description: m.Description, + Website: m.Website, + Version: v, + status: status, + } + work[p] = entry + index.names[m.Name] = &entry + } + for i, p := range work { + index.sorts[DeclarationAscending][i] = &p + } + slices.Reverse(work) + for i, p := range work { + index.sorts[DeclarationDescending][i] = &p + } + slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int { + return cmp.Compare(a.Name, b.Name) + }) + for i, p := range work { + index.sorts[NameAscending][i] = &p + } + slices.Reverse(work) + for i, p := range work { + index.sorts[NameDescending][i] = &p + } + return index, err +} diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index 8ae00c9d..070a21e7 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -1,19 +1,12 @@ package main import ( - "bytes" - "cmp" "context" - "embed" "fmt" - "io" "log" "net/http" "os" "os/signal" - "path" - "slices" - "strings" "syscall" "hakurei.app/command" @@ -23,150 +16,6 @@ import ( "hakurei.app/message" ) -//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" -//go:embed ui/* -var content embed.FS - -func serveWebUI(w http.ResponseWriter, r *http.Request) { - fmt.Printf("serveWebUI: %s\n", r.URL.Path) - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - w.Header().Set("X-Content-Type-Options", "nosniff") - w.Header().Set("X-XSS-Protection", "1") - w.Header().Set("X-Frame-Options", "DENY") - - http.ServeFileFS(w, r, content, "ui/index.html") -} -func serveStaticContent(w http.ResponseWriter, r *http.Request) { - fmt.Printf("serveStaticContent: %s\n", r.URL.Path) - switch r.URL.Path { - case "/static/style.css": - darkTheme := r.CookiesNamed("dark_theme") - if len(darkTheme) > 0 && darkTheme[0].Value == "true" { - http.ServeFileFS(w, r, content, "ui/static/dark.css") - } else { - http.ServeFileFS(w, r, content, "ui/static/light.css") - } - break - case "/favicon.ico": - http.ServeFileFS(w, r, content, "ui/static/favicon.ico") - break - case "/static/index.js": - http.ServeFileFS(w, r, content, "ui/static/index.js") - break - default: - http.NotFound(w, r) - - } -} -func serveAPI(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) {} -} - -func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if index == nil { - http.Error(w, "index is nil", http.StatusInternalServerError) - return - } - base := path.Base(r.URL.Path) - name := strings.TrimSuffix(base, ".log") - p, ok := rosa.ResolveName(name) - if !ok { - http.NotFound(w, r) - return - } - m := rosa.GetMetadata(p) - pk, ok := index.names[m.Name] - if !ok { - http.NotFound(w, r) - return - } - if len(pk.status) > 0 { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.WriteHeader(http.StatusOK) - _, err := io.Copy(w, bytes.NewReader(pk.status)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - http.NotFound(w, r) - } - } -} - -type SortOrders int - -const ( - DeclarationAscending SortOrders = iota - DeclarationDescending - NameAscending - NameDescending - limitSortOrders -) - -type PackageIndex struct { - sorts [limitSortOrders][rosa.PresetUnexportedStart]*PackageIndexEntry - names map[string]*PackageIndexEntry -} - -type PackageIndexEntry struct { - Name string `json:"name"` - Description string `json:"description"` - Website string `json:"website"` - Version string `json:"version"` - status []byte -} - -func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { - index := new(PackageIndex) - index.names = make(map[string]*PackageIndexEntry, rosa.PresetUnexportedStart) - work := make([]PackageIndexEntry, rosa.PresetUnexportedStart) - defer report.HandleAccess(&err)() - for p := range rosa.PresetUnexportedStart { - m := rosa.GetMetadata(p) - v := rosa.Std.Version(p) - a := rosa.Std.Load(p) - id := cache.Ident(a) - st, n := report.ArtifactOf(id) - var status []byte - if n < 1 { - status = nil - } else { - status = st - } - log.Printf("Processing package %s...\n", m.Name) - entry := PackageIndexEntry{ - Name: m.Name, - Description: m.Description, - Website: m.Website, - Version: v, - status: status, - } - work[p] = entry - index.names[m.Name] = &entry - } - for i, p := range work { - index.sorts[DeclarationAscending][i] = &p - } - slices.Reverse(work) - for i, p := range work { - index.sorts[DeclarationDescending][i] = &p - } - slices.SortFunc(work, func(a PackageIndexEntry, b PackageIndexEntry) int { - return cmp.Compare(a.Name, b.Name) - }) - for i, p := range work { - index.sorts[NameAscending][i] = &p - } - slices.Reverse(work) - for i, p := range work { - index.sorts[NameDescending][i] = &p - } - return index, err -} func main() { log.SetFlags(0) log.SetPrefix("pkgserver: ") @@ -186,7 +35,6 @@ func main() { if err != nil { return err } - log.Println("baseDir:", baseDir) cache, err := pkg.Open(ctx, msg, 0, baseDir) if err != nil { return err @@ -195,19 +43,12 @@ func main() { if err != nil { return err } - log.Println("reportPath:", reportPath) - log.Println("indexing packages...") index, err := createPackageIndex(cache, report) if err != nil { return err } - log.Println("created package index") - http.HandleFunc("GET /{$}", serveWebUI) - http.HandleFunc("GET /favicon.ico", serveStaticContent) - http.HandleFunc("GET /static/", serveStaticContent) - http.HandleFunc("GET /api/", serveAPI(index)) - http.HandleFunc("GET /api/status/", serveStatus(index)) - log.Println("listening on", flagPort) + uiRoutes() + apiRoutes(index) err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) if err != nil { return err diff --git a/cmd/pkgserver/ui.go b/cmd/pkgserver/ui.go new file mode 100644 index 00000000..0205b679 --- /dev/null +++ b/cmd/pkgserver/ui.go @@ -0,0 +1,48 @@ +package main + +import ( + "embed" + "net/http" +) + +//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" +//go:embed ui/* +var content embed.FS + +func serveWebUI(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-XSS-Protection", "1") + w.Header().Set("X-Frame-Options", "DENY") + + http.ServeFileFS(w, r, content, "ui/index.html") +} +func serveStaticContent(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/static/style.css": + darkTheme := r.CookiesNamed("dark_theme") + if len(darkTheme) > 0 && darkTheme[0].Value == "true" { + http.ServeFileFS(w, r, content, "ui/static/dark.css") + } else { + http.ServeFileFS(w, r, content, "ui/static/light.css") + } + break + case "/favicon.ico": + http.ServeFileFS(w, r, content, "ui/static/favicon.ico") + break + case "/static/index.js": + http.ServeFileFS(w, r, content, "ui/static/index.js") + break + default: + http.NotFound(w, r) + + } +} + +func uiRoutes() { + http.HandleFunc("GET /{$}", serveWebUI) + http.HandleFunc("GET /favicon.ico", serveStaticContent) + http.HandleFunc("GET /static/", serveStaticContent) +} -- 2.51.2 From b3fa0fe2711aeae5915ad0a5a02536a18f7d5044 Mon Sep 17 00:00:00 2001 From: mae Date: Mon, 9 Mar 2026 18:18:51 -0500 Subject: [PATCH 08/34] cmd/pkgserver: add get endpoint --- cmd/pkgserver/api.go | 53 ++++++++++++++++++++++++++++++++++++++++++ cmd/pkgserver/index.go | 12 +++++++--- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/cmd/pkgserver/api.go b/cmd/pkgserver/api.go index 30496046..1c22a27f 100644 --- a/cmd/pkgserver/api.go +++ b/cmd/pkgserver/api.go @@ -2,6 +2,8 @@ package main import ( "bytes" + "encoding/json" + "fmt" "io" "net/http" "path" @@ -53,7 +55,58 @@ func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Reques } } +type GetPayload struct { + Count int `json:"count"` + Values []PackageIndexEntry `json:"values"` +} + +func NewGetPayload(values []*PackageIndexEntry) GetPayload { + count := len(values) + v := make([]PackageIndexEntry, count) + for i, _ := range values { + v[i] = *values[i] + } + return GetPayload{ + Count: count, + Values: v, + } +} + +func serveGet(index *PackageIndex) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + limit, err := strconv.Atoi(q.Get("limit")) + if err != nil || limit > 100 || limit < 1 { + http.Error(w, fmt.Sprintf("limit must be an integer between 1 and 100"), http.StatusBadRequest) + return + } + i, err := strconv.Atoi(q.Get("index")) + if err != nil || i >= len(index.sorts[0]) || i < 0 { + http.Error(w, fmt.Sprintf("index must be an integer between 0 and %d", len(index.sorts[0])-1), http.StatusBadRequest) + return + } + sort, err := strconv.Atoi(q.Get("sort")) + if err != nil || sort >= len(index.sorts) || sort < 0 { + http.Error(w, fmt.Sprintf("sort must be an integer between 0 and %d", len(index.sorts)-1), http.StatusBadRequest) + return + } + values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))] + payload := NewGetPayload(values) + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.WriteHeader(http.StatusOK) + b, err := json.Marshal(payload) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + _, err = bytes.NewBuffer(b).WriteTo(w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } +} func apiRoutes(index *PackageIndex) { http.HandleFunc("GET /api/count", serveCount(index)) + http.HandleFunc("GET /api/get", serveGet(index)) http.HandleFunc("GET /api/status/", serveStatus(index)) } diff --git a/cmd/pkgserver/index.go b/cmd/pkgserver/index.go index 46ee7137..37c1b05f 100644 --- a/cmd/pkgserver/index.go +++ b/cmd/pkgserver/index.go @@ -2,6 +2,7 @@ package main import ( "cmp" + "fmt" "slices" "hakurei.app/internal/pkg" @@ -25,10 +26,11 @@ type PackageIndex struct { type PackageIndexEntry struct { Name string `json:"name"` - Description string `json:"description"` - Website string `json:"website"` + Description string `json:"description,omitempty"` + Website string `json:"website,omitempty"` Version string `json:"version"` - status []byte + Status string `json:"report,omitempty"` + status []byte `json:"-"` } func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { @@ -43,16 +45,20 @@ func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, id := cache.Ident(a) st, n := report.ArtifactOf(id) var status []byte + var statusUrl string if n < 1 { status = nil + statusUrl = "" } else { status = st + statusUrl = fmt.Sprintf("/api/status/%s.log", m.Name) } entry := PackageIndexEntry{ Name: m.Name, Description: m.Description, Website: m.Website, Version: v, + Status: statusUrl, status: status, } work[p] = entry -- 2.51.2 From c39c07d440df6371d8b53bb08039e5f9371cb7ab Mon Sep 17 00:00:00 2001 From: mae Date: Mon, 9 Mar 2026 23:41:16 -0500 Subject: [PATCH 09/34] cmd/pkgserver: api versioning --- cmd/pkgserver/api.go | 56 +++++++++++++++++++---------- cmd/pkgserver/index.go | 2 +- internal/azalea/azalea.bnf | 0 internal/azalea/azalea.go | 69 ------------------------------------ internal/azalea/generator.go | 36 ------------------- 5 files changed, 38 insertions(+), 125 deletions(-) delete mode 100644 internal/azalea/azalea.bnf delete mode 100644 internal/azalea/azalea.go delete mode 100644 internal/azalea/generator.go diff --git a/cmd/pkgserver/api.go b/cmd/pkgserver/api.go index 1c22a27f..8c4ebbbd 100644 --- a/cmd/pkgserver/api.go +++ b/cmd/pkgserver/api.go @@ -10,15 +10,29 @@ import ( "strconv" "strings" + "hakurei.app/internal/info" "hakurei.app/internal/rosa" ) -func serveCount(index *PackageIndex) func(http.ResponseWriter, *http.Request) { +type InfoPayload struct { + Count int `json:"count"` + HakureiVersion string `json:"hakurei_version"` +} + +func NewInfoPayload(index *PackageIndex) InfoPayload { + count := len(index.sorts[0]) + return InfoPayload{ + Count: count, + HakureiVersion: info.Version(), + } +} + +func serveInfo(index *PackageIndex) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - count := len(index.names) - w.Write([]byte(strconv.Itoa(count))) + w.Header().Set("Content-Type", "text/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + WritePayload(w, NewInfoPayload(index)) } } @@ -91,22 +105,26 @@ func serveGet(index *PackageIndex) func(http.ResponseWriter, *http.Request) { return } values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))] - payload := NewGetPayload(values) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.WriteHeader(http.StatusOK) - b, err := json.Marshal(payload) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - _, err = bytes.NewBuffer(b).WriteTo(w) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + WritePayload(w, NewGetPayload(values)) } } + +const ApiVersion = "v1" + func apiRoutes(index *PackageIndex) { - http.HandleFunc("GET /api/count", serveCount(index)) - http.HandleFunc("GET /api/get", serveGet(index)) - http.HandleFunc("GET /api/status/", serveStatus(index)) + http.HandleFunc(fmt.Sprintf("GET /api/%s/info", ApiVersion), serveInfo(index)) + http.HandleFunc(fmt.Sprintf("GET /api/%s/get", ApiVersion), serveGet(index)) + http.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), serveStatus(index)) +} + +func WritePayload(w http.ResponseWriter, payload any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(payload) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } } diff --git a/cmd/pkgserver/index.go b/cmd/pkgserver/index.go index 37c1b05f..29ba371b 100644 --- a/cmd/pkgserver/index.go +++ b/cmd/pkgserver/index.go @@ -51,7 +51,7 @@ func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, statusUrl = "" } else { status = st - statusUrl = fmt.Sprintf("/api/status/%s.log", m.Name) + statusUrl = fmt.Sprintf("/api/%s/status/%s.log", ApiVersion, m.Name) } entry := PackageIndexEntry{ Name: m.Name, diff --git a/internal/azalea/azalea.bnf b/internal/azalea/azalea.bnf deleted file mode 100644 index e69de29b..00000000 diff --git a/internal/azalea/azalea.go b/internal/azalea/azalea.go deleted file mode 100644 index ecf6b7e7..00000000 --- a/internal/azalea/azalea.go +++ /dev/null @@ -1,69 +0,0 @@ -//go:generate gocc -a azalea.bnf -package azalea - -import ( - "io" - "io/fs" - "os" - "path/filepath" - "strconv" - "strings" - - "hakurei.app/container/check" -) - -type Parser struct { - Generator -} - -func NewParser(gen Generator) *Parser { - return &Parser{ - Generator: gen, - } -} -func (p Parser) Initialise() { - -} - -func (p Parser) Consume(ns string, file io.Reader) error { - return nil -} - -// ConsumeDir walks a directory and consumes all Azalea source files within it and all its subdirectories, as long as they end with the .az extension. -func (p Parser) ConsumeDir(dir *check.Absolute) error { - ds := dir.String() - return filepath.WalkDir(ds, func(path string, d fs.DirEntry, err error) (e error) { - if err != nil { - return err - } - if d.IsDir() || !strings.HasSuffix(d.Name(), ".az") { - return - } - rel, e := filepath.Rel(ds, path) - ns := strings.TrimSuffix(rel, ".az") - f, e := os.Open(path) - return p.Consume(ns, f) - }) -} - -// ConsumeAll consumes all provided readers as Azalea source code, each given the namespace `r%d` where `%d` is the index of the reader in the provided arguments. -func (p Parser) ConsumeAll(in ...io.Reader) error { - for i, r := range in { - err := p.Consume("r"+strconv.FormatInt(int64(i), 10), r) - if err != nil { - return err - } - } - return nil -} - -// ConsumeStrings consumes all provided strings as Azalea source code, each given the namespace `s%d` where `%d` is the index of the string in the provided arugments. -func (p Parser) ConsumeStrings(in ...string) error { - for i, s := range in { - err := p.Consume("s"+strconv.FormatInt(int64(i), 10), strings.NewReader(s)) - if err != nil { - return err - } - } - return nil -} diff --git a/internal/azalea/generator.go b/internal/azalea/generator.go deleted file mode 100644 index ebf0cc72..00000000 --- a/internal/azalea/generator.go +++ /dev/null @@ -1,36 +0,0 @@ -package azalea - -import ( - "io" -) - -type Generator interface { - Finalise() (error, io.Writer) -} - -type JsonGenerator struct { - t any -} - -func NewJsonGenerator[T any]() JsonGenerator { - t := new(T) - - return JsonGenerator{ - t, - } -} - -func (j *JsonGenerator) Finalise() (error, io.Writer) { - -} - -type PkgIRGenerator struct { -} - -func NewPkgIRGenerator() PkgIRGenerator { - return PkgIRGenerator{} -} - -func (p *PkgIRGenerator) Finalise() (error, io.Writer) { - -} -- 2.51.2 From 4ac9c7213215073512d5d68419f8a614a5e6de8b Mon Sep 17 00:00:00 2001 From: mae Date: Tue, 10 Mar 2026 03:31:14 -0500 Subject: [PATCH 10/34] cmd/pkgserver: minimum viable frontend --- .gitignore | 1 + cmd/pkgserver/api.go | 1 - cmd/pkgserver/main.go | 2 + cmd/pkgserver/ui.go | 1 - cmd/pkgserver/ui/index.html | 7 +- cmd/pkgserver/ui/static/index.js | 161 ++++++++++++++++++-------- cmd/pkgserver/ui/static/index.ts | 102 ++++++++++++++-- cmd/pkgserver/ui/static/tsconfig.json | 5 + 8 files changed, 217 insertions(+), 63 deletions(-) create mode 100644 cmd/pkgserver/ui/static/tsconfig.json diff --git a/.gitignore b/.gitignore index ed10d54f..47939c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ go.work.sum # go generate /cmd/hakurei/LICENSE +/cmd/pkgserver/.sass-cache /internal/pkg/testdata/testtool /internal/rosa/hakurei_current.tar.gz diff --git a/cmd/pkgserver/api.go b/cmd/pkgserver/api.go index 8c4ebbbd..8409f71d 100644 --- a/cmd/pkgserver/api.go +++ b/cmd/pkgserver/api.go @@ -122,7 +122,6 @@ func WritePayload(w http.ResponseWriter, payload any) { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") - w.WriteHeader(http.StatusOK) err := json.NewEncoder(w).Encode(payload) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index 070a21e7..701f769c 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -16,6 +16,7 @@ import ( "hakurei.app/message" ) +//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc -p ui/static" func main() { log.SetFlags(0) log.SetPrefix("pkgserver: ") @@ -36,6 +37,7 @@ func main() { return err } cache, err := pkg.Open(ctx, msg, 0, baseDir) + defer cache.Close() if err != nil { return err } diff --git a/cmd/pkgserver/ui.go b/cmd/pkgserver/ui.go index 0205b679..2edac77b 100644 --- a/cmd/pkgserver/ui.go +++ b/cmd/pkgserver/ui.go @@ -5,7 +5,6 @@ import ( "net/http" ) -//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc ui/static/index.ts" //go:embed ui/* var content embed.FS diff --git a/cmd/pkgserver/ui/index.html b/cmd/pkgserver/ui/index.html index 3ca3fc24..3b5cc4c6 100644 --- a/cmd/pkgserver/ui/index.html +++ b/cmd/pkgserver/ui/index.html @@ -4,14 +4,13 @@ Hakurei PkgServer -

Hakurei PkgServer

- +
StatusNameVersion
Loading...

Showing entries .

« Previous 1 Next » @@ -22,5 +21,7 @@ -
© Hakurei. Licensed under the MIT license.
+
+

©Hakurei (unknown). Licensed under the MIT license.

+
\ No newline at end of file diff --git a/cmd/pkgserver/ui/static/index.js b/cmd/pkgserver/ui/static/index.js index 759c2c27..e53f8d05 100644 --- a/cmd/pkgserver/ui/static/index.js +++ b/cmd/pkgserver/ui/static/index.js @@ -1,67 +1,136 @@ -"use strict"; -var PackageEntry = /** @class */ (function () { - function PackageEntry() { - } - return PackageEntry; -}()); -var State = /** @class */ (function () { - function State() { - this.entriesPerPage = 10; - this.currentPage = 1; - this.entryIndex = 0; - this.loadedEntries = []; - } - State.prototype.getEntriesPerPage = function () { +class PackageIndexEntry { + name; + description; + website; + version; + report; +} +function toHTML(entry) { + let v = entry.version != null ? `${escapeHtml(entry.version)}` : ""; + let d = entry.description != null ? `

${escapeHtml(entry.description)}

` : ""; + let w = entry.website != null ? `Website` : ""; + let r = entry.report != null ? `Log` : ""; + let row = (document.createElement('tr')); + row.innerHTML = ` +

${escapeHtml(entry.name)} ${v}

+ ${d} + ${w} + ${r} + `; + return row; +} +const API_VERSION = 1; +const ENDPOINT = `/api/v${API_VERSION}`; +class InfoPayload { + count; + hakurei_version; +} +async function infoRequest() { + const res = await fetch(`${ENDPOINT}/info`); + const res_1 = await res.json(); + return res_1; +} +class GetPayload { + count; + values; +} +var SortOrders; +(function (SortOrders) { + SortOrders[SortOrders["DeclarationAscending"] = 0] = "DeclarationAscending"; + SortOrders[SortOrders["DeclarationDescending"] = 1] = "DeclarationDescending"; + SortOrders[SortOrders["NameAscending"] = 2] = "NameAscending"; + SortOrders[SortOrders["NameDescending"] = 3] = "NameDescending"; +})(SortOrders || (SortOrders = {})); +async function getRequest(limit, index, sort) { + const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`); + const res_1 = await res.json(); + return res_1; +} +class State { + entriesPerPage = 10; + currentPage = 1; + entryIndex = 0; + maxEntries = 100; + getEntriesPerPage() { return this.entriesPerPage; - }; - State.prototype.setEntriesPerPage = function (entriesPerPage) { + } + setEntriesPerPage(entriesPerPage) { this.entriesPerPage = entriesPerPage; - this.updateRange(); - }; - State.prototype.getCurrentPage = function () { + if (this.currentPage > this.getMaxPage()) { + this.setCurrentPage(this.getMaxPage()); + } + } + getCurrentPage() { return this.currentPage; - }; - State.prototype.setCurrentPage = function (page) { + } + setCurrentPage(page) { this.currentPage = page; - document.getElementById("page-number").innerText = String(this.currentPage); - this.updateRange(); - }; - State.prototype.getEntryIndex = function () { + this.setEntryIndex((this.getCurrentPage() - 1) * this.getEntriesPerPage()); + document.getElementById("page-number").innerText = String(this.getCurrentPage()); + } + getEntryIndex() { return this.entryIndex; - }; - State.prototype.setEntryIndex = function (entryIndex) { + } + setEntryIndex(entryIndex) { this.entryIndex = entryIndex; this.updateRange(); - }; - State.prototype.getLoadedEntries = function () { - return this.loadedEntries; - }; - State.prototype.getMaxPage = function () { - return this.loadedEntries.length / this.entriesPerPage; - }; - State.prototype.updateRange = function () { - var max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length); - document.getElementById("entry-counter").innerText = "".concat(this.entryIndex, "-").concat(max, " of ").concat(this.loadedEntries.length); - }; - return State; -}()); -var STATE; + this.updateListings(); + } + getMaxEntries() { + return this.maxEntries; + } + setMaxEntries(max) { + this.maxEntries = max; + } + getMaxPage() { + return Math.ceil(this.getMaxEntries() / this.getEntriesPerPage()); + } + updateRange() { + let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries()); + document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}`; + } + updateListings() { + getRequest(this.getEntriesPerPage(), this.entryIndex, 0) + .then(res => { + let table = document.getElementById("pkg-list"); + table.innerHTML = ''; + for (let i = 0; i < res.count; i++) { + table.appendChild(toHTML(res.values[i])); + } + }); + } +} +let STATE; function prevPage() { - var current = STATE.getCurrentPage(); + let current = STATE.getCurrentPage(); if (current > 1) { STATE.setCurrentPage(STATE.getCurrentPage() - 1); } } function nextPage() { - var current = STATE.getCurrentPage(); + let current = STATE.getCurrentPage(); if (current < STATE.getMaxPage()) { STATE.setCurrentPage(STATE.getCurrentPage() + 1); } } -document.addEventListener("DOMContentLoaded", function () { +function escapeHtml(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} +document.addEventListener("DOMContentLoaded", () => { STATE = new State(); - STATE.updateRange(); - document.getElementById("count").addEventListener("change", function (event) { + infoRequest() + .then(res => { + STATE.setMaxEntries(res.count); + document.getElementById("hakurei-version").innerText = res.hakurei_version; + STATE.updateRange(); + STATE.updateListings(); + }); + document.getElementById("count").addEventListener("change", (event) => { STATE.setEntriesPerPage(parseInt(event.target.value)); }); }); diff --git a/cmd/pkgserver/ui/static/index.ts b/cmd/pkgserver/ui/static/index.ts index 0301df01..db790931 100644 --- a/cmd/pkgserver/ui/static/index.ts +++ b/cmd/pkgserver/ui/static/index.ts @@ -1,27 +1,74 @@ -"use strict" +class PackageIndexEntry { + name: string + description: string | null + website: string | null + version: string | null + report: string | null +} +function toHTML(entry: PackageIndexEntry): HTMLTableRowElement { + let v = entry.version != null ? `${escapeHtml(entry.version)}` : "" + let d = entry.description != null ? `

${escapeHtml(entry.description)}

` : "" + let w = entry.website != null ? `Website` : "" + let r = entry.report != null ? `Log` : "" + let row = (document.createElement('tr')) + row.innerHTML = ` +

${escapeHtml(entry.name)} ${v}

+ ${d} + ${w} + ${r} + ` + return row +} -class PackageEntry { +const API_VERSION = 1 +const ENDPOINT = `/api/v${API_VERSION}` +class InfoPayload { + count: number + hakurei_version: string +} +async function infoRequest(): Promise { + const res = await fetch(`${ENDPOINT}/info`) + const res_1 = await res.json() + return res_1 as InfoPayload +} +class GetPayload { + count: number + values: PackageIndexEntry[] +} + +enum SortOrders { + DeclarationAscending = 0, + DeclarationDescending, + NameAscending, + NameDescending +} +async function getRequest(limit: number, index: number, sort: SortOrders): Promise { + const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`) + const res_1 = await res.json() + return res_1 as GetPayload } class State { entriesPerPage: number = 10 currentPage: number = 1 entryIndex: number = 0 - loadedEntries: PackageEntry[] = [] + maxEntries: number = 0 getEntriesPerPage(): number { return this.entriesPerPage } setEntriesPerPage(entriesPerPage: number) { this.entriesPerPage = entriesPerPage - this.updateRange() + if (this.currentPage > this.getMaxPage()) { + this.setCurrentPage(this.getMaxPage()) + } } getCurrentPage(): number { return this.currentPage } setCurrentPage(page: number) { this.currentPage = page - document.getElementById("page-number").innerText = String(this.currentPage) - this.updateRange() + this.setEntryIndex((this.getCurrentPage() - 1) * this.getEntriesPerPage()) + document.getElementById("page-number").innerText = String(this.getCurrentPage()) } getEntryIndex(): number { return this.entryIndex @@ -29,17 +76,32 @@ class State { setEntryIndex(entryIndex: number) { this.entryIndex = entryIndex this.updateRange() + this.updateListings() } - getLoadedEntries(): PackageEntry[] { - return this.loadedEntries + getMaxEntries(): number { + return this.maxEntries + } + setMaxEntries(max: number) { + this.maxEntries = max } getMaxPage(): number { - return this.loadedEntries.length / this.entriesPerPage + return Math.ceil(this.getMaxEntries() / this.getEntriesPerPage()) } updateRange() { - let max = Math.min(this.entryIndex + this.entriesPerPage, this.loadedEntries.length) - document.getElementById("entry-counter").innerText = `${this.entryIndex}-${max} of ${this.loadedEntries.length}` + let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxEntries()) + document.getElementById("entry-counter").innerText = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxEntries()}` } + updateListings() { + getRequest(this.getEntriesPerPage(), this.entryIndex, 0) + .then(res => { + let table = document.getElementById("pkg-list") + table.innerHTML = '' + for(let i = 0; i < res.count; i++) { + table.appendChild(toHTML(res.values[i])) + } + }) + } + } let STATE: State @@ -57,9 +119,25 @@ function nextPage() { } } +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + document.addEventListener("DOMContentLoaded", () => { STATE = new State() - STATE.updateRange() + infoRequest() + .then(res => { + STATE.setMaxEntries(res.count) + document.getElementById("hakurei-version").innerText = res.hakurei_version + STATE.updateRange() + STATE.updateListings() + }) + document.getElementById("count").addEventListener("change", (event) => { STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value)) }) diff --git a/cmd/pkgserver/ui/static/tsconfig.json b/cmd/pkgserver/ui/static/tsconfig.json new file mode 100644 index 00000000..8589a396 --- /dev/null +++ b/cmd/pkgserver/ui/static/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "ES2024" + } +} \ No newline at end of file -- 2.51.2 From 26a346036dcc2688704aa35d87aea365f46a17ea Mon Sep 17 00:00:00 2001 From: mae Date: Tue, 10 Mar 2026 04:24:49 -0500 Subject: [PATCH 11/34] cmd/pkgserver: add /status endpoint --- cmd/pkgserver/api.go | 29 ++++++++++++++++++++++------- cmd/pkgserver/index.go | 21 ++++++++++----------- cmd/pkgserver/main.go | 2 +- cmd/pkgserver/ui/static/index.js | 4 ++-- cmd/pkgserver/ui/static/index.ts | 4 ++-- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/cmd/pkgserver/api.go b/cmd/pkgserver/api.go index 8409f71d..8f673657 100644 --- a/cmd/pkgserver/api.go +++ b/cmd/pkgserver/api.go @@ -8,9 +8,9 @@ import ( "net/http" "path" "strconv" - "strings" "hakurei.app/internal/info" + "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" ) @@ -36,14 +36,14 @@ func serveInfo(index *PackageIndex) func(http.ResponseWriter, *http.Request) { } } -func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Request) { +func serveStatus(index *PackageIndex, cache *pkg.Cache) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { + download := path.Dir(r.URL.Path) == "/status" if index == nil { http.Error(w, "index is nil", http.StatusInternalServerError) return } - base := path.Base(r.URL.Path) - name := strings.TrimSuffix(base, ".log") + name := path.Base(r.URL.Path) p, ok := rosa.ResolveName(name) if !ok { http.NotFound(w, r) @@ -56,7 +56,21 @@ func serveStatus(index *PackageIndex) func(w http.ResponseWriter, r *http.Reques return } if len(pk.status) > 0 { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") + if download { + w.Header().Set("Content-Type", "application/octet-stream") + } else { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + } + if download { + var version string + if pk.Version != "\u0000" { + version = pk.Version + } else { + version = "unknown" + } + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s-%s.log\"", pk.Name, version, pkg.Encode(pk.ident.Value()))) + } + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") w.WriteHeader(http.StatusOK) _, err := io.Copy(w, bytes.NewReader(pk.status)) @@ -111,10 +125,11 @@ func serveGet(index *PackageIndex) func(http.ResponseWriter, *http.Request) { const ApiVersion = "v1" -func apiRoutes(index *PackageIndex) { +func apiRoutes(index *PackageIndex, cache *pkg.Cache) { http.HandleFunc(fmt.Sprintf("GET /api/%s/info", ApiVersion), serveInfo(index)) http.HandleFunc(fmt.Sprintf("GET /api/%s/get", ApiVersion), serveGet(index)) - http.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), serveStatus(index)) + http.HandleFunc(fmt.Sprintf("GET /api/%s/status/", ApiVersion), serveStatus(index, cache)) + http.HandleFunc("GET /status/", serveStatus(index, cache)) } func WritePayload(w http.ResponseWriter, payload any) { diff --git a/cmd/pkgserver/index.go b/cmd/pkgserver/index.go index 29ba371b..3768d08c 100644 --- a/cmd/pkgserver/index.go +++ b/cmd/pkgserver/index.go @@ -2,8 +2,8 @@ package main import ( "cmp" - "fmt" "slices" + "unique" "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" @@ -25,12 +25,13 @@ type PackageIndex struct { } type PackageIndexEntry struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Website string `json:"website,omitempty"` - Version string `json:"version"` - Status string `json:"report,omitempty"` - status []byte `json:"-"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Website string `json:"website,omitempty"` + Version string `json:"version"` + HasReport bool `json:"report,omitempty"` + ident unique.Handle[pkg.ID] `json:"-"` + status []byte `json:"-"` } func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, err error) { @@ -45,20 +46,18 @@ func createPackageIndex(cache *pkg.Cache, report *rosa.Report) (_ *PackageIndex, id := cache.Ident(a) st, n := report.ArtifactOf(id) var status []byte - var statusUrl string if n < 1 { status = nil - statusUrl = "" } else { status = st - statusUrl = fmt.Sprintf("/api/%s/status/%s.log", ApiVersion, m.Name) } entry := PackageIndexEntry{ Name: m.Name, Description: m.Description, Website: m.Website, Version: v, - Status: statusUrl, + HasReport: len(status) > 0, + ident: id, status: status, } work[p] = entry diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index 701f769c..065b1b67 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -50,7 +50,7 @@ func main() { return err } uiRoutes() - apiRoutes(index) + apiRoutes(index, cache) err = http.ListenAndServe(fmt.Sprintf(":%d", flagPort), nil) if err != nil { return err diff --git a/cmd/pkgserver/ui/static/index.js b/cmd/pkgserver/ui/static/index.js index e53f8d05..92ca8776 100644 --- a/cmd/pkgserver/ui/static/index.js +++ b/cmd/pkgserver/ui/static/index.js @@ -9,7 +9,7 @@ function toHTML(entry) { let v = entry.version != null ? `${escapeHtml(entry.version)}` : ""; let d = entry.description != null ? `

${escapeHtml(entry.description)}

` : ""; let w = entry.website != null ? `Website` : ""; - let r = entry.report != null ? `Log` : ""; + let r = entry.report ? `Log (View | Download)` : ""; let row = (document.createElement('tr')); row.innerHTML = `

${escapeHtml(entry.name)} ${v}

@@ -50,7 +50,7 @@ class State { entriesPerPage = 10; currentPage = 1; entryIndex = 0; - maxEntries = 100; + maxEntries = 0; getEntriesPerPage() { return this.entriesPerPage; } diff --git a/cmd/pkgserver/ui/static/index.ts b/cmd/pkgserver/ui/static/index.ts index db790931..18019bc1 100644 --- a/cmd/pkgserver/ui/static/index.ts +++ b/cmd/pkgserver/ui/static/index.ts @@ -3,13 +3,13 @@ class PackageIndexEntry { description: string | null website: string | null version: string | null - report: string | null + report: boolean } function toHTML(entry: PackageIndexEntry): HTMLTableRowElement { let v = entry.version != null ? `${escapeHtml(entry.version)}` : "" let d = entry.description != null ? `

${escapeHtml(entry.description)}

` : "" let w = entry.website != null ? `Website` : "" - let r = entry.report != null ? `Log` : "" + let r = entry.report ? `Log (View | Download)` : "" let row = (document.createElement('tr')) row.innerHTML = `

${escapeHtml(entry.name)} ${v}

-- 2.51.2 From 98ab020160c2efc612773a42a709e5e22c131e4a Mon Sep 17 00:00:00 2001 From: mae Date: Tue, 10 Mar 2026 05:18:55 -0500 Subject: [PATCH 12/34] cmd/pkgserver: add sort orders, change pagination rules --- cmd/pkgserver/ui/index.html | 8 ++++- cmd/pkgserver/ui/static/index.js | 52 +++++++++++++++---------------- cmd/pkgserver/ui/static/index.ts | 53 ++++++++++++++++---------------- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/cmd/pkgserver/ui/index.html b/cmd/pkgserver/ui/index.html index 3b5cc4c6..3cd13a6a 100644 --- a/cmd/pkgserver/ui/index.html +++ b/cmd/pkgserver/ui/index.html @@ -14,12 +14,18 @@

Showing entries .

« Previous 1 Next » - +