From b7f354e0812bd14e2d1d14655e97c2fec259c1d4 Mon Sep 17 00:00:00 2001 From: dataforge Date: Mon, 6 Apr 2026 00:57:13 +0200 Subject: [PATCH] init: rapid_dashboards app from fn_registry --- .gitignore | 19 + app.go | 140 ++ build/appicon.png | Bin 0 -> 132625 bytes build/windows/icon.ico | Bin 0 -> 21677 bytes build/windows/info.json | 15 + config.go | 224 +++ connections.go | 62 + docs/ejemplos.md | 525 ++++++ docs/guia.md | 569 ++++++ docs/schema.md | 202 ++ examples/demo_dashboard.yaml | 144 ++ examples/fn_registry_apps.yaml | 202 ++ examples/fn_registry_overview.yaml | 285 +++ filter_engine.go | 40 + frontend/index.html | 12 + frontend/package.json | 36 + frontend/package.json.md5 | 1 + frontend/pnpm-lock.yaml | 1628 +++++++++++++++++ frontend/src/App.tsx | 115 ++ frontend/src/app.css | 176 ++ frontend/src/components/DashboardShell.tsx | 62 + frontend/src/components/FilterBar.tsx | 119 ++ frontend/src/components/Section.tsx | 63 + frontend/src/components/WidgetRenderer.tsx | 93 + frontend/src/components/widget-registry.ts | 18 + .../components/widgets/AreaChartWidget.tsx | 73 + .../src/components/widgets/BarChartWidget.tsx | 79 + frontend/src/components/widgets/KPIWidget.tsx | 81 + .../components/widgets/LineChartWidget.tsx | 66 + .../src/components/widgets/PieChartWidget.tsx | 78 + .../components/widgets/SparklineWidget.tsx | 29 + .../src/components/widgets/TableWidget.tsx | 114 ++ frontend/src/components/widgets/register.ts | 19 + frontend/src/env.d.ts | 18 + frontend/src/hooks/useFilterState.ts | 27 + frontend/src/main.tsx | 10 + frontend/src/types.ts | 74 + frontend/tsconfig.json | 27 + frontend/vite.config.ts | 20 + go.mod | 74 + go.sum | 231 +++ main.go | 147 ++ query_engine.go | 156 ++ scripts/dev.sh | 16 + scripts/prod.sh | 36 + wails.json | 14 + 46 files changed, 6139 insertions(+) create mode 100644 .gitignore create mode 100644 app.go create mode 100644 build/appicon.png create mode 100644 build/windows/icon.ico create mode 100644 build/windows/info.json create mode 100644 config.go create mode 100644 connections.go create mode 100644 docs/ejemplos.md create mode 100644 docs/guia.md create mode 100644 docs/schema.md create mode 100644 examples/demo_dashboard.yaml create mode 100644 examples/fn_registry_apps.yaml create mode 100644 examples/fn_registry_overview.yaml create mode 100644 filter_engine.go create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100755 frontend/package.json.md5 create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/app.css create mode 100644 frontend/src/components/DashboardShell.tsx create mode 100644 frontend/src/components/FilterBar.tsx create mode 100644 frontend/src/components/Section.tsx create mode 100644 frontend/src/components/WidgetRenderer.tsx create mode 100644 frontend/src/components/widget-registry.ts create mode 100644 frontend/src/components/widgets/AreaChartWidget.tsx create mode 100644 frontend/src/components/widgets/BarChartWidget.tsx create mode 100644 frontend/src/components/widgets/KPIWidget.tsx create mode 100644 frontend/src/components/widgets/LineChartWidget.tsx create mode 100644 frontend/src/components/widgets/PieChartWidget.tsx create mode 100644 frontend/src/components/widgets/SparklineWidget.tsx create mode 100644 frontend/src/components/widgets/TableWidget.tsx create mode 100644 frontend/src/components/widgets/register.ts create mode 100644 frontend/src/env.d.ts create mode 100644 frontend/src/hooks/useFilterState.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/types.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 query_engine.go create mode 100755 scripts/dev.sh create mode 100755 scripts/prod.sh create mode 100644 wails.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1b9a2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Binarios compilados +rapid-dashboards +rapid-dashboards.exe +build/bin/ +build/windows/wails.exe.manifest + +# Logs +*.log + +# Frontend +frontend/node_modules/ +frontend/dist/ +frontend/tsconfig.tsbuildinfo + +# Base de datos +*.db + +# Wails +wailsjs/ diff --git a/app.go b/app.go new file mode 100644 index 0000000..f95b089 --- /dev/null +++ b/app.go @@ -0,0 +1,140 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "sync" +) + +// App struct — each public method is an IPC binding to the frontend. +type App struct { + ctx context.Context + config *DashboardConfig + engine *QueryEngine + pool map[string]*sql.DB + dashboardDir string // directory to scan for YAML dashboards + currentFile string // currently loaded YAML file + mu sync.RWMutex +} + +func NewApp(cfg *DashboardConfig, engine *QueryEngine, pool map[string]*sql.DB, dashDir, currentFile string) *App { + return &App{ + config: cfg, + engine: engine, + pool: pool, + dashboardDir: dashDir, + currentFile: currentFile, + } +} + +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// GetDashboardConfig returns the full config for the frontend to render. +func (a *App) GetDashboardConfig() *DashboardConfig { + a.mu.RLock() + defer a.mu.RUnlock() + log.Printf("[IPC] GetDashboardConfig — title=%q sections=%d", a.config.Settings.Title, len(a.config.Sections)) + return a.config +} + +// GetWidgetData executes the query for a widget with current filter values. +func (a *App) GetWidgetData(widgetID string, filters map[string]any) ([]map[string]any, error) { + a.mu.RLock() + defer a.mu.RUnlock() + + rows, err := a.engine.Execute(widgetID, filters) + if err != nil { + log.Printf("[IPC] GetWidgetData ERROR — widget=%q err=%v", widgetID, err) + return nil, fmt.Errorf("widget %q: %w", widgetID, err) + } + log.Printf("[IPC] GetWidgetData OK — widget=%q rows=%d", widgetID, len(rows)) + return rows, nil +} + +// DashboardInfo is a summary of an available dashboard file. +type DashboardInfo struct { + Name string `json:"name"` + File string `json:"file"` + Title string `json:"title"` + Theme string `json:"theme"` + Current bool `json:"current"` +} + +// ListDashboards returns all .yaml files in the dashboards directory. +func (a *App) ListDashboards() []DashboardInfo { + a.mu.RLock() + currentFile := a.currentFile + a.mu.RUnlock() + + var dashboards []DashboardInfo + + entries, err := os.ReadDir(a.dashboardDir) + if err != nil { + log.Printf("[IPC] ListDashboards ERROR — %v", err) + return dashboards + } + + for _, e := range entries { + if e.IsDir() || (!strings.HasSuffix(e.Name(), ".yaml") && !strings.HasSuffix(e.Name(), ".yml")) { + continue + } + + filePath := filepath.Join(a.dashboardDir, e.Name()) + cfg, err := LoadDashboard(filePath) + title := e.Name() + theme := "" + if err == nil { + title = cfg.Settings.Title + theme = cfg.Theme + } + + dashboards = append(dashboards, DashboardInfo{ + Name: strings.TrimSuffix(strings.TrimSuffix(e.Name(), ".yaml"), ".yml"), + File: e.Name(), + Title: title, + Theme: theme, + Current: filePath == currentFile || e.Name() == filepath.Base(currentFile), + }) + } + + log.Printf("[IPC] ListDashboards — found %d", len(dashboards)) + return dashboards +} + +// SwitchDashboard loads a different dashboard YAML by filename. +func (a *App) SwitchDashboard(fileName string) (*DashboardConfig, error) { + filePath := filepath.Join(a.dashboardDir, fileName) + + log.Printf("[IPC] SwitchDashboard — loading %q", filePath) + + cfg, err := LoadDashboard(filePath) + if err != nil { + return nil, fmt.Errorf("loading %q: %w", fileName, err) + } + + newPool, err := OpenConnections(cfg.Connections, filepath.Dir(filePath)) + if err != nil { + return nil, fmt.Errorf("connections for %q: %w", fileName, err) + } + + a.mu.Lock() + // Close old connections + if a.pool != nil { + CloseConnections(a.pool) + } + a.config = cfg + a.pool = newPool + a.engine = NewQueryEngine(cfg, newPool) + a.currentFile = filePath + a.mu.Unlock() + + log.Printf("[IPC] SwitchDashboard OK — title=%q sections=%d", cfg.Settings.Title, len(cfg.Sections)) + return cfg, nil +} diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..63617fe4f746b8a878bd5f44725f4f317b9d9850 GIT binary patch literal 132625 zcmeEuX*^VK*#ALNNmB`xWkj}YMTsyJrI5X3&z?PGkQt*Sm1S&MLdaU#cPUF^?EAj& z>)3~3hME7FQorZL^ZNhlIX*sS&V8SIyRPedt;bspH6?~)=Z^sZz;N%bq80!g0zV!C zjvNO6px6a{Dc`Jb-oFU|d1(4QQ(Evlr}U;MN z^+j&RLvJ{bhP={=#a4|;lH9Vk2czw7@_ z;QvnG|4!il;|bJ!B>T~`h!~bWo%rcTuS;(X&b{^&{ok!>DgdaWk^9>N?0V!lc;^UW zoILjDq2Eszjr;k4k3#l5>M{QfI1ap|0(>UO;|a6A>*R58hJ&eke(LJo1V?_-=qY9ff53C689?q#}wBZwou2(CvJnyz$LFW(Tx{lbH3Z;nty}(eLb38 zSpAZVnmPkq;?)*k)F|ElRcI+0@^6DI*%wOR9cQ*+ILvtx0QmQ-(T6|%=Oa~_b z_T=VytZ^IE7J!S~&Kz>QZDV3!c8BGE3?U}cccZXAQU1fCPvHx&A3v^oY^y3^<|g!i zbh4_bdwv*_f0#D^FwIF&Yo$Ej5uL1-Yw{&MszH@!|Bv%5eLe1yrHQ1bwLC;~^2qO= zc;s5!M*Hf!xXC1re`EIDJ;H49`AjbD?_Osjx7=lSh><(x96Lz4%jq75L*piU0jPCR zQf=i93whFH&tRgm`0JZQAE(OzxJ-FV2+5|A5YDk<1li2?_x^9grLPw^?rH2EKGJ!Z zCjT%sWwj7;8Fa5p21m5mq|0$gSBwAF)csl8JqCs(+!VKJmbB{4)JvcIk8C~1`vT92 z2mfyOwd-w>7_uLVc=7To)eD&yK!PpEFqe@#>RrQa_-eUj1+qk}L-BxwcDmY#WnJJo zjLcdU4a<-9I@S^)lZw5CBD=ndv97L7Hz%hYX7GH$OaIO2sPPZ)eL~yA%tz~sVLU1; zv2rLi+$YEyJRZK^o z2=brw0S{h(zM72kjM$s=UNiabu^wN)P4jzn4Ti4|v!07vasoHOA=he9E16{6LKj3o z-=YEOu}1g1)8lz+ssigaSV7|NLXx%wIdbRm9w55{I6@7y|DpoM z{882oMv%sTcZtjXtKgKtAyve?cdbuS0Wj zi&p+5;B#tit+Iu3qQ8t$zk30%>6#*iy{>k$Ru%I6-g0LR`LRC=p-BC^{qpxm;2mtzndv^Lp&$tnS9hdTqx@PW~C(C$u5ovWesV z5$gWnBZ}baF9_|ED_;OU>a(cW-OV|Zcm4;PxFeB(BJI*gGE#RQqH2eLJ^S&iJDj*H^jUof;?_|Jd25OkDWvBDh5h`qe86UYVPSXBMa;)Z&W9Yt z2>>K2pF$(DIgteQ7=ID4)f{S~NZdwf)RPW~^&t)f+8}W2jW(pYX3}p;vJnrCD~nFqiJ$Ll0)TugmK;H0Dt|QlpU0{+PJX7I zSJtHU0SUBOd9{U{2I?F7eDa@WpCk(kdw)8|2`o?ppPR^YZ3!p=nB$*T;4n#;t1@Gf zC5wU()Uj05J{u_Q&B_N~AO3jTcI{vY?jssUyVBL=_!Y$E;|>FN5CG@ry~@!&NP_i2 z538z<<0ppzfP-{hWo%!rdv7)9`e-RFRQzxI<9)(b!ntCqz*-5)E~sY6c754M=zkqd zDJqeRONc_xG$64;bf&{HeB}F~1=lwxP;K{>wI>5*5Jm46x|CcUShru!UACrHpjh~nUzD|tsoC><`coTHQ zfe>-vve!0NeBJr)FjkSGCZuPt@Aw=6kEsu6KL`8XiPjZs0fa{+3B1luA%se)&T{)te@gS3GdpZL!;E?aJS! z)7mOgIToA?rap88NPz&V6pTXz9xO=zD&Uh0P*DRFR6c)1?bHF?1am#sC#At}M*xfC zK=X%dOFI8O^}@0#Qr7k;P@}l zvu$$ZJ=#M6$P^&>ji2%v`fWx{n1i<{SNk!F(!8$#RN;SbDi;Fm$sM!-Kn*{T>p*oN z#eW_E+PSEF1pds(2U73gk_Udv?ccA*K_LNSDu;l7Cj)i>zy7^ILV}j{!3Exy|NHs( z?E^CZx&6OC#0mKPZvNMmngXGR{u`AO9PxlC0HrUb`QQR?bN>BIx$X1s3+49z{?JLv z5`Ry6(3!$FVBdeE9&|Wp{ojU7lZx`9^N48{d?-(#vd`6-a(}_I# z8@rE`H=Iq)qaKoWU!9#OuO?6pk9p>~E4N&gg*$tRM zb3~y(x+}ED*UTckSs7uFUpo*rDm=T$g5e{DG`^zL?E^%QMje6|I)bGN^UzccOnVyLWu z9X1gXFd~Kv{V9U$P$rz5SNL>$Ze~#-5p4mzUOfzsr%h`G0J@5H)MdX_< zj6O>eM8F8D^#Z+*jr@w9gbf**bhKmcb}Lz$Uq6jY^7n_Gq>%^bI|Rr}X#&KqitYF} zg=*GgQ^appeK&TkmbSD_a-WPn`Z;bauULT=viO^o708MS%2X{l^`cxRvu5I6EFt0yvu&061`lZp> zgV#gZx;Ii}%59KBy9oPCBUN$u?cG}+1~68v_fsJZNROqO8}M&CdA_hj$h^77g2IOf z(1YP_9|91{wA!~b7%Y7TcN^`)4JW5F_A5^BPl>61>)rRRBS#7`rJD`vEXcvXt>rN~ zmD_CRF<*w?epN#a7wqtthmI0L#BdkdmF^&S5Z*RpZN*w!q4|p-Gl!K`mTe``xBI7b z(jA;mQYfIXqZixDuq}(+7Aw6gIW%YQk(2n{3zd{t6f9vuU+CaxS)3!wYn5yy+yF1y zgj`we>w8`J0xo=B7MU52jB9o5+|`nR+M%E-cc1}iG&fwO!#`UUN{zf41G|_2yNaVi zTJ-owWS}2@Af`s=HDy6$h!jMp1`;;_`JXCoF{?LOHXn+|6+gtLd{xPh-P~WCbXdwC zbN%3@7M1cMt;7E~BeH(2UFi$@8Us=usvrYzNk{*jBV8aLx79-a#s+XZRKR#(l}fIn zXe~*SUG6YH!8YD@CWYn-B6dWmD0C&cy0C*~a)Lf!Bx%F>Sh*uqMFeX1l}Xz(z4aPA z%oS@0dkuSii?Zy{-)oNe1kx#j!}8_Yuw>jBfv^;||Mq9qNIXe~ZR{ z{ZXgDvMLmBibZ&Pg~B4ukXU0UoB3qR-@s!X{A}MYBGXlP zQH=1&up&ees$vC&)b`(mwj@D}LSXMQU%ZAjcKbWtKpp{){7gDTX6fLVzhO|qD++eP zDiHRBzN{9&G00cU@)34~(BaEA6n@@352+ z*MzAJjNi3x!aJK1+8@j-P$ZAsKeL^u^A)XXuA2t4P+U@gxFhc}{R6;-jxm^xE#}ub znee_jkL#OudyTul#9wmcePz&o+FAf-RA20XSyLJ0pB{x5c~Z@apD;dma*owG*a!|? zI))IM**yzF;A>UrjT+Bs5FBflz*)JCKu);W{8uP>>ZrbZb2@OdWyyL!*pMoHoG7r)4Jm;|URfTJ3LBmdH zblzVzkFUdglS0_f{~6-f_Wy&pSNh-0+L@x}d=L3CWWxzJEG?oyjuwaeP`^qm(6_uiXq`+$e5;fs!@GYHh$eYJ^=2}_EYl1 znT0C!2Rz+mix^{K{?UZ*%n%uAHDJzb01fg9x19D~ekuE4O zFAkKf>yiQp!O5Pp-Ul;WI7+*t*4pbAi&>YtK_C`la+IB$!xX((EoDq}`D!Pp;BNX* zuRB@@ggvYD@>%JI%Wx~GG_!<2jmB+s{PCIo1+%*BlgNufMxfk0*m=bA3#U4WIl1EIK~jrXlx|$Ukq>TY^E~{3ji^ z^a`I&RNwf;Th3Gayt`(mBbb19_&jqgv#7BU6om^_qRQSc?$-Cpx`$+p z_QWM2$K=C|mifk_(S?zgQ12s15PTJvE4F$K@)5fm^5;MW{}5VDS6ETuy*n8os;>$S zNQ9`DUoeS~g;PQCcs#~!E(vmQn8&!OB0GO-^m>n@$os3PCPsCc3$Am`p;E4M^}}VZ z7Olf&G-OmOTa2`OcJpVXQ)`@*n0xDRUEl203W+uHT;njIt;bl}x@vU1QnQ?J%PZ%!s@9yl7h^tXi#?J*5Vp5diou{t~PWcA*`YqP9 zWvks4P%;kh*ZW~=K2;aA(JZdsw*oDP%}RWQ%|@?Gl~J(@e?m*u&U4>@qsT;~jm57y z$|fcz-V|^ON$XVVUis#>G;KmAL}@}@zjk7V6JfKja92vsAe-;ypvyFo&7ckAdK!Vf zhHn!#!_I!dI6CeWIDlv3G6(*y%HqM{5}u4C82yEzRGGb{>D$SlKdfvIIYs$GV2>iS ziUMfJatP;rFAHRl83zAk+$0}~TEn}pxetiG>kq1o+CjO_P=mi)s+A$I+KCoPzcstt*<9ut%m(i_rJ+9- zth%RKlAx=o;jw`bMB+`Nq}Dy+L0it1pVfMR78;$zh+0) zK*%|!NsyjbgzNP|WvUsL?c5Y_muzA%^j z7_wxq549h~Z?=?D57S$C6?kjW{>}xsL(Go|xIk*qaTLv5o=OA!!CP|{MB9*Kd6wbR z3BOUMx|P6>V?o&}-Tpnnt;D81@9A!B_4hViX*Z!DR(IIh6CX*fes`^50%(`GgP6sa zYQnNa4WUQjZIV;R2fS8)Y@&^pt4|Tm>LYjhc|@Unl_-h+e)y2X%sK|zcC~MO7y8Hi zosq*6q2cy z=M_AcxS4^oLn3nt?iKdKrAbF|7j|(;U;C6IXdpKG?8(y6- zf8U9DSZ)eMt$Il}r8mh5%l~E7+pmGsUKh)(KVdk$RD-2v&zc`XK0R%*J=UhwQOkH+ z1xF262={g|p$cB6pPv5V{|=i&k7d7@gQZCy<<8U^Y{5>w%G(_9I#=9Om#2NyykzQx zbR(3E!{9taezi35=m7IVt8KdO#ZJ5ND*MYNV~ra#2|FWEpv?|iCps@-I){-OHwkx% z1HGfLhu*}YvAX9#>571C*p}(;0Ey(Shi|zqW{Z1bV`rz*PhRe&vu^hv8{IKYgnW4j z-GvRY!M1nohmiP@8C;jTXu24~ff|a|^0~`0lX6G3C`WdyTiXSTM%g1YEG$>3PL6i1 zUU{qJ9n7RBerSC^<{rEuuPUhOecJw;&{kbqfLG!_YxSyD2Pehz6mWzHa9zLq) zSi1iDm0u`4?(}2(&ukuL+a4b6qlCMQ(pF3%%=ZSTga*YQTi2(F7!<3n{FwVljWc}? zd}_Y=qMLBn^m-(c$JfZ^+Sa8z&@YNeMf^v!gHsflv`Rif3+U@YzafxM3Lw2XCZeK0 zcESpK@O9~ZN}c^hfiQkwn2z|O@97fAndPZ*yH=2q!Dv!(`pOy(4eDFadblt^Gp>7e z;O<`tz6+}B3kpE+CQH$?((Kif<7+g}>Mj1foi z(~$;__lCcI)gGG{z%zR-DM)W6C0FXEzfY0qe;;BLt~=^UbSPVF{Q7G zO)&Y@PZ@+~1mr!*3G$ zufQ`OWtUDK^PG*b-g{=<@z=NGp#tJJ{Ea^qC6?Bcih*If$`xg{(Ob9|xd=R%^R37WJJP_E?=H^K>Ee5)fS) z$=wCKk(gug2$Ze0O#!AQ!DH|7jq<@YpH%vL#ON%UeOy1viYhyt+9dEX@kNip>ZCQZ)q41s&nnyoyIre3A)fc zyJ4%70?xeo12a#xd5YENs~PF}E&6coPxtG&isPjm`(5PIru>8gYF_xoCZHRp&A0Ja zV#OZ37TdU0C$izS-ER&lBa`TaAX63Uoa=P)Ynm#9j=C&g(S>kE=tG%rA95%ZDV@p@ zv+aIT0Mm&fj2jZht-AbeMUaYeLxV%URKPscw;iK1z09t0_jr->mve-Q@gb_ByoQA#hCwtDDIKRCn0`Wi1X;QZ!tHOv zb2sJQYb$4N9jHM5#%Pe4HYw{t%YMY#M@fe*e=U~*%cv$WEjH}V%dtTTrcwcyud3s3 ztW&APHVV+hB&DiEH8$`ioPP64>HM)Po@!P0^=EUd9XEEXT2GLQkRgipt5(gkv#6Wp zX9S6?J4TB3PW%xA*V@0%itrSJ=Z6=V< z=q6urkXdZP!+Pt8rSNZh)9y?T*Uj2osWOZPG<-+=IS_YSVMOc(&NPn>=>Z#`ZdZDS z9UK~(c}Xu4aX(h#{a486TTrdHuur^|gbfwX2|t!QJ}{k2NDW#-V%XNHuV=?I&?9`} z7VPE3l}cSOtF8HF1vJ?K5HgjI)6J`DxAH+zLh13(ZOwGzbVh}4e*E%MNQqk$>!u1j z$Em#SXD)Za8j+)iXf`$qlL|-ge_@}5Z$JW0DOlL20V{t8MUf8#bsNs5Czy`1 zu+~repU0m&Pn|bY%DFA{BjEO;=hGbCTKsash488lN20ISO&TvMf$yv0-9B&XZA&Vy zhwzWq*FU~qm?POPHN0YeY*(_ko&Wi;)+MOxFU*+90sz#6rnM zO0N}(*Deep+z2l3p0bu;;KXQ zSiucfme0G>Fag3zkaJqd=+cF>SPQU(U;&rJM$o8TR5-Vl)VbjMrHf9mW&u!0frxE0 z-^CBi;_{9WJ&4Q2;Rz&M`X@J(^FK#eP9!l4VmO$aOCT)e%;;7U-n-@)+n-MJ(3Wvo znKHN2<1pZZCuAOuZBw@5VRP6?`AH75X7)=CXsjA*i{v#+t7R$+ekdj(p6_mDWe0y1 zZ<)AWYf_wNw>Pr>BU5f~0s@N5?bhLhzDX0lt>f(j`GKIa%Qne{eK^YzScvgj?$ZZ( zQ9MS>?HyQ!@vQ-u^x7Dz;|I&o0X?f?&?2s_{7MU=eGdNoD0|`zKFW{_9F;0{LE%9X zq-p;_3QUK7v8KdHf#2igk>ZrCb&91UZrzHbukxIy0XY(6%*S`lfY-Ut<^`8}v%(8n z2}Pb}K|&mh@y`_KY4hXC?e%cl{f@F)PSPLvlenGvzuuhndt_gIYAJizql(M2E%FeH zVZ=`n9wDn+@P?kNZ*S3`jcQ3cUk(-wXpr_-D%t7zyL^Mx%IrJ6$NFxRz&hh>(~2ZH zLXX3_b5@s^!ub*qTHos2B|wa$C1}{-LO-oP0(CKyKlbH>cu?kwV1-dI zRXCAIocR^57I81u$v7BR%7)BnCM23>K@##@cO_Sc$;nW5L3P-!qUm2cX27kh#*bYx zUGMK36P=554W2o|1$Vz|r6BQwff1e=6H+abzB(-XIEq~_WS)4hD{lz=7!=rF*sfvB z5b_c};F5NwTvat|Y-|I)Mm0$md*#*@ymyfP_@39-l~0XG=3)LSi`ISDg# zEH0_SdFqZ75nEu1?cbhAf*k7?Hi8;fz$7bSpKgBN-iF6WxmsmVCqCG^z_V2IYsjfm zm~L;)hmCL~_>^=bsK_%QU)ud;`R`gmmB_ps(DHk+>R|yQ-|jy5@{)B+*PK1DOJ+S* z@N%{`Pe(FlzHhdMYbM`e>h5mK1GxLlGE2G18~Z}329nHcA2aSkSrBoI>1^dSCuB2w zWy=!%UkgE)zeyr>9yER~igfBj3WB~`f@Xl_g6~;Eh8A?)92l~kZB{V@RZ`7r-!N#{ z=Y!^}g)K83@@ggQyNHO@F#5(Kf7$-_P->+KOXgqC{nEobgYTtmBR((a;|QRV&I)9g z=9M_*YLP#!uboIJ`}XUF`8%OhpBG0z-jZl=y#8!Y6Y(P4B1-svg(`jeI7v=flI4TZ z7+>+=ef_}PB%IijzG-upou7sE_c)~A+h9MzV0*bWGfO!Y?<{y&@v-Ej_pq@OV;IW? z?%3npm_!?|-9qNjiVB$C{#Kuu+oFz*IfocB*B|xqoobBhtP%h~&)_6b zpAC`4ZkdY1Tdwuht4Xgs$^d0%L1Cub|GXLU=}XurrE`PWO9^EO!{+WJ&$%WAi0sv8 ze>sc)hW6y-rvQ<#w<9AfN2FQeg>w~G%c7I1<5#1;G)bi7$MzO4$xL^z#&i-JALHL} z2ZvC6-tfBamL&8gEX9od2-bbfw)>ywCGg{9=hpHR(wCQ)T^d$VqOlSVY|MD)9yq9$ zkHoMIkjIV_5+^=EvTs3KuE0gVuf0UwoG8Lf{%_fhaI;o&$YNsnh4v&tCpmu7Y z7F}sUF<@?LXY0Y!*4BR6^N*@RS^JMWk03a}7%ad;pbehSK$6>0YMqWeSvhYPaQQWI50-5t@&l(u6aZ4^6-we@pT_h{9ODri>wIF*aZgNV znLGS#)sPylpe5I@8JuTlXpRYf53-Ho^XFbmFFybL7r-?(N;OT&;OQ-sc;30eq-%I# z$e|?OJXF3H8IqQqWj}Wq(=pCHK9)m<~_1&7ZGv zIqj)mp5y`y*`0@rmgBSrZ06!))u6`+{HoBjJUeKgNR%=B^KaW>Enm9IIsqlpCX2T( z1J&q)Z>%a5+fB!mjLtBUKo$$2^Rzk4l?GOL%C!kTR(PMN4SA4zu%>o2m zHU!dB?#kG@7Wo8=CdbXx)%mk6cPpPq_7~hN&R1U!ZLvqJ?&IY`7}X?`WRM{S(1wxH zfiDn|ThNZm23@F(4k1yMa5>0`Sn5o%R2g)D!ta!<=~+b9iGCzl0bw!Q9P@rr;hHbZ zP|3YG0@kkrRf8G?^B;Hgy-buu+#f~cgHS9TL=*(mv0;Dxk7LL^^?v`f`#IN&soTZu zdW8n;a(=M21k{&iT)>3FblSwua(F9PZhNPO)wMRVk;oHxm9h`i&eLp`;YMb`?#{e@ zG)za8w#DkOcaYdzyR2`Q9V`>4J5^)RNQ~oPVHU+RSI^t%2-Zn6XPC(pX)p5k1Knf( z%3gi!xeMkSoxL^R*9$07AvVO?1(0)ZLj9I}U!LoZOK_MP#5BxAnK;4@U;YZS$iO8r z5ZcSOObILD>M#K%4$nilE6rP{jI3YHd2gJR#y@=55YE>G^IGK|Lu}r^_~t?2zg^}4 z?Y)TwS({;=-rrWfEm@8s@d5XD*cxu-JG8NEOlO!DTF)(XYH zETV+Gjtl5}*Rzq0ia6<55vTH1ip}4ak~lVYCBbOURd^gBf~p*S-@If5KGdLR{n5tg zkWC*lI$EZqxG4I`R9`@sQg=4W;-~qA47%cRY*F7wzeR-ypCH%1}~752~>x>`6D{5z6yJc23hFSs02 zp~f~O*Q+>vTi}(NANM^i2uRkXBoup|N|pVFc9G~QzzScDR3h}JUMzo687eNJ$%S5~ zDno>}P&|4Om(j90{gvy>o^5GA&_&jhSIc2xP}7Ycb2KL;<O!tU8Y%cz~DaA^F{*Sk2$~ux%GY+tZI$ipBK)CfM?!$ZBbAeYM74>C?Rf! zdyA9|yV&J3yDS~pb%}|`fE?`G;;2U_yyqxU-{jyMwwx{;lDz(h3+O}lpJm%q*Pf?x z?srV7K1It4(fc5DMY}Zf=~K;L_Ii7?XqrzR+==s{@?X|VUN48cUAg1S@Vegex>oi* z=khaBr{6_LF-A0ELi4!MdE$iV{zdZ>%V_qsbubwrOmRH6Y!tjA1z1r(J1kWBERJt3 za>pCRx7UAPZvmaChA;2GDmokRpVg_LxK2+GBQs3DK;COXBgUtKgW+Y1Uj)JUV!MCW z3W%E*t>fJu+7QN-aW^>#m#?)7>qEbM-a5r5os4GqLERZRl2tOwl^{e;LHx}eMVy1w zhb8VUvAfsjVMzRa>qyUSm09V|3K7P!^sx>ZVvbW0^Z=HVy-4Z2* z3oT0&pHO<3V=SCgK~TPev6l#ioxx2Yi_olmx<+pA%jtYXX1xnmuJ9{Di!dBUaWMOZ zZdS)q<6t?G$9&DwWMr?BDV#x~c+gg`Y_)=D(emM11A*Ve_HWI_-ixvHuKaAbZ#|zZ zx3L2o;R$;wqc$ZKeCV2hBgAbV`H~HNDK6*S>G+x$29Cmp;3+EB0VWAc;WK?$7l$!( z7c0&|&Nuh($C?dX*nV_Z^Si4wWalMQ4??qP`*LiPBofJ1KnaEJJgP;iuBR!Rmb0BL z-yzDZCHhWxs))K->^>X(2HdkPw7!pK&Oq-pb=eKH-Q`zeiM2r7FJq@ zyYi$&45{&+Fs=wK(CWP67-#6#(^CYy_)f(Y7$P>v(_UUJZ>me;bTw|M{QcBy}=GW}rV`996lwy0ICK za+C^)`uCjUB6KATpR2w~3=3$Nry|rROZWK>$2wIA$dT63DqVC6P_S^K%X`atd1@Ms z`uhe!qXqv53A4ev9)W$mIs?Ddl{$1Rg`W*Dne5^tyfX+K|Gdf-^)=&W>9aL%EG&{AeZ!@ zZ!LccY$YjoV#Z;!i6hQabq!GRj#3k|5181^N`iEuz|>k3=w=CxjEsPRr;Np|q(tx>RH$F zI_1mE6NFCk=X$(_|46UaQtT_Mxl?}T$%?i?bf}$X)yIG&4?R95mIut`aqWH|?klOD zBS~!Jfyq^|UCFL~zqZ%KLGcUj9CG^}Wnh`{59Rj(22dW06JT-4GV9%Qu?@$K@zZs=oXTkX6Vf+EG3mxxX!vA8;WRVu;}xzL3aG1eSRiuFhDJD}Ov znbz+A5+-(4tI5v&@u~|qklXNK2F&cf{L&!&4 zrevdmIq)5Xl8EGB*q8HvsS^=H>MM3_l>IOuv)wPVJ5pQe%69J8EUjX$O=!V!wDx_R zY6mxdlAiD3nLL#%7`3a%=;`Ui-7DrR4 z_T!1^ZRfShHknP;7-{QlPt7yW5y2dxXMUjFCXtyx&ZjXYL#!{?SY3aiVr4Ak5B2tV z3Cj;OxD=db79k_PFrHUOh*WwRgb7BRpr=z8VjwK1oP-8;e3E2YnOQ5?vf zK%?`>LvrB`@XdK&r3_f7cX24Df4OqZ+of;B{lPz@Udx&xx-Bi)KkWaK0bL(Mm2}tN{6zl)U%0?<^jmJPBQ;*v8y-j1V0e1a z=EuIPeQ9I%Ey9@GG0WuVeR#JPi$G|f~dq( zxxef*Q$bu|ys|QW4OROLY5;v!_-Gk);q>?9U#Azm~V` zP@WR(9HUy)?TchQ*6lfUdWjt_Z+W#|2EKq~E0lJfRrQ<>Rmx|&xp>v2ugn{k1sgU| zcf!}Qm_*og6?2dXiGgwn2B1;ztK9CdoCPtUtx6#8&FKS)ipaZ5^EJPStCf7}GP@Qe zuTS@INgMMDL2O9=8PB~8>!M%IASPcQL%5a^s$Av%#BMJ^s1;-RY~m$7K_L!s*}Q+9 zPpyK@NWO;)Yd^ou8W%0loAPGF67kX6hEcopC8s_~eWKa)gCA50&CXThd$e{8TFxvq z4NLdDOn+u;J<%u%eIH4KHOC}jkMQg~60pQOC%WR^>lzEF%dEe$zWH6v85)54YH9Qx zb}OfChxet)ZVi;g8P0ouqn8eS2ARR!rIgktaV%&Agu*~7AFCdvx;ghIIF;OpO~=NL z95m;_=hQB`8PFM}1+A+zNYg!<*A;YcVG@BOCXSUx#(HbYTmRH?^4{30QZxG}6YpJ1 z5PXJ&7~CQ*^Q@i>)sR-~i9^lq<#zrfnS?hm z+|mwfhnmpriT6dEBs38tm*c;?_q?4b&(>!!)5u zrxyn)6(R18=d9zXkkepyux`;6^%AzxvTp@NpA)~sf!cHSB=6$|yANX7RT?^n&hmY# zHg`HqLV|*A0a{*f<9%a(5^-mo%~zAZvV|ry7Awn)`m+bHg&P$|x(}imxU|#w?FRT` zq=nLj`dhtV5Yh6U2Y$@5$i;WCiGE0qtZ4|I|sZ71?lk4JvY0qiblDbXM-w~u|sSv=A zx~iL>PF*_%D{j8G_oV7B2t_SkhnK|+_@2LGe_`cu1l`XN1$$jU9pMV$XC)2IAzWQZ zCzSVOMau1E^93%$*`U$|==V|R&!Z_{Os_9afzinOV4%3@Rl0qp>-(ZU_H4lprOcN% ziTm60cu+H6P+=B*0n?XAp7edJBqO-hI4mH zjCvCtK)sCm#c|Am3GfsRAE}!R_`w3hnhsgC=@`j#zy(^IHSG1}wWMHH8?EChwwcp; zQ1Q~IU_m{~Vj`AaIJN2=^X-oetLe8Ts6;S@z5Ijd;NYOe z%5Yge_~zn;e>JhD*MJxHp}hipsdac3QDZK&%wC3D$FI-%oM+ywc?q594@nw-<;PlE zsT7)~6tkRPBNBs`_^|BqJ*DH}%X`y3JN1j^%Bxd~1MyyShMr(db_Ac4q&n&i zO>b*I)buW()4yP?%B-*Z*K$mHLTHo7k28eJ3%=s>zTsOFqk;~_vBuCw6(}#%{aIjm ztmF5*4jN8hSfRCX{=n8L;>3qN==1ptQaj_muJZ~-8L;lPZl#z62VQ7F*cJ0TO)qKb z!KmDQh2*~`(x)20I{-#3As0ORECX6O95IX=E1d?4mhv`mZR=AFgFr{*M7{e}CO#{* z4;>i%7}*CjBd<;o_--H8)cuNXb`si{F>w0vV0+ZdX`Z|5HdB8VsURykS-}x>q!$Ld z!_}eRm~a#3*GYq}ZL+%XL7Vhh5VDw&?YVtQ#uV3MWJGw#u^FpJi%KZZzTp}{NS$;*TT4k6L(xkTk{W|bTdK4@jVH=D zI{1{fQ=O+Z61nB*uwUL7aWvOyJQ3w|P6-tE*gF~1#yR&m#-hq`O_uRG;$)#gTWeqb zX?q!$hGp~O-ROuOjAnUK;voZO)i@{YT>(#Q9%dReOg2nZdOl3n8lRkRv|o9X&A80` z8U1J^sP`>AS+J%Jwy#4e6*||`3#OXh5&9+Jmz=S21Pw4h8fOeO41w9}4Fs>VyKJ^x zhv)!-kCCOe+3o|DEHUk}NuP12q$`4(l~Tb&QT~d~7%@#mUxaGsDp!l)p)U1FVpA{A z{YS0FNveCIZ@+JY(b-5^dgfoskmLdR;U@3S0ysyhb4Xy+pNPh@; z*Xys71!>4c_m5rTq8JS;V6M0^LZEXL)_MajoD4Cb^D1shZONi|UuT?G@}w7jg-_?8 z_LoBbqF!PypSJ_U4TDd8k-%1ZQ&iJZPNnHGTWeirqIE-V(p&X<5s1Zhbl0_$?^do{ zRAJ7kQy&?`!*-|9Ro-@toY?Aj>3c8ydko8IGm%xHg# zGtX52HN+mapsgN!xiFKSPnh)fw6&o4a_-lFJ}|zt>U)2!sbs`Emd*LssdKm_v4xsz z@Y)51+O2A4WQILK11vkH6k06QL`W^q&xaT|mfe2-{P~?in5zAwN@Lir#qQ-!e_IBm z#^!v@5GFXUd}lIXX1Qc^qf6=NlAsTmjsr_gj!jYz3TmH6?7w?2Xt5qvyTFToeD9;> z2HNU!fAsppyxPEg3kJG^qZW-fv^G}rhukb4sLH9o7XKi7>3kTAYJ|&+nR!RGY}#{t zR>q%TaJW(S02n#awOk&&)*4v3UeWy#pSRlOEAkL}JH-GkIeBCNKD2zS*Z-0R^xikY zTTmGp+|3inls$XazK#o^75mlxOFXs4XV|l90)M9ZpnO*mT8{LHWZg=ZhqiozJoo_< zL$fom*_47o&n8*?Fynx-cGBN~pz`Nkb(t`|a+TcM(_eEcB0!J1&D(;RD0->>%*FAw zvI+~C^KaQ0Ue-TS!)|hZeL2Swo~7EkR`KIJi?cGSAI+m3gS2nu(mrL*QlA!WtPtwh z0sg0f2nN>P^qhFym-EGPM)RP?G1`jvR!UMqp(+sug614!9ieK_IBlq59IRVoNuj6$ z)_fnpG=OoU^F!3_SVG84+(bTMTY9w~td3dw4BEFA4%1**tK}0~$Q8AAWA1uoC>X=K z;0sH6^w-%s6FJ}a@ooF7AcwD&B>hNXVs3AyKR3f0dm@RNo_nrCVLS5>E1!#)sF0*X z*$vhv+%kO1IzhXz@r4k3fZkChyzlvCt$xh#TAsrd%R;+#XM1b}68p&;zmd1KH~V4< zEai~ES_YG%;BrD9Fv%xNHLA-`3aPhF-8kPk74h?V@ZA9LJ-&MD1SD_rouJc>cCLX& zOAFlFW0oM0N5jB0R#$(}63BE;tjB@EZ?I>!$Wo8xBHbB`9IV#>dQ}y7^ZmTomrfo0JY>~pzZa8*nt4OedIJ}x@o$F?|LY#JGb3GKi)P(8=8{w6uH9=d6Gbs+Zm?;Uu|w_|L1B2 z(s|~LBo4|9I1HBFs~{1UcFt{Ez*hJq*H1$%PN;^gbUPQ*obTgwAO9U?zGU&E@YvBAXTI>mt;e3KeyQ8jd)OUOW z-mLchY+xdMxT3*}P$Xe7S&(jXddsw#W#QH7_Q5E*oizoiiC2s&-EPVIOY6oH7!*&;{BAnh(z&ytCIF9ul!JHO>wJ}#H1uU3tnRdm)2YW#Olso6g_J?P1crgV57JNbNbIAzm zS(gnm7+8OGV&QW<_2V9Y%*7C0s8${f3oqK65p}QsT)A`m5_I$AV$Bm4LI$=#_=X5r zZjwBV*lcmW|32&QUN5w*r$||*@un~V+k`t zX7D(NxKC_1?62Qv{F1VKf@B`mgYhM!@FVWwVA)-L$c5u_^-s)iB5|=rhc{AVf&g@guPAH;*EYSXeP+#uG z+njbkcA7Xwt^n(Oc!s$RKf@xTU^*hm3@%(X=nZN@!!g#gj^)d|VA1&XR)=dn3x87| z5hvwKPY4(KbZ5_(1U~>&<0LhshXQBT&lp~QCB`{z#3LU{M#n@Z+VjE0Qo}hzl}kGymkHvaMq5i zh-`b;ANn*b*iJd$AOaSKt;0SERS_-MAuUy&Xqz^FP)f-jwA01nG^?!*lrTFQz~f)@ z$t2RVOd;#qcTD+|Y}UMH@@ueQ^%ycE3@q36gY6+R7hy2MeDchq94YVfzijbSMR^Rl zviSc0W9qHLnttE+e@q0_sUQjhQxIuTRGOgzj+RoSkx@$50AZk_AUV3ETj>@?hzO&* zMv63y9<>p_d#}%1f8Ren4m^%+&*y#JSDfeLQVjZJ0v?zZcs+Utg}7fNUKi*8+-&c3 zo!yMO*(5q`Jg}vZ5@+LLH`S1$u1by9J%6m&>Ok-CfQOOf{{3jTWU5AX#Q|h9YHGn} z)%0{lLZ{Dh7qe_fc;Ua-LQ6MD7e#(k?1Pv~E4VOHS8cdV7rRe4Ai%Ia)w*ucum%FM z!Cj%qH10$gj)sr?FuSe2KDg}0gJpTwrhwQ7})GT$Z8$(z5OqWrJeIQO{Ap_?7GYEo3e@n%d;>6f|IyiQ1L?M|!o!Cq zWo;YkPpQpQVpeF%WuO}USnkznI%ETTVoy;3zY^}vZZlhO#B5tMd^P7IDhk#SRl4jc zeCU;6AshfR{*)>-J^-d|!X{|R9Sdz#wgMrgc+p#M$va?>(rvk64$P@G-cvl%gH~~( zdCkrHV2H^hRPW~^~ z`{PZ)u(+}q2`+!_r?Rd58qnu+Wyc}Ra7O#gNn3r*tInENu9{>OrzL*Br2A`Tn+1HB zm-}GSOLN{#_IN=08#=-oXSy0+S=?|@{6P|ox~lH`>~;!p9Iv=DGcyaCcQV!SQRGvG zrvmAEcMPc!%tY)3ZN@|v(!T8`ZPkd9=w|xiBcgA?bU%#!_^5*Lbo)X7-&}x`@Kx$7 zZ~qQz+%>zWjN*q4X7x@Q9PQ71@7K&5I@jbvm6ZDPQ=2@O>rrhNFlx{*3O80@`D@ccdm=ET z6|;CWnsO!1gQ?8@fG|B<(gn+z)Ee2il&AbDdc<}rLwVP&J0BRO7o<5B*vjkfMZEy6FGB3G86rn~bh9bX;nfLA+29Yoa_!0p_GYH}*<=YR$_Fz19|UFAbO#Rmi<( z#Rc!;1?h}_%E)i%5Pb*~;4o^6uI5>wZu2z+{{8+Ie^}oEXa)`p&Cdp(Y`^N~5~=Qx zZ-d1J%PP3DLqE+$Zu4v#D`FQ@DnCcYgVJ@gaXtyM5tJuctI_ez3Ji53gYB6=ErL<| zeo3LJL4H^HN9uQ#j`Iq(wrihzef^x52U~!Yndf^!4&9F_FPx@0d;bcPTlb6|B|?zbO2idud-ync z&?%d~1u|-?Xl_#O!f+Ay+!^C9S~?#pfxb{2&C~P+-gy7(9K~5tj7YfTXjcmo_jW>X z0K539~k!zREfp~c6=Tfl){4MOnbDqz{!=wSoA(C7I zJVi6mRzV#Xz1w=gNJD+e8(2Yvkj&HKGo9jV5fc+EX)vqEwvpwFwk#+M^lqK&hB3Ki zdwy59s;DgoGFenIS|(+34a~AX!TLiM8(i>Bfy_J!2?@n-x8E!-FW>JJ0S49oofLJ% zsS%f&7Z3`v%|{T4A9khpwq$=h)5=tMSeGVstynST(W9t2B`f?Tr9mHL{wF$F%+)Vq z5XxY6L>FFW;p55jYW^PVTrM@djWZ7sJ%kO-eQtyYg}H}zk#WXUV#&^du*vh%ulc>= z(_c~1h75$MZArdOTvk%c2Oy1LMsWf?kGbw!)Ucd5dygU~g6w zsJRXDX-5eJz8|iZWDTyVE8v=X3Dt{%g#}A95jN(&*z$|7RhNJ?{_~*hqV)XGU#)E= zeu%o!C@%5%`Cs8@3T2y(ip>XmZ(b1~w<6X?PYgQrwA|^w~(kr7~ zUOSsmH8ZF=*A@Y1GeszxT00tF^W@;s%2zVjw$xa?T}Tid!yZ*$|1y3+EadS2Iq3>I z46{BrY~I11Cd0yxY{Jo47pG=@uezMe6u<#5fu2wx+A&umLGQF}vF^ZB1(luBM+uaq ztzRMe;EKAAx{k{I1DkN{T5AK{*NN)C2Zhs~@*O6;YU-i)aYaFeRG24-JYxR2*+`+{`y$L{E6;43Kwaq_c_rbb6<|CZH9nC!pZ4pt@@1mk znY$m3caKQWkW&zmLLF50ogqqKL@nIUMM6=9v-K7Sdm9cQL7{w0ao!d+j9>EP(=u?& zQ2J8-ysC&t4f^dgN>vm3%^B!xA3wz9YP3E{WNUbP2wuD@Gl<#> z8gts}5HwwicmvXOh!-kb6J}@A2WRd8 zOdkqx?04$z`n)vAjw`f56$*3oa@jq-f5;x)JfGw>=<)B-@tsQ!FUd-XiSW6fsgdAVJ0vR!IdRjocHG5}?Ix3hfY>iJMEMV}Zi36UO+ZET zp*GY2p#^j%>_pYsMQ>ec=qW^2JffnbT(@mocuol%YZ|`Pu+0YQxDqCZ=379q zKEMmaodpmmv}T70uRv_(0k{k4&5OX5zPn;MjfjQ-2JF;hzd#pgR&a6V|Dcv&S^?FVDQZt-Uhx zB`wri{A^_{JyiUU-P4v)Y*_)&YK?bpMVE=JDvqM?MdliqUXe3PY-(OQZNRxFfLSRg zbuW4Q_`$NTqh3NSxCebZu*?b5vlFD%$V465A3$9-bn!j-HB`!vS9e;7svX}zXJr`V zO=LBQ*Y<#x>Hg+yB_XBlU&Fkzo&v2cH`;~+lEE2Zum}NtP(S#F-HF(%U-T8MqXkW` z(5_s`C_(5ZGwkrICn^OmT4ea6tg2YPAD_@Bb?9IZGbPt>rMpG>GSK|hSymnJ`_&kZ;0KtF9{FT{=cZW2?ACVQdzY~+z1+GzMbrU}BA^j7C~lq#>_eVUoM(Gk4m5$SBax_zDd!|6Rqdq7YLnXNOa|bEo#(^3V-Pm z*@^E?({D~N=W+a*dhOiXi*W>c-?~!zT*>DB^OTS zT_DyXFOVd+B72@bp2oJJb|Yjpa)3ggQeS9O{2xQ?q~CLJrcA3^4_g3OT{%?q0(M# z{r>Wh7Z0c-E!>F7o7?r>@UO&k`y%TULY@b%ZlI2RqD+=WzCGJxw z4+P8$dVCpA5LH|0lJ6SoD&d~+*ZtYNHQ?L%PwruSih}D~@~PwT$svJ~{F)b4Vi@(v z`O7zUxO6&MRr-E$Rb_P#Aj_Uh(bHg%*Ggp_zflM#zHn^9;ztlLWQy zVSE3s@(t$cS+X{VjY9PuEHHNOR-fzmI6pjZL{1aR^~$6Y4%qxf`RWW(Aksyb_GPyQd^Df{-N-+@XuJge&p3_+DI6npp8 z2d}e$rNN4fxKE6rJxhN|uisuZ3ja}D`e)Hlc!p-U2FtWb$(Qo6i1>nA=%N7LnPJKN zfJryFko8#1>eGQwlS`kL+5C=xH=x`q)~a@YPB{w#)HqAhSMwpG!$H6Y#p1DE-%bB+ zDjhOi(0u^|8gS)!AWG?m)4Owl4(hV>)l~48D}~b+9sW>pM5Uo622O+goQ(64-oQg! zOI=;$hk7R`1putz>lq4>K|McK_iL4&o&s?Chf`jnrAg>%IE8F6L{ETg=0}g5$?F}N z?cF!Ux?RnME%}xAjMi&}j%D$tQFSgS4j?mObUUd4ZW z^wZF32&^BX!U`w2xVU&(Lz><3ZDSda#tj>NLkz=q3Mzu$tEcYx(3X5?sQY}&l*)l@ ztdw{Y0w$ScjWhWc-CtlMnUC&mFmD2rzO2d#i{~iU{^Qd}r63Z8?r$Z9l?$otS@Ebt z$C!vWp{VQ#;_uDv#{)|ZJ2>PHt}d!>(pf9u-z8Og>7B|xab1gS&{@T-MXjz<&H2bS zETsm*uV$>D$#8>;q64`{Y6{ z%5+^r~}maiLMOTQg15E3QR{d@q_r zr_u<*Ii}_MZeKK2`oOTdAli!8+q)cF%wWQ$mEIky!gPp$J@QEL83 zvpe&~B}Kb}cmyM%$YDPwuQqx31ywORp%o0&ZQf)=gk9y)BAXgA?x>GeKrH)ExqN7d z59y^=8hXK*s4Z(UIF2{2N_2b9->$ACwJlE*l=}lux|90q8Aq z*Tt{0h}&pVNNhHzO;NuC53VTk)zDGR+=%5Yht$5j7?i^#cbb^|hj5ue+##YQIt7Hr z?aSMd#_Yb#Z)13MzGv@mzn}Y`R5qLYqUv#2PI&37@l5AB3E(aleBFCnT9lPNg4f~T zcnGL!;4O#``hV!yWAKJ5^6zW>BhpTX|2gj12ItklQL;_usP^3GZ1dz}pOS9?vLncU zNgpBXy1l*QKR7atYn*?Wc<u4)!7R-l%|48niYLD)Cy`axSTMzTdxh`rx(Vd@;!if6Wn9LFBOv}b4W{I|; z3HIS4NT@o#z(kj0wi(w_6nLsq@o{9Gz5~Cy_2l%gK8T*X9FDMVDt^@Bj^`9Yw)0x2 zjmYdSyW&iY=e?|}U!ipU**)tXp<-MY8?Vj2I|X49B_@Yvpaq+P*^uH`bhKMNQ1ULY z1?jIOm|Zxo0;Z%^H8?YSIL9-a*Qji2LJELSrNYb_H^DE``t5m+&Xd2It=aS=XMfQa zwWdr`8Sl%&M?hJrZuP-zlk0M+%Y|}NtKq_Hp~}cK^ON@(=!NjHxL^SZk&x}o=Gdlq zA!(U8E3r@XY7DLV`T;MbGus{_@#|{MSih^(yyS}j+>aV#Q9bMLI=wK<@AR!FY~!`S z@0h4{yzyp#Vz-tO6?l^zgyf1xaX2pUQr~|;@t_9As^6W5yb$>q%14qzPNG~Os6u^c zEkZb*XtcoQ3;O`GnHB=<#?t=;pziLvGJn5IB>r@>18)$0UKy`Wsp=iuS~rW6chFa) zg_M@8h%iw`#-cSA?2jTjZ<;SgTUjB>jD^cJs{YWl^kF+8!O?3*^0ju6)>kzCRCZQ+ z;49G)%5@EYrZ*CpiyI^O)F*9Q&YZ-pPOGmxJ#tJ!?S!tbuD)rqc?tSd1(4@?uqA}~ zhP}CY7SL`Y=4c<3@_d~*Al@Cx+W?+9FV+f!4MWHphe z!`L$&)xTdsnx4td)~ZjZ?N4LCDfZab64B_IU)}vwF{cuOM7LSJ0~weEhaJofn%$*< z_UDTPDs;{rU$fv)XIN}n?KV;Ru|6fs;j59FhO{mzV8rY#(#&(W%75>wSxnr@ z;*PyQx*W?``}wSd5_7WKZ%Nggu0JvbJCo6ozUa!S>H0c9ZSAIDJb6~}5CcSXVJj7+ ze737;)>or_@X10Z+NJEl$w9kCqRR^@kd;ArcRO)=vNbEolNu#LNa61G-M*C}hsyRK z8fBri#l*2~Qqbcp!p?S74us1PDz^BV%V_>1t>+zD4i%+-N-S`lfD}6gG{+?fmrLD2 z+pCoW{l!y9epR1fXTx^s7&;%UPx zt?paXR>pG<;~vlV!`B}k z?{|64fbzFM6Y7kaDK;!5tEN@i|GV|iEU*!9W+Nx>dR9vubzw8QDoz^75klF78RY|a zmOU5OmdQ6oYqnY%_aJn`m=yJ_jt7=SvhXRM*bQ9N|7*~t z@<@hr_mtK!itl-pJ2Wc9UO7kcpqRe?(e0(HTXtu8}oW1h#P99~ixQ=-7hI|*3>@wT>C-Q5E^b}>dHfFk*mN;&C z3&j}@`+gHus0h`ogry?j!CmF`yMwp^u4k}~bRaF79r88l^HoWM$PfB1e*zb2gv*4d zE)aSB*|paus)Db1b%+uqi9nnJT9t_P-D5(QRAgO8BA?E#a0bi+fAW8V`AbxV0yBZ* zj%86~2MKWY{E7L|QY0|pkb+qO_;9Kgfn^;#(4!B!wkLzz%*(yZ!OwP+#zldj#}u3k^g^_A@}B_n zRs>pduZy%wEFd zZa9wEe0P_ptE-%R32%aQv^?Z*F+(-^(jo#tK=-0jc_|ZM=#2bWGUJ-QwwZqvE6+ zNI6lRY#l?#Sp$j4A%Tv~PL1ppBfgVKi@76#Zw zyd@{ds%2!zuo%&~7(_DqC!wD2E3n)n&?O647w1-uXOhpQ&iv4Ys^`O;SR}QFOJ9eR z-D;Z&YaSdJm?mhFCx|C00y{%QI=W~DF*^)}@_czK1;s)55h=zu6lRR|ERTWRv4+VTpjHOncOIE8pMtFX zUqx+ELwWpYK|3JrvW7?Rd7*wBhydyC#x8j!_s;Arsb%9-J$hXvVq`pnBT-(re5(Y* z#cxPY9R}RPoFIW;9P%`iE&C+$YY{Y7qVrW2@iLrlk;QubUIt(pp_E#H*9(b+LS28H zZ~#ygMjIY?MwuaD229o2r9G*U8il^j7hUqiZWR=Ww=H12$D^;We;EonMIMx-=sbpM zhQNx^`s71EYA;Elc*L{RU?m)X<#1f~TFYZfmX}qqe!BtRHz!ec&cqlAH%FZLS`7kN z3lm_s!~Y)bxVxU05({@GENyWfTk>;~XciJ(%geduo#5tAD{eD-Qe8#6aa*WDL=u~6 zOS=!My5~H>7wv+HU#GB;L=8{)_|d9Js3AvD75?Nf)JVMf9B&Tz zM=N(Y;_aT?Bu)PO7jaAU@@i={mNlgw>?M~Rdy?8BxHM$!r#V@^N&QxJsDzGo#-Lp; zWsVd{nY35Q(m!Ux<;bZtSSc@zna(x4^)pRKwbtX$X_qK(uQt=I z@k<(o=fTW;*SMlG`~sOdoCbuPtnQe<`>+%n2PVZzO8GE65d{H_mwo8NAc&;(X? z88Mw1JxJ*`ZmG0U2WRgI!9G4FFb=$*wXQQ)!gS zy$j}VzG7g>l_EY9A`Mc|*K?mfZ&(>4*QjnIS1*{fP5iixiH`~O6J}v`Fn!%pG~eS) zr~VVya|>f-5TVv7nN9`klt5CE#I!@1Q2lCTr+~a&O-4e%SEn96W_>I}7iw^ka0}Su z%7FJI&#G+T#Wt-VC}t!+5b8U=qSK3(@&A2?35WAWFHmy(M7g&kkm~Y^0`jkw~Xp@)=F zH34=Y0Pi}XqO0&#WUBgieS0Mv4`M|^DY$sgNL%;|kg>lsVxV_uUk!s(ALY&HJc{+m zbs&lymE^V~t`Rnhuq-^l(i?zfZDzT=zAzf!Vq-iNm<4;0uXl^wmI4zt$b$jU9exwD z>bEK9qIQhiTM{;&;RRknue znL$cWh+LFdf8@4PX`YiLc|CTKud5oXNazZ9{8-wwd2mkY2fkzI1x@DU%Ew9<;z6>LQsffW`J2q zuINniK4B`Kx_cA+t64xra$p@GdAA?ipniQg%K)&L*dB5#xta0UJnd( zO8*8t2Rag*gVT||Nl<%n@5Or!6;bV-y(5%WjN(QJ5Lq8a~Hs*KTuQg@1=e z;l(_1Sz$sevSn!f1}sOCL$SocIvBDw%W-AyyySeXxRGgzcEG~`{dC&Dtsq3-Wk1mr zK-~f2Obn%3t){*BU)#|Gn10q}vH2w>8lXAe2-GtHOAW5)b$+ap*1T?lr5Db@2^GMS z&`r6&001nM4|8CesW7luf*;HTZd8`(X#PEN9i8A-y#p*NT1!5UUh{MJ@RLm3Hfs%2 z_15kymp#5CMK6ZLA0%WYzWqjqei5kD_9;uM4fGHGw3^t1S)IeG+k7j%u9eznwA<(H zlyBYe$Jd^(Z)q08!WdJWot<02G(ST1M0Yh&&KOv@H%FEWKz24?*aKHXR#KzrR%l!2 zD{iBl0+&PbvXsLgS`NxRd-lxlE3JO>q5e$QssAem(zzA44vOy;Gouz6%nuZHK%e{l z8HMy^`?NrGp6TZlzr=Qsy)Z4xgwDw@iCskV!dYcY80kuGbVS799d7fO6kEP6pj&8f z_g@)L7ERos;ycN8VBmL>AuR;Wr5eT{GS^-FBVa!!^+IxQV8S@IPU0|nv%l|IY@2@& z1EWT{OqQP9KC|e{k1*JOQq*6xd9dK%1cyLo!aY#b0kyg|N{I@D@uje6L(j^~6r76C zDkjvz$BK;}I22U4?(K?D{4@eUF_VgJyg_AGlSeX-rIXQocWiG_vN#!SZ^7mNUT1)^ z^sYQI8|S4G(Cjv!$V18EYV@S(gSx3SSBiVl*VFeM?|jz72`ePwrcd6lj<`coXu%49 zisP?DwQCEirhmK=H+D!Is*5^PP_3@SC^HJgTC)*VFHvK5(?P|PM z|L0q8RZb#p^46Xi+SQ+NSV-}m;|vHC_~8*JbvcWxY4?Tz^N zYYH(M#pa$xgIzbq*N#_R9QM4JZS+L$%F^HdwY^5Gt#INLZM8cZc0=)|-ya%Cg?_P6 z{e1z8X7{8_+nul{*e@BQjvc|DXhnm~sEI$1#6?PDteEc2>S)HHB=)MnHZenhagGn6 z?-@L;P(=thMuAS7jwP5RgsuT3vEb`1c~69}3{kZko(enIL0l_<3=MMu!b^@K&`n&X zu%JfC?d-G3)eSn-pSjk&yL(Uzrjg4X4D2^lFP(VCS#13P!`i zmU)dG68GE75t&QliCm%q#xE)EjiTyd5RJ=8MLGQ<#zTvUHHQHoG{7P~0hWCT^KRtM z%U{|{+_a;@S=vmQeQ=OUd+g6f_cmFl#@t8i+k@{$>3;Tx!-6->pzmS{-=3qse!^yJ zP61;D#Y~?s|8pp8auifC2YXnlsGXf10>}h84=tG)9=*X^Z{qD0a~|1HF!ifu&T2SI!9I!p9+CxC zYci*_e-I$>WH(tTO5sgiUxRb+2i}%^ei>*IiuiTRZ9c(|4Bj02Moi`=fO^K8y9d5G z%)b9Ur$o^JAmeo8q2>E*%`^fM0;JW7Bw6UJSGj%e?OJfiL-WuPNnrkn`~up$$$R_K znAU@U-v0+O$zXbvUyv2!cXTi>)drLSN&CY$>rkIE+vV3!NFH`!6|YYhym~QV8X57& zcBV8a(IZZDR-%^cIc+vm-86tuM1t|?+u=pTVs8fd<)Qb?rFRrUrI)CMz`j$X?1Z2i z6YdyB)NNEbIT9}y)T|DiR)s{u1V;|ZM2~#TYTQyuo;Kle2S7CdANCod#X%6ceQciO z_5ub>YCmlkl`1Oo|3v?fVn8Rj!r(9AJo61+U3Tvmnc>sir6O`GYLW5fxe#_oK|6^n zjr3Yf;%C3A!J3PT6@&RbQ`zHYMBK$qgmo^qzQB0H>F-hQiafpc$fQtw{bBGhv&VR{ zfQTx;yv)5Dz&kgEl0h-?)}?2o(~yxU{Mq8iCObvgVSUg2+072&$zOdeV(K)>Te)$i zQ?T;kfvQU=UcJ3c&>z4uU^^G4Z}s{*Xz5u-Ks<%GQvg`1^X5e4L%n(hY=jw=?O`D_p=$GP^KERX(StIBZ20DKmUX!F08xDXXT1x zHLXMcHM-8!Li;+4VXOi2`E88p3fySto~VaX{rStj*WNtFtgIl&d$oLY6|-JiR9-Wz z-L)soZ!Jtmo+)Ssyk$57$y3YW60;;7q=XW|^qMnZK5orms;d5DwFBMyM`B?Y^b}Iq zdgOMFRzVa1dn@SP03*o(F`}v|xrYTT+vO(-J%fXH$iJ-#_#HDkp@RZZ*?-%p4%`$@ z{-SV>c6U|>ARuii@Z8Rk+cz>%(|9x|GH_X*1=$kv{BtyKSlP1^+B;ccucR`!z6%YJ zScQ_VGIKJU76cFUtTh4|t=`P=1r|_{Z4YG>v_Ba41w|el3BQ(x2EKPgAkZ9>fs$8b zIC=?}0I%OA3fC>eV`{A)x)ewPcygvkmwJlLIvBO9oL=Yke{o#$M!=w+#N^B&%II`7 zzzmWf{Y-~f4Hww-IM4xQA$H6DH5Mcwqj0y=8%oph;-2M_vAr!l8K z<@e%pqiezl4*RZx%2hB8#dEd94kk|(;fE%u=v#z@c2WP3wy&{$NIV&m*CXBM}GFR_+&aV%` zpWto@MZb0^-M`G?WH9&gRC;>wtO?OA*xXLj3_3(_Km}7N)!i!E9p+*u)7Hm4Nh_gF z{xuhSh24!0s0=Y;88B7=w9qvZw=kq&8@!mytgN;O*Tgwe?{5k!^bp{L-tr!`x z3uJHKx_xK*;JpX8QfC3XE2^YZA_o9_fIK5ef010J*Yl;L0bNS9Q*@W9fb>1%-z}xW z^)5|c$Nyp#88n~=a-g}ubCtnVsWZ7};cZkc2COfCT{h$jezfI&ft|$GciGQm`}ts6 z-0*yj*>%is_X)s?n+3*9OaBosY|R@C8o~TUU-yLia9Q;L8QrTQzH&Pa8>nJmdYz#5 z@xMVT_4d?57I*h`2RSk+UW#u1k5j38)ta>1X?t58?6=Sa3+XbzOsKy zA7y5x>w3*KpgXY%tTC`krx2R!!!G?$A>#)%|wY? zs-aljIE_HH>K&E%g~>-ed9drm^+CA$?)FK-<4nqu4=DWRbF!)Ja7VXy%{^l7(lXm( z8`N_zy#=yY1KdwFDKHRByC;Dd}{_5Z6b8@NgEoM|QG*Cv@ zAg3dAjqG8{uik8Z`qX<2@?@sfYW?@olc6Vm#>@kLR!C6~o8lE-J8Boc%aE#AE+ZPL!9y4oH$JX(?3N5Y(za4;GHCpSZ0@a2>=s_ zB6WDmgqLTzN~|KRYk`de%+Fs|I%M$f?}mXD!cXS{p14?m(hl2lO#UN^1W$4qyOM(5c&iSDzL-02M=3Z|#h3f(VxxBLxU=bpPaG zoWe}W8qY*ClfY}fOO~~N=1VO$*S(@}=4wV~VOZAq%Q?TWk{fvZJC@g~jrN%MuO)AP z<(uF6TvPR&M5)kA+=~EQ4L5X$quH}m-&?V3>dd;Xiyw0XRdAQM<)LK0sva#4qhzql zVcPmskZ=#bJ%y;Sd=d>-{(kqQeuvD?bVV&ZnMc=780>dOO@;zx<^>wEO`rs(hZyPs zO-g_Z0!vt2UnW`~Sumo~v-81*N$~bjaMn60FFVaTj2zrg^4KgI^JwEMQaZ2A!Wd7w zTtmaYMB+9+{$SJjVqom*YN%}#_GpX=VTMS=+tVoo0^6!sO zgZ{#M)3k<~7g+kPzKN45Z|Bw)WMhKya}~->*Pqe)?P8a%rnx7_(U&NjKY9UbOJ}9` zbi0vJmp<8K1O9-P&Ss9zP?Mb$GfguTgAT{4xCM{fcEWV$r1njo*r;;M9d`spIr+Cn zp2koS7l5A%q|TrPZ@t@na?Azq-$MyjDLpTQS-HRUeFD`h{O?YB)K_VDCu*rS=5C7O z(cv2V6d#3hCS*(AJZN7!K+e_{EkPP{+{PbwFOXLZT+KoMXz&Cyyx~(QXW}s61I?C? z+HU3YZB$x<{Gv1eZ~m5Yp5`S-#)4(t!KUK==;V}WcseP^hfWf@hNRj zBAA$j1B-pvOa3pJPNG3Di|b1u|Gx*-_&F)^>gQ=UU~jl_mBMsUnzYQ(ad5BM=fd(V zd4mi{LQVSNTS5A<#B-Dsu6v8pI8^oV5vdK{2HSL%*|F*#I{7!{=G^s@ddnL$=ouc( z(md~hu}L78k{jfWD+D?*6+dsuljmoB=qyh+ZZux>-DN6<IB+hiRH$XskReYr^ zuvb2y(Bf+G8tb#HA*|u*UI3HxjM~131fDSEX(!j~yV>KrTpy?lhjObwq!Skm@>X8? z2}=ZOP%>(y`b=^QVd}nDu71gji((7OcKgS`ON78H3;+tDH$0QPCjlY~SaK`7Ml_w! z#!s0ifTIe$wF;ddMq0;&Y{ocUCY@dK(?a}3o&ubVq4o{evLo4yy|LXY;!=)j{f9H6 zYr6VjEC(w`yMYYFUA3gA3ZE6LvQMJ!5~gw>JJ%igH%jbu2*q^VH$4kz@?G{kQCSdu|G#})A8w&bqYXl?cx8{D zOUo2WUg3vjJv=_QHb`2VGtumBs-f?*Pa(i(WOH1qC+R zl#s2))fAkI&?<##4iJA24)_8PVRU;ndAuF^v6qxOkEY%ZZRLLiq61ha;OBf|#66X1 zZvu#5APkakoqWzoX)9cBAl>3p&wOpLAcD8^SbJ5~dZIN_jkQa#TNAG2W*&B>Rh1d` z!T|Qd40?m!tzr_1sotM^(w5p@uFxC1hH#fO69Tiu-4@`YAdiZvBKfa9M6x}Gj=cd+ zk0tLX=6RUnddl0oRj+zuRl?~49TUauU2EsTG6<C*S$J$IerZIyBIF=|=Gy zTzOf%sJV;#&CR7byoiD>|8f^^Sp|N_*`2yE8wPPhP=HEjLTn6Bd%@!$L2`5CzBc;o z#OEb8WKwh%WO9GHz>Aa<@A}gZ%Y=qn)`je+z5b^l z(C(>2*8QS;CH(RGe8(3KFjIJ_grDKGRDxaktZvq){!r4vB+6L(89FWYry)n1CGK~C z-JH)Uq@NxG7qzpt5Uy-Lra4P5B0gZw2_d{L4BP9LE_PZ}cN}5ce%pdLuDe*U=%j+; zCE3Lufc95Ku)Z3M_R;tNUs7VBJD1=mRc@BDuC7 z_k*@OUpmBez;|1&*PmcduJ8e~G9jedB4NKl>u-*KRbp@Xhr~BF2+7By>Q?dLp42fH z7|)@8yRw$^Mp?OYRo2?kMA-__GivgPn{2_fkrHDXGtGI#BE>0(ruSCoKgHb-tr%}& z6)=guf%LAuX%q3Ksa54``RCamJ?{bT#$IGuDMi+Jj2}*qMM87FqeZz!8$tus8~EW( zHb>!Gvkefb^RwDcaS!7L=SCF9zq^#he0U*-v-to6*n`<^VS{LpjUu6NEPqr>&P!-K z5J4yrE*Is91i?}v5uo45wO?ZW1Q^c1kpcEBAbZa0^`*-sKh(b2HdA6(ru#y}5iU~@ zRSqC^H`*q{f#fs_rj9D#Uq%qJdw|39<-Xa9&f@YK%_LS#cF3Rp9|Xg#>zHcRMy#Q7or24NiR~{D*dw(M5VP6eTGOa5uX}+t?}o^6)1kLA zNvD(}hf9AuN6u;NU1z7}W&J*Z=d7v=yK=)ycK5gX0!hy{)uc>u?yPdpVG7;bm3QN= z-e2W>nl61eq9z%4gO$-upI%c#;Vax`%fq5yVlL~V+7(*bXy}_aXWPbeZdzAPrV7X_ zC0Z=&v?HMy!Uibm70EV>`C+cMVJ<9K56iT#pO*)b!f!8;E3=$n`e80h_ zsl-rFQ44FG{Z*F|a@_O8q}5*|YO=A#&=D-A?6gp^7Pg)|H8i2jZHMPAr{~kT&GRC6 zy{fJ>L&Kr>@N3D~7|Ysx(+!&l0C2xmuci0>EO!rJ{Nmy)>2n}SYtO(3CHuD=&pf)- z0>&wFZpeK=$lwKNef^Cj(R}wPK^A4LLfARtdX>cIc@2&vf>ZifQLB1T(txwp3Y6jTo2)^ZUo- zjh=3HDle_K+1n091dF^qe+zqqM=eb1d@Oyqey~YuH7b`8Hg_9vAgk%1;=btENDxAf zL&@gPuro#GPEJeg+GjCh+F%f~zu)yN4DgCZaN~vm7B~yKI?Nj1Ay31Xy{}e&nFlNq zZaL`8pf4JY!2pP$S~K}s0`kXK4y_oX8a_JDP&BL`9rC_204nJ~?ZC?}BZN9!YF zL7r4TO!|)7o^q?c`M>KKtocRrS1ty3TN5Z;$wmX<1}Ry^-PS7qbXanw7JfbyJ^pgh zWYhh=kQJM+ezD8jU}tsVk&JHbfqY)&G(sO1OE(F2Two^<*S>?-4v zD^3eKeEJ>8Isi$lwT)PYok_DyuyJ|+(EC8RPr_p8lO&V9W9X*nTIv^M6OIUR2P zT%M1<%FA^76^pwrRGb_S98CVO0bKj zKDmJ0+1Z`sbyS(F3y?YVMPr^~4ZY3tf$Rz2cnKY!XlBXe5=Z=5F>j3qEL}U1xHp^! z`KC@jB;RGX>yHpw9#W%Vht|5|?nkOkFh?1ZLVtM-g$d}Dm<3fI4(b9Zn&8>9ak5Z= zXlPfVBC5(RG~C@yavRhZw4Fe3CG}dvPn8D4N;|s)FWm&+_$0+m)Xg7=iqd&p6%9Ra z2zi}RO0KuH+6-RqSyb>&MEsc)qs3bcamCtVvlzUCNeW%N>)pz0FXD&)10Hn)1OX)* z1gh~CD36kakls7n_D1+YV6yunWaD((#OWYHDxprV5&1T`U>pp{l>nZ_?7BEII3XHM zL!OL*Mf3i>aQ{9bk!kZA;3E0?(MG%0!wp18DsqNieOyoJFrHMfm2JZC1`J5+y_2kE zml|yG2Ch?X`@m>$U{jL5-=GNXr6WE%LFOKSZ}rU&>^3ZTIP|LqB;)^y5+Bkj@J6OP z%|-(;HwqtOj{Mat8iy)p$R@Do#&`E~E3)w^)XDyA13RLHZN~1Zj>KxFq&iNHhog+;8=uqx)iQUu_ z(d|+V$K|w;B1>vi_S_&Pz#5jTrI{G-%IyE4(TI$3VL&wPBNn#bpZniu<{qU^w%!Uw z2tHI+BXfx29a_0U^GQs@T{%zYaoCa&x)C0^P}%0+6Y~Bmw86ZtA~qF0F<%GQKmS1@ zaF4z9{Syk94ul=zX&LZw#Xp%0`|Szm%0C0k*98WKVirl+CQ6rSKdL)W9t=9N4cp#3 zGxOX-(B0wTRvo^NS6+ff{Dtgms{aOxS{j5f{NB#cnj@QwJegn8$A312m z6DW*4BfJKourN>Udq_1i;Op86)hXqDxn9f!D3RU1_56esz&PqD20MP_ft6{&b46Dq z33artfBXNTAqAXvt$TVyaT%bAyjnnbkA1u5UVf#K7b%*h|HdCN=|K9>N+=q)xlsaW z-a@3qn5jj>Yj$g37v574E212Zt>nRqaLruw91{ju#x`;0NuPKqfG+ml7S^aP2CNHc z1?dPcSc?Nlhpn%pzeqy?qlB|(bq;Ky_;gJ#hAq-?_CT%OyP6(<;t@Cqpx|hP9F7bU zyK=OR_RO#G>2LziyQh&C6wc_(5cigANbRYWwQKs53kM%?wB<~?4_t5G)Lyx0CdAp> z&1oS#a@{{SBZTGByF^VWx>}<^J$Zx~W5bVG2hqqvRK7DDyf~|57-$W?IJ&^a< zF`gjC=i%}0`jH1G-!caED(yo zvuLZEaBX-CbvK>liI;MEPy8|c7gBug#K-0Cc-q+cK*FFdFreHw`_;y74QF;U`_(+( z?u_KkloXU|m%k8LjAP!za4{L(#brE*eGDjqBO6_MMG6S4EpP*~@PWknDydS%ht!3k zDz$8hnXpm)uOoJ_Q*pd<5BYkB6YT`*-&f4y3Z>#H7ey-)^Gl)KkKgq0_*61dM#T7zkTyd#mB!-XFFSY z`Zv`mYYc$iHQRlGScc_nCjI7*op*vwY?{?r<=)#2Pi4x}Eyq}Oy^|?k-)>j=J!YT! zqqcFquICVKHD=p7mG=AWeO@}(BpE+si;-{?mH(<%&|Zu&?2b2Dba49S10+-7mnLi| z9yfdsVX|ypE?+G$9l$<%BGQK%+Zezjjo{C&ZuP>4M$)hOq|^UuJKz z@1X(lgbfWu0xSDZFRyr0|Fec~kc0x3MR5Mu+fj|(R5&VV-}jnNe9=@?&hI7Em;ITW zHsIAI0B>*v8KK>kl<=-$y#ix@UALyl!H!QJw{=g>NhS8;5-6Ob^*T$p1maR2q6%YK zeh?NMuWgztlEN%wamaecS`}P}rJTi(kjoV)kylMM;GzRF(H9ke?i7l`#)R}A%!^aaPpdktvkA zwngWSYPDC7uviyqb_Sxwt%X+~wL6drQ=4~{VGqr|x|!0kW!-7!%tG;jxaf2U4I@ZN z2298vnYjG5&CRGtHqGXO^wCn$a!xf@`*_Qt>`-tsq??@svs9oz`GHLckQZD-GH1eU zs&<}hm)WIEArp?HtnVPFul{`$B&}+-N_=k8-!+Y+RA&l^VGnO!Y9mW%~_!XD&ciyN#a`Vl>l>j+;m;jP!PC?az0& zo8m&(7sh%a?SRtp~yw8>Cj=bA2yCJA%bcAy?Xn6K88pUMx9R>H5CSk<9z$&jIv1k-;84={-Sh7XD7>u7*5 z*v~5XngWI#c3|ymc@6pfC7|-Id2jq92LgSU6g1)~v^a@U&?aFVSRL&ilP6K4cKS++ z{km`u0m)!@g-{f)Zw6g_cmLQWT|9W`u=cC8naQ)jY{w7BcX_>|Le(gvJEC=(WFgUn zy5ZSibv6aBY-8*smj^%UXRVAJNEC2&b#?(t2Aa6@=k2F^-b+bL=i~qLm8j7%V&Pmc zPZ;RH^NqkEtBaL&WS{fdw9zhp`|6Yi&okU5mGBk4A8t4$M0A86{7&I3kZSh#+hDr{550h@y^2^f+N<-jG2w^P_kh>~B!+H}N$BOldJ!eguCDkngh&qJA6W?!S8Cn? z2Av#8%j)*kyjQDL`Dv}8wDx^2O1l+sH8?b!3gUJ`jXyxI~e8F_U-6pXmyuLG|G9nnIPjx*wr?Qb6)ox7LJEsUoz-^>?%*uA35ox&1{N0%iZ z*LJjT=2UK5?x^0$3je01JY-O z=K>AV5A#pWYv|8Oze8*ok>3Zw)x1VFqErgUHogvX02+z_4WT*5N0)x_%mH89pgo}G z#f#~)6ritAdfM1&jb0*?qa6($Yld@Y%nXfd?9I!OR(rnlF%ne=51Up9yUzZ%w7UPB z>GX}d*!_mV!+$_2JY89BZ)r$`N9M?)xsYn{GD#Cnj{ijudf_G`aV>!tJHCg_p0m$g zZGxRlY@AG$^4g<%^=}&6#z@v&6|nyDIO$uy2b zXVCRtA|Op28)jHbJ)2~9Td!bpX^(qsc`=hfJrl*hhX(GM8RO1FQ*(MdmfF^isLwb& zpIoDlNNpab0S++<%nW26fb-FqyT}dL^~+^WI2W7W$EhI)WPd+0lN|ybip-P_4qWwz zbG>|{PhueBvpCiFn_}J-^1D=8PafZ6>M4wb|6{hzJA4DmL24fLtfct`snEw(I?>;u z@C0@bAE_H{!yyk1)%??1$-io=U5EKN8rMgGexxbRe*HIEuzdYZm{&;;b!0fwBA ztzcrzC;o`*@79TKBGQ25Wydvvlk#~E*k)XUW|3l5Mt~y^@bS_3N4La-j5xfl?gm8=b5 z%MGRj*cQd#x4DezjQ^?G#-2C_2!}`qoIqL)Y)e~}Z%?`Tw|5rol!xTli08m5C5!=d zM%AdOq$T>w<}1n(m>Pu>B}LCd8iPnQI$zG)=|<-_cpjM6+n{H57OQm;hYEEuwHe`e zg+j4QuF;&4L{LO`t}w_oSznop>L)47kukdo z5{sRQZ0J*rkQXxKhnpUlj=N>Zuy#QcG5PTMq1v-za1$~xB=5BMd~+wL*4P>^{O<%) z(Hop{`dYMCFLCx~z`%*0BSUIa97V-iP1P~0LciA1v#x>+ z6|l>`?KTo*toovh+CSWHYvMDLGtf;3$6Z@I**jq6RdQVf!DLkpJr=`CqKmb`h^z^aFx=3lNCk6-j4$OyroLmr&WG zQ9&1hqZz&qd(c_V6v=BcRmiVt+pusGmMTyqOSaaRkt)SlKU8Y9{7Iw#1fK9{EP9387uKu54fWhkNpJ>$ zg=t4jFFFTsHh~8%~B3hUNN(xe40wP zHCk+G0;r+~*;gsaWk?O`@>sM~+T|`C$zS1>CY^9KAJ4MLTk9Z=6R{1S5lVfhmYPic zD0n2uMKS}c-T&G21y!*;NI@H+e7nmtf&ePD42AiN59aBox*z%|WEpU}Z-sKnZTTwQ(N${}N5V^+Ca zpv+EX1J~_{9}wzZnYiPB=z=ep68Vkx>wi8z2wf@sMSvXlIw#x(Ga1Du4Rx)A+NNr@ z%~?whs`1~_QWp|&F>So#5SVSWp`pkG-mK}+8#D7AFEX+pE_FqiVco#!`vn)TTMco* z-ISX92`IW6i=DA*y&FI84%&=nCF}ux0qDLZ#F~afyaeJ4jVF>(fUykbM~fW1q`Sg- zZI+CH13@@OO`}r#UVPPK5>=b^X(zgu!FE2 zqb9&Uj0+;R{a)sqG`j2EU~C^t%E>Uo#c3`;aKb5k9rSw?&A~Wya6u3lp}zyW?LwXJ zFtChO(MQ_-AqP$!7O*k=)B;ZYU)XMj6}lk2-q;Q##tw4)gaCJr#~*=n|1x~N%td&o zF+45POwEX(eeLiMx7V`z(K}&m-X`ypTcFp)x4f?yaL&!FAExs~Kp5{oE`8mYFAXyK z3<*qIV@&T!i8_r+&#Ur%BJLEhQR#Mo+a4Ll_v@fnAAXlXZc=l#<3X6oj>VBU!?6(d z0V>~dqwsVSjPKwfg{^!Z_&P#qt@rA90=Jat`W4UYy<$Kn=+aHJKQjj(8de0R#RAy| z2Ia7@xmH56(j(ye4d_4Yz`USM%q9IFyHoxFAq}KTz#84z9)7fL=c-fym`_L2-llf) zk0l5cnLH2u-<2q89~*dO46S%pm6mD(PRY9WB0*?z2p$(*AV}$I#iGkLqcWTI1mLUen(acgjaT4vl zz99NN>EG|>2}9S}sIGi8XIoA&@MB=u)H4=vRu^#AVqCk(@Kt!GMOA8jnDRyOmLqQY zSG_c%h0_ga@oP1Ct)VKR&o#kv%%eQ5JQdh^R2#4UwmRe~?7DW%cp3I5G&N&*1@+Wm zy{b=XcQ$}ady~(aC6Ry7;kAvLwbrx+DolX`ufgsD*&fr%0>+toFpXnmf+V!V6!{%+ zH7P|)s)q{pc`$GKcd;s$v1L3icR@eW97HMK?|W*&XmN%ChWL0P|Bn3(Jv8ffHY73` z@<0;#9lW5%V0Sj+JT#G3S}JJxD(vT?*VRQ7JND`s(bu#81rc)lJS0dgFY9#hi`$F_ zgj@=84+~0$(88YE3^WHap}|cN12*i0k}T)JEyBIJ3XWOZC)-Cn9*;{LgA$SXOS?Px zcL^}@D~2XA#P7Yzkd9Fboh11=4Z8MlFe{F)!rMI7;|m{ZmE@-?1l5630-VM3*FmiV zXfPN_Hja2~xB?I3B=GtyFWejy!~(mazI=jx=|;mqG%Q|}%z@T?E1w~8%6JSKN*O3Y zDyvh<8k^6RzqQ2ZDy^<>M$m0Lwf(Uk$PEoZ$T)6sWxcKXmc(<5E9p~kHA*8L`@dZ*5c1l76=Yz>8 zi*7V4AP=ep+fa)}dak>SEO-={_k8O{rlkt1II*N{9KFWDl(e3_t(?GfRyj(?ji^L3 zLBOn;wgLn*tZrja0Q(Le1uGs+@NN4EVU-5K$aL*yV~zgd_~2@P>{YLam9X9pC={{% zA>m-H@H5De6T$zD86K`5#+4%=Rd#=84yfEU4V+)Cb{%8r3J%Lx;$K3whyGoNMcUrk z_Roa$4h%2={(_P-$sO_#j#(-f;R+$g%}n}}sm9=rJE8VdnIJ!LZ2L=CA>X`V^zeCg zz8ND}XSqhEpC6;PKHou|aOg=fL{9)Mu2QfF_WQ@+y*42iC1iI9C=w}n@}N2yurk_D zxg*jl!$tshDnJb~DiijJm>kd6oo{IjpMaXIG>jzVJ^=hRshk?@TkSG)!xDv3PSb<1 zgJ@C^p!rtL%(M1yS=3&l-56mJoVl7%pOhrx{IjUEbO`z!Y{KR+7ej<15)J#hn1-#5 zd4_w{>Kq>`*;k60hf_&3v|%6Y7PRklP+TrhFy6Tgc=tDfc4DazBV;64k>m-cz-Z-Y zuF?tEoyu#)3!evQGTB!eCoVb<{?cIrAg-$sP||u7BwzvIlBP;e!;1 zHE#e4vsS|bU0TJ!SEdvO*xYjaGiNwR7r6h~Au=?`XEDOOE)B|1;|*=Teyj=^KAqbd z6fCey5d(6C(t#oQSs3!y=@MioMuI*U=2QUFwN=b?Q~P_(imz_pG9A_EGX_~#`g&`x zzH~5#D+_qzpjsrkOsMtvz3&XDE#T21+56S8t?;Oj8LIa4s2DVU_$(PQcUp_u6CP(u zlfAX*vn2_KjciMk{bki3h)7`_S&QZ{xd)Y2N9y+27L6Mb&!PQBSi9q4r|VA)3~U*& zvf;4^a6-%j7Qy{1SY})Ng_nMWT^wTz7FlJ+OJOgboI4*5$c`RMd2Depzk)WMQT?4) zMq3&G=w=kB=>IA3QVg)FoN(aVO#xW*TEDVM%SXG5svR4SHGXqtF@ zq`X2V#0K~)f7>^tsO;UZ7e1)JHa5uIOGKXzEmyU#_U$9gLm;K}t8?l2`b{UOq%;9%cwaBAz&k1NHyom2KEJs(}1DUyV zeu8!X$10UYt;7x+Q-xgTaY=AocUS|gLb6qFB!VihohHc|d`;PrFD*0Uc30{C;Cgb4 zsU(mU3h@FQ&e|R+SU=OVAh>1~?A`F1lu0idvy)iR;9AQD;Nf5Uhm2M_{);Qlz1YU1 zmM30F5Y!WDi~P6Ce|d|xq@43qjOo#%la0z;ut>3Z^AH`xE9DZQE70~pxMYb}W3iPm zfahk_SK;#b+7ZrJa<}3PLmB z1!KbRF{$BbmgY_!sO|?CvoDMU(rUHtsnqnpG8zM0g}DUk6-9rQ^hB1GGbOP5dIVjbTOz*HF(6CUsw>(@KB<-;#5OH7Tw5JoXmi6%{ICaJG^_4)2Sr9-z~)<9O8tIZ1U@iL}fI_KY+#D_5BnoJZsL#g}9%HuN~#-l#MuC$@Guh-&{n9 zAfSTvIK!fN2V{Eq5zSw`Sv~JY^84g{ylcbMai?n-RKwBCOs)-Urz*1*nlv#i$+ysS zTxf1`f)UlRVZjsUkb#_PKprca;@Cf>eJpeDi8 zg<&tk8A0f--Z;)4L@2~(IrT(@gGZ>65{$L*iMZP>~qs!>+5^nZbFlu?I<;>b9v9#*~`qN zhW;KQ$vNk|?qtC@=PCb%s-9HXp;ha}trAOjj?0t^mqi7_7uKDE1yX05%Ui8p>)K;v z*qAGBeKPiR!~s^m(XDBJFr&^T|p}!<;wM3bEDmGgi>DDx`GPmeO;q>{tJ() zg6-2MbZ5lVKy1uhP~$4BBsj4iO}6Zv0%;$<5d>q_nzi1s&pvUlLwDfEp3Yo|^^osS z;4Qa}8DDBnpstR}XVbu^I7*rH5#{d`e_92}l5<2S=TCsa$?kB2Q*YcOcc`VS{S)Cm znR;yAs~gMnyO-Y+{KF36{dgg&vpBf9e6;tM=Ck<-HKi!)k`a*^^QvvP;l|G|(ZiBo zc%4OpIdUX(<=v{t%g-y%2s(4-s)dU6ayxsizSGO@=F4sK09v}jLQSZtJ6>s%;J*6f z@_@){Bpe?Ue%IGE}t$UsSjyh_98S@#;JkBw>xB#D)) zBV!DVOUA~==+=gEqk#wI)zac7G%&PCJF{Yw2fGf;D&7HN7lhNfn+!U?jH=Chef$TCpfd=4@=p>g~fyVpby3*7mC${)KQzD_PU$aeT?=HCG?MQ?hv;5Yjf z`>Fx|!?Pme@n+6=5wvMi=!98B1A0V$AzlSKp9OjR?cnx=w_f9LadC0$JVyy=NJbBQ z?i~*TH$7HBOt}>di+S0cffPQN;lt0dPoZ)w6yk4-ssO_SWC^!5b|Lh}5V?Kx%%APf zWK&TJ|8a3&CY=@^o;j-1^L&OWED{LN9php83h?;()gBDH1US{I#^r63M*EIz?R~ zY=LV9u-P|EgG8fPG%j>A>L`3oP=bc-I!+o5@q1MPTDAj_n`nN6RO-=0VW;m6q9cB& z&7U4kMT1Y+_$1*7_xjQagYn%=MVaEC8yaRXTHRl!(J<-IL4GB|JP(%rrk?&Q>_5P=LD}RcKBoWee+lmleZSw?Krx(23XSx!CgQL)CP8>!uzTP zfD_vZNr62Ndcd%;F*iSj59!%IjI*Wg3O>?rC)L})g+7OxWas|n9ytu=NzaXmOq>Z( zi))y@XUnO2=`!K0`LL+g=Qr`;$jZ&&CNz8| z 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 DB$9&s literal 0 HcmV?d00001 diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bfa0690b7f8aea8d793026b51f9fc4eed2e00af7 GIT binary patch literal 21677 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 ou5=8}@A^#05^yFjv{|%CV0m?W0y8r+H literal 0 HcmV?d00001 diff --git a/build/windows/info.json b/build/windows/info.json new file mode 100644 index 0000000..9727946 --- /dev/null +++ b/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "{{.Info.ProductVersion}}" + }, + "info": { + "0000": { + "ProductVersion": "{{.Info.ProductVersion}}", + "CompanyName": "{{.Info.CompanyName}}", + "FileDescription": "{{.Info.ProductName}}", + "LegalCopyright": "{{.Info.Copyright}}", + "ProductName": "{{.Info.ProductName}}", + "Comments": "{{.Info.Comments}}" + } + } +} \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..d7492f3 --- /dev/null +++ b/config.go @@ -0,0 +1,224 @@ +package main + +import ( + "fmt" + "os" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +// DashboardConfig is the top-level YAML structure. +type DashboardConfig struct { + Settings Settings `yaml:"settings" json:"settings"` + Theme string `yaml:"theme" json:"theme"` + Connections map[string]ConnConfig `yaml:"connections" json:"-"` + Queries map[string]QueryDef `yaml:"queries" json:"queries"` + Filters map[string]FilterDef `yaml:"filters" json:"filters"` + Sections []SectionDef `yaml:"sections" json:"sections"` +} + +type Settings struct { + Title string `yaml:"title" json:"title"` + Refresh string `yaml:"refresh" json:"refresh"` // duration: "30s", "200ms" + Width int `yaml:"width" json:"width"` + Height int `yaml:"height" json:"height"` + Columns int `yaml:"columns" json:"columns"` +} + +type ConnConfig struct { + Driver string `yaml:"driver"` + Path string `yaml:"path"` + Host string `yaml:"host"` + Port int `yaml:"port"` + User string `yaml:"user"` + Password string `yaml:"password"` + Database string `yaml:"database"` + SSLMode string `yaml:"sslmode"` +} + +type QueryDef struct { + Connection string `yaml:"connection" json:"connection"` + SQL string `yaml:"sql" json:"-"` + Refresh string `yaml:"refresh" json:"refresh"` + StaleTime string `yaml:"stale_time" json:"staleTime"` + Params map[string]string `yaml:"params" json:"params"` + RefreshMs int64 `json:"refreshMs"` + StaleMs int64 `json:"staleMs"` +} + +type FilterDef struct { + Type string `yaml:"type" json:"type"` // select, date_range, text + Label string `yaml:"label" json:"label"` + Default any `yaml:"default" json:"default"` + Options []FilterOption `yaml:"options" json:"options,omitempty"` + Presets []FilterPreset `yaml:"presets" json:"presets,omitempty"` + Placeholder string `yaml:"placeholder" json:"placeholder,omitempty"` + Debounce int `yaml:"debounce" json:"debounce,omitempty"` +} + +type FilterOption struct { + Label string `yaml:"label" json:"label"` + Value string `yaml:"value" json:"value"` +} + +type FilterPreset struct { + Label string `yaml:"label" json:"label"` + From string `yaml:"from" json:"from"` + To string `yaml:"to" json:"to"` +} + +type SectionDef struct { + ID string `yaml:"id" json:"id"` + Title string `yaml:"title" json:"title"` + Collapsible bool `yaml:"collapsible" json:"collapsible"` + Columns int `yaml:"columns" json:"columns,omitempty"` + Widgets []WidgetDef `yaml:"widgets" json:"widgets"` +} + +type WidgetDef struct { + ID string `yaml:"id" json:"id"` + Type string `yaml:"type" json:"type"` // kpi, line_chart, bar_chart, area_chart, sparkline, table + Title string `yaml:"title" json:"title"` + Query string `yaml:"query" json:"query"` + Mapping map[string]any `yaml:"mapping" json:"mapping"` + Options map[string]any `yaml:"options" json:"options,omitempty"` + Span int `yaml:"span" json:"span"` + RowSpan int `yaml:"row_span" json:"rowSpan,omitempty"` +} + +// LoadDashboard reads and parses a YAML dashboard file. +func LoadDashboard(path string) (*DashboardConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading dashboard file: %w", err) + } + + // Expand env vars in the raw YAML (for ${VAR} in passwords etc.) + expanded := os.ExpandEnv(string(data)) + + var cfg DashboardConfig + if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil { + return nil, fmt.Errorf("parsing YAML: %w", err) + } + + if err := cfg.validate(); err != nil { + return nil, err + } + + cfg.computeRefreshMs() + return &cfg, nil +} + +func (c *DashboardConfig) validate() error { + if c.Settings.Title == "" { + return fmt.Errorf("settings.title is required") + } + if c.Settings.Columns == 0 { + c.Settings.Columns = 12 + } + if c.Settings.Width == 0 { + c.Settings.Width = 1280 + } + if c.Settings.Height == 0 { + c.Settings.Height = 800 + } + if c.Theme == "" { + c.Theme = "dark" + } + + if len(c.Connections) == 0 { + return fmt.Errorf("at least one connection is required") + } + for name, conn := range c.Connections { + switch conn.Driver { + case "sqlite", "duckdb": + if conn.Path == "" { + return fmt.Errorf("connection %q: path is required for %s", name, conn.Driver) + } + case "postgres": + if conn.Host == "" || conn.Database == "" { + return fmt.Errorf("connection %q: host and database are required for postgres", name) + } + case "clickhouse": + if conn.Host == "" || conn.Database == "" { + return fmt.Errorf("connection %q: host and database are required for clickhouse", name) + } + default: + return fmt.Errorf("connection %q: unsupported driver %q (use sqlite, postgres, duckdb, clickhouse)", name, conn.Driver) + } + } + + for name, q := range c.Queries { + if q.Connection == "" { + return fmt.Errorf("query %q: connection is required", name) + } + if _, ok := c.Connections[q.Connection]; !ok { + return fmt.Errorf("query %q: connection %q not found", name, q.Connection) + } + if strings.TrimSpace(q.SQL) == "" { + return fmt.Errorf("query %q: sql is required", name) + } + } + + for name, f := range c.Filters { + switch f.Type { + case "select", "date_range", "text": + default: + return fmt.Errorf("filter %q: unsupported type %q (use select, date_range, text)", name, f.Type) + } + } + + if len(c.Sections) == 0 { + return fmt.Errorf("at least one section with widgets is required") + } + widgetIDs := make(map[string]bool) + for _, sec := range c.Sections { + if sec.ID == "" { + return fmt.Errorf("section id is required") + } + for _, w := range sec.Widgets { + if w.ID == "" { + return fmt.Errorf("section %q: widget id is required", sec.ID) + } + if widgetIDs[w.ID] { + return fmt.Errorf("duplicate widget id %q", w.ID) + } + widgetIDs[w.ID] = true + if w.Query == "" { + return fmt.Errorf("widget %q: query is required", w.ID) + } + if _, ok := c.Queries[w.Query]; !ok { + return fmt.Errorf("widget %q: query %q not found", w.ID, w.Query) + } + if w.Span == 0 { + w.Span = 1 + } + } + } + + return nil +} + +// computeRefreshMs converts duration strings to milliseconds for the frontend. +func (c *DashboardConfig) computeRefreshMs() { + globalRefresh := parseDurationMs(c.Settings.Refresh, 30000) + + for name, q := range c.Queries { + q.RefreshMs = parseDurationMs(q.Refresh, globalRefresh) + q.StaleMs = parseDurationMs(q.StaleTime, q.RefreshMs/2) + c.Queries[name] = q + } +} + +func parseDurationMs(s string, fallback int64) int64 { + if s == "" { + return fallback + } + d, err := time.ParseDuration(s) + if err != nil { + return fallback + } + return d.Milliseconds() +} diff --git a/connections.go b/connections.go new file mode 100644 index 0000000..acc9689 --- /dev/null +++ b/connections.go @@ -0,0 +1,62 @@ +package main + +import ( + "database/sql" + "fmt" + "path/filepath" + + "fn-registry/functions/infra" +) + +// OpenConnections opens all database connections defined in the config. +// basePath is the directory of the YAML file — file-based drivers (sqlite, duckdb) +// resolve relative paths from there. +func OpenConnections(conns map[string]ConnConfig, basePath string) (map[string]*sql.DB, error) { + pool := make(map[string]*sql.DB, len(conns)) + + for name, cfg := range conns { + db, err := openOne(name, cfg, basePath) + if err != nil { + // Close already opened connections on failure. + CloseConnections(pool) + return nil, err + } + pool[name] = db + } + + return pool, nil +} + +func openOne(name string, cfg ConnConfig, basePath string) (*sql.DB, error) { + switch cfg.Driver { + case "sqlite": + return infra.SQLiteOpen(cfg.Path, basePath) + case "postgres": + port := cfg.Port + if port == 0 { + port = 5432 + } + return infra.PostgresOpen(cfg.Host, port, cfg.User, cfg.Password, cfg.Database, cfg.SSLMode) + case "duckdb": + p := cfg.Path + if basePath != "" && p != "" && p != ":memory:" && !filepath.IsAbs(p) { + p = filepath.Join(basePath, p) + } + return infra.DuckDBOpen(p) + case "clickhouse": + port := cfg.Port + if port == 0 { + port = 9000 + } + return infra.ClickHouseOpen(cfg.Host, port, cfg.User, cfg.Password, cfg.Database) + default: + return nil, fmt.Errorf("connection %q: unsupported driver %q", name, cfg.Driver) + } +} + +// CloseConnections closes all database connections. +func CloseConnections(pool map[string]*sql.DB) { + for _, db := range pool { + infra.DBClose(db) + } +} diff --git a/docs/ejemplos.md b/docs/ejemplos.md new file mode 100644 index 0000000..263995e --- /dev/null +++ b/docs/ejemplos.md @@ -0,0 +1,525 @@ +# Ejemplos de dashboards + +## 1. Dashboard minimo — una BD, un KPI + +El dashboard mas simple posible: una conexion SQLite y un numero. + +```yaml +settings: + title: "Contador" + refresh: 5s + columns: 4 + +theme: "dark" + +connections: + db: + driver: sqlite + path: ./data.db + +queries: + total: + sql: "SELECT COUNT(*) as value FROM items" + connection: db + +filters: {} + +sections: + - id: main + title: "Total" + widgets: + - id: count + type: kpi + title: "Items" + query: total + mapping: { value: "value" } + span: 4 +``` + +--- + +## 2. Dashboard con filtros — Postgres + select + date range + +Dashboard de ventas con filtros interactivos que re-ejecutan las queries. + +```yaml +settings: + title: "Sales Dashboard" + refresh: 30s + columns: 12 + +theme: "dark" + +connections: + sales: + driver: postgres + host: localhost + port: 5432 + user: analytics + password: "${SALES_PW}" + database: sales + +queries: + revenue_kpi: + connection: sales + sql: | + SELECT SUM(amount) as value, COUNT(*) as orders + FROM orders + WHERE created_at >= :from AND created_at <= :to + AND (:cat = 'all' OR category = :cat) + refresh: 15s + params: + from: "$filter.periodo.from" + to: "$filter.periodo.to" + cat: "$filter.categoria" + + revenue_daily: + connection: sales + sql: | + SELECT date_trunc('day', created_at)::date as day, + SUM(amount) as revenue + FROM orders + WHERE created_at >= :from + AND (:cat = 'all' OR category = :cat) + GROUP BY day ORDER BY day + refresh: 30s + params: + from: "$filter.periodo.from" + cat: "$filter.categoria" + + top_categories: + connection: sales + sql: | + SELECT category, SUM(amount) as revenue + FROM orders + WHERE created_at >= :from + GROUP BY category ORDER BY revenue DESC LIMIT 10 + refresh: 30s + params: + from: "$filter.periodo.from" + + recent_orders: + connection: sales + sql: | + SELECT id, customer, amount, category, status, created_at + FROM orders ORDER BY created_at DESC LIMIT 25 + refresh: 10s + +filters: + periodo: + type: date_range + label: "Periodo" + default: { from: "now-7d", to: "now" } + presets: + - { label: "Hoy", from: "now-0d", to: "now" } + - { label: "7d", from: "now-7d", to: "now" } + - { label: "30d", from: "now-30d", to: "now" } + - { label: "90d", from: "now-90d", to: "now" } + + categoria: + type: select + label: "Categoria" + default: "all" + options: + - { label: "Todas", value: "all" } + - { label: "Electronics", value: "electronics" } + - { label: "Clothing", value: "clothing" } + - { label: "Food", value: "food" } + +sections: + - id: kpis + title: "Resumen" + widgets: + - id: revenue + type: kpi + title: "Revenue" + query: revenue_kpi + mapping: { value: "value", format: "$,.2f" } + span: 4 + + - id: orders + type: kpi + title: "Ordenes" + query: revenue_kpi + mapping: { value: "orders", format: "," } + span: 4 + + - id: charts + title: "Tendencias" + columns: 2 + widgets: + - id: revenue_line + type: line_chart + title: "Revenue diario" + query: revenue_daily + mapping: + x: "day" + series: [{ key: "revenue", name: "Revenue" }] + options: { curve: monotone, zoomable: true } + span: 1 + + - id: categories_bar + type: bar_chart + title: "Top categorias" + query: top_categories + mapping: { x: "category", y: "revenue" } + span: 1 + + - id: detail + title: "Ordenes recientes" + collapsible: true + widgets: + - id: orders_table + type: table + title: "Ultimas 25" + query: recent_orders + mapping: + columns: + - { key: "id", label: "ID" } + - { key: "customer", label: "Cliente" } + - { key: "amount", label: "Monto", format: "$,.2f" } + - { key: "category", label: "Categoria" } + - { key: "status", label: "Estado" } + - { key: "created_at", label: "Fecha", format: "datetime" } + span: 12 +``` + +--- + +## 3. Dashboard multi-BD — SQLite + DuckDB + +Combina datos de distintas bases en un solo dashboard. + +```yaml +settings: + title: "Multi-DB Analytics" + refresh: 10s + columns: 12 + +theme: "dark" + +connections: + ops: + driver: sqlite + path: ./operations.db + warehouse: + driver: duckdb + path: ./analytics.duckdb + +queries: + entities_count: + connection: ops + sql: "SELECT COUNT(*) as value FROM entities" + refresh: 5s + + executions_count: + connection: ops + sql: "SELECT COUNT(*) as value FROM executions" + refresh: 5s + + assertions_pass_rate: + connection: ops + sql: | + SELECT + ROUND(100.0 * SUM(CASE WHEN passed = 1 THEN 1 ELSE 0 END) / COUNT(*), 1) as value + FROM assertion_results + refresh: 10s + + daily_events: + connection: warehouse + sql: | + SELECT date_trunc('day', ts) as day, COUNT(*) as events + FROM events GROUP BY day ORDER BY day DESC LIMIT 30 + refresh: 30s + + status_distribution: + connection: ops + sql: "SELECT status, COUNT(*) as count FROM entities GROUP BY status" + refresh: 10s + +filters: {} + +sections: + - id: kpis + title: "Operations" + widgets: + - id: entities + type: kpi + title: "Entities" + query: entities_count + mapping: { value: "value" } + span: 4 + + - id: executions + type: kpi + title: "Executions" + query: executions_count + mapping: { value: "value" } + span: 4 + + - id: pass_rate + type: kpi + title: "Assertion Pass %" + query: assertions_pass_rate + mapping: { value: "value" } + span: 4 + + - id: charts + title: "Visualizacion" + columns: 2 + widgets: + - id: events_area + type: area_chart + title: "Eventos diarios (DuckDB)" + query: daily_events + mapping: + x: "day" + series: [{ key: "events", name: "Eventos" }] + options: { show_grid: true } + span: 1 + + - id: status_bar + type: bar_chart + title: "Distribucion por status (SQLite)" + query: status_distribution + mapping: { x: "status", y: "count" } + span: 1 +``` + +--- + +## 4. Dashboard tiempo real — refresh sub-segundo + +Para metricas que cambian rapido (CPU, colas, precios). + +```yaml +settings: + title: "Realtime Monitor" + refresh: 1s + columns: 12 + +theme: "dark" + +connections: + metrics: + driver: sqlite + path: ./metrics.db + +queries: + cpu_current: + connection: metrics + sql: "SELECT value FROM metrics WHERE key = 'cpu' ORDER BY ts DESC LIMIT 1" + refresh: 200ms + stale_time: 100ms + + memory_current: + connection: metrics + sql: "SELECT value FROM metrics WHERE key = 'memory' ORDER BY ts DESC LIMIT 1" + refresh: 500ms + stale_time: 250ms + + cpu_history: + connection: metrics + sql: | + SELECT ts, value FROM metrics + WHERE key = 'cpu' ORDER BY ts DESC LIMIT 60 + refresh: 1s + + queue_depth: + connection: metrics + sql: "SELECT value FROM metrics WHERE key = 'queue' ORDER BY ts DESC LIMIT 1" + refresh: 300ms + stale_time: 150ms + +filters: {} + +sections: + - id: live + title: "Live" + widgets: + - id: cpu + type: kpi + title: "CPU %" + query: cpu_current + mapping: { value: "value" } + span: 4 + + - id: mem + type: kpi + title: "Memory %" + query: memory_current + mapping: { value: "value" } + span: 4 + + - id: queue + type: kpi + title: "Queue" + query: queue_depth + mapping: { value: "value" } + span: 4 + + - id: history + title: "CPU History (60s)" + widgets: + - id: cpu_line + type: line_chart + title: "CPU % (ultimo minuto)" + query: cpu_history + mapping: + x: "ts" + y: "value" + options: { curve: linear, show_grid: true, height: 250 } + span: 12 +``` + +--- + +## 5. Dashboard con busqueda — filtro texto + +Dashboard que permite buscar texto con debounce. + +```yaml +settings: + title: "Buscador de funciones" + refresh: 10s + columns: 12 + +theme: "dark" + +connections: + registry: + driver: sqlite + path: ../../../registry.db + +queries: + search_results: + connection: registry + sql: | + SELECT id, kind, lang, domain, description + FROM functions + WHERE id LIKE '%' || :q || '%' + OR description LIKE '%' || :q || '%' + ORDER BY updated_at DESC + LIMIT 50 + refresh: 10s + params: + q: "$filter.busqueda" + + result_count: + connection: registry + sql: | + SELECT COUNT(*) as value + FROM functions + WHERE id LIKE '%' || :q || '%' + OR description LIKE '%' || :q || '%' + refresh: 10s + params: + q: "$filter.busqueda" + +filters: + busqueda: + type: text + label: "Buscar" + default: "" + placeholder: "nombre o descripcion..." + debounce: 300 + +sections: + - id: results + title: "Resultados" + widgets: + - id: count + type: kpi + title: "Coincidencias" + query: result_count + mapping: { value: "value" } + span: 3 + + - id: results_table + type: table + title: "Funciones encontradas" + query: search_results + mapping: + columns: + - { key: "id", label: "ID" } + - { key: "kind", label: "Kind" } + - { key: "lang", label: "Lang" } + - { key: "domain", label: "Domain" } + - { key: "description", label: "Descripcion" } + span: 12 +``` + +--- + +## Patrones comunes + +### Una query alimenta varios widgets + +```yaml +queries: + summary: + sql: "SELECT SUM(amount) as revenue, COUNT(*) as orders, AVG(amount) as avg FROM orders" + +sections: + - id: kpis + title: "KPIs" + widgets: + - id: rev + type: kpi + query: summary + mapping: { value: "revenue", format: "$,.2f" } + span: 4 + - id: ord + type: kpi + query: summary + mapping: { value: "orders", format: "," } + span: 4 + - id: avg + type: kpi + query: summary + mapping: { value: "avg", format: "$,.2f" } + span: 4 +``` + +La query se ejecuta una sola vez y los 3 KPIs leen del mismo resultado. + +### Seccion colapsable para detalle + +```yaml +sections: + - id: detail + title: "Detalle (click para expandir)" + collapsible: true + widgets: + - id: log_table + type: table + query: logs + span: 12 +``` + +### Grid flexible por seccion + +```yaml +sections: + - id: kpis + title: "KPIs" + # hereda columns: 12 del global → 4 KPIs de span 3 + widgets: + - { id: a, type: kpi, query: q, mapping: { value: v }, span: 3 } + - { id: b, type: kpi, query: q, mapping: { value: v }, span: 3 } + - { id: c, type: kpi, query: q, mapping: { value: v }, span: 3 } + - { id: d, type: kpi, query: q, mapping: { value: v }, span: 3 } + + - id: charts + title: "Graficos" + columns: 2 # override: solo 2 columnas + widgets: + - { id: chart1, type: line_chart, query: q1, mapping: { x: a, y: b }, span: 1 } + - { id: chart2, type: bar_chart, query: q2, mapping: { x: a, y: b }, span: 1 } + + - id: full + title: "Tabla" + columns: 1 # 1 columna = ancho completo + widgets: + - { id: tbl, type: table, query: q3, span: 1 } +``` diff --git a/docs/guia.md b/docs/guia.md new file mode 100644 index 0000000..508f564 --- /dev/null +++ b/docs/guia.md @@ -0,0 +1,569 @@ +# Rapid Dashboards — Guia de uso + +Dashboard builder declarativo: defines un YAML, obtienes un dashboard desktop con datos en vivo. + +## Inicio rapido + +```bash +cd apps/rapid_dashboards + +# Desarrollo — usa symlink dashboard.yaml (ver "Cambiar entre dashboards") +wails dev + +# Desarrollo — build produccion +CGO_ENABLED=1 wails build -tags fts5 +./build/bin/rapid-dashboards --dashboard examples/fn_registry_overview.yaml +``` + +## Compilar un dashboard especifico + +### Desarrollo (wails dev) + +`wails dev` no pasa argumentos CLI al binario. El binario busca el dashboard en este orden: + +1. Flag `--dashboard ` (solo funciona con `wails build`, no con `wails dev`) +2. Variable de entorno `DASHBOARD` (no se propaga al binario en `wails dev`) +3. Archivo `dashboard.yaml` en el cwd (metodo recomendado para desarrollo) + +**Para desarrollo, usar un symlink:** + +```bash +# Apuntar al dashboard deseado +ln -sf examples/fn_registry_overview.yaml dashboard.yaml + +# Lanzar — el binario encuentra dashboard.yaml automaticamente +wails dev +``` + +### Produccion (wails build) + +```bash +# Compilar +CGO_ENABLED=1 wails build -tags fts5 + +# Ejecutar con un YAML especifico +./build/bin/rapid-dashboards --dashboard examples/fn_registry_overview.yaml +./build/bin/rapid-dashboards --dashboard /ruta/absoluta/mi_dashboard.yaml +``` + +El binario compilado acepta el flag `--dashboard` directamente. + +--- + +## Cambiar entre dashboards + +### En vivo (dropdown en la app) + +Si hay mas de un archivo `.yaml` en el directorio del dashboard actual, aparece un dropdown junto al titulo. Al seleccionar otro dashboard: + +- Se cargan las nuevas conexiones, queries y widgets +- Se cierran las conexiones anteriores +- El tema se aplica automaticamente + +Los dashboards disponibles se escanean del directorio donde esta el YAML actual (por defecto `examples/`). + +### Cambiando el symlink (para desarrollo) + +```bash +# Ver dashboards disponibles +ls examples/*.yaml + +# Cambiar al dashboard de apps +ln -sf examples/fn_registry_apps.yaml dashboard.yaml + +# Cambiar al overview de funciones +ln -sf examples/fn_registry_overview.yaml dashboard.yaml + +# Reiniciar wails dev para que tome el cambio +``` + +### Por CLI (produccion) + +```bash +# Dashboard de funciones +./build/bin/rapid-dashboards --dashboard examples/fn_registry_overview.yaml + +# Dashboard de apps +./build/bin/rapid-dashboards --dashboard examples/fn_registry_apps.yaml +``` + +--- + +## Temas + +El campo `theme` del YAML controla toda la estetica del dashboard. Cada tema cambia colores de fondo, texto, cards, bordes, acentos y la paleta de colores de los graficos. + +### Temas disponibles + +| Tema | Descripcion | Uso recomendado | +|------|-------------|-----------------| +| `dark` | Azul-gris profundo, acentos azules | Default, dashboards tecnicos | +| `emerald` | Verde esmeralda oscuro, acentos dorados | Dashboards financieros, naturaleza | +| `amber` | Calido ambar/naranja sobre fondo oscuro | Dashboards de alertas, operaciones | +| `rose` | Rosa/magenta sobre fondo oscuro | Dashboards de marketing, analytics | +| `light` | Fondo claro con sombras sutiles | Presentaciones, pantallas con luz | + +### Ejemplo de uso + +```yaml +# Dashboard con tema esmeralda +theme: "emerald" + +# Dashboard con tema claro +theme: "light" +``` + +### Que cambia cada tema + +- **Background y foreground**: tonos base del fondo y texto +- **Card**: color de fondo de widgets, sin bordes en temas oscuros, con sombra en light +- **Primary/accent**: color de acento para interacciones y highlights +- **Border**: tonos de separadores y bordes +- **Chart palette**: cada tema tiene su propia secuencia de colores para graficos (`--chart-1` a `--chart-5`) + +### Anadir un tema nuevo + +Anadir un bloque `[data-theme="nombre"]` en `frontend/src/app.css` con todas las variables CSS. El tema se activa automaticamente al usar `theme: "nombre"` en el YAML. + +--- + +## Estructura del YAML + +Un dashboard se define con 5 bloques: + +```yaml +settings: # titulo, dimensiones, refresh global, columnas +theme: # tema visual +connections: # bases de datos +queries: # SQL + refresh + params +filters: # controles de filtro +sections: # secciones con widgets +``` + +--- + +## 1. settings + +```yaml +settings: + title: "Mi Dashboard" # titulo de la ventana + refresh: 30s # refresh global por defecto + width: 1280 # ancho ventana (px) + height: 800 # alto ventana (px) + columns: 12 # columnas del grid CSS +``` + +- `refresh` acepta duraciones Go: `200ms`, `1s`, `5s`, `1m`, `30m` +- `columns` define el grid base. Cada widget usa `span` para ocupar N columnas. + +--- + +## 2. connections + +Cada conexion tiene un nombre y un driver. Secrets via variables de entorno `${VAR}`. + +### SQLite +```yaml +connections: + local: + driver: sqlite + path: ./data.db +``` + +### PostgreSQL +```yaml +connections: + main_db: + driver: postgres + host: localhost + port: 5432 + user: analytics + password: "${PG_PASSWORD}" + database: myapp + sslmode: disable +``` + +### DuckDB +```yaml +connections: + warehouse: + driver: duckdb + path: ./warehouse.duckdb +``` + +### ClickHouse +```yaml +connections: + events: + driver: clickhouse + host: localhost + port: 9000 + user: default + password: "" + database: events +``` + +**Paths relativos**: se resuelven desde el cwd del binario, NO desde la ubicacion del YAML. En desarrollo el cwd es `apps/rapid_dashboards/`, asi que `../../registry.db` apunta a la raiz del repo. + +--- + +## 3. queries + +```yaml +queries: + revenue_total: + connection: main_db + sql: "SELECT SUM(amount) as value FROM orders" + refresh: 10s + stale_time: 5s + + revenue_by_date: + connection: main_db + sql: | + SELECT date, SUM(amount) as revenue + FROM orders + WHERE date >= :date_from AND date <= :date_to + ORDER BY date + params: + date_from: "$filter.date_range.from" + date_to: "$filter.date_range.to" +``` + +### Parametros + +- `:nombre` en el SQL se reemplaza por el valor del param. +- `$filter.xxx` referencia el valor actual de un filtro. +- `$filter.date_range.from` accede al subcampo `from`. +- Placeholders se convierten al formato del driver (`$1` para Postgres, `?` para SQLite/DuckDB/ClickHouse). + +### Fechas relativas + +| Valor | Resultado | +|-------|-----------| +| `now` | timestamp actual | +| `now-7d` | hace 7 dias | +| `now-24h` | hace 24 horas | +| `now-30m` | hace 30 minutos | + +--- + +## 4. filters + +### Select +```yaml +filters: + category: + type: select + label: "Categoria" + default: "all" + options: + - { label: "Todas", value: "all" } + - { label: "Electronics", value: "electronics" } +``` + +### Date Range +```yaml +filters: + date_range: + type: date_range + label: "Periodo" + default: { from: "now-7d", to: "now" } + presets: + - { label: "7 dias", from: "now-7d", to: "now" } + - { label: "30 dias", from: "now-30d", to: "now" } +``` + +### Text +```yaml +filters: + search: + type: text + label: "Buscar" + placeholder: "Buscar por nombre..." + debounce: 300 +``` + +--- + +## 5. sections y widgets + +```yaml +sections: + - id: kpis + title: "Metricas" + columns: 4 # override de columnas + widgets: + - id: total_users + type: kpi + title: "Usuarios" + query: count_users + mapping: { value: "value" } + span: 1 + + - id: charts + title: "Graficos" + collapsible: true + columns: 2 + widgets: + - id: revenue_line + type: line_chart + title: "Revenue" + query: revenue_by_date + mapping: + x: "date" + series: + - { key: "revenue", name: "Revenue", color: "#3b82f6" } + options: + zoomable: true + span: 1 +``` + +--- + +## Tipos de widget + +### kpi + +```yaml +- type: kpi + mapping: + value: "campo_sql" + format: "$,.2f" # opcional +``` + +### line_chart + +```yaml +- type: line_chart + mapping: + x: "date" + series: + - { key: "revenue", name: "Revenue", color: "#3b82f6" } + options: + curve: monotone # linear | monotone | step + show_grid: true + show_legend: true + zoomable: true + height: 400 +``` + +### bar_chart + +```yaml +- type: bar_chart + mapping: + x: "category" + y: "count" + # O multi-series con series: [...] + options: + horizontal: true # barras horizontales + show_grid: true + show_legend: true + height: 300 +``` + +### pie_chart + +```yaml +- type: pie_chart + mapping: + name: "domain" # campo para nombres de segmentos + value: "cantidad" # campo numerico + options: + donut: true # hueco central + show_legend: true # default: true + height: 300 +``` + +### area_chart + +```yaml +- type: area_chart + mapping: + x: "date" + series: + - { key: "users", name: "Usuarios" } + options: + stacked: true + height: 300 +``` + +### sparkline + +```yaml +- type: sparkline + mapping: + value: "metric" + options: + variant: area # line | area | bar + width: 200 + height: 40 +``` + +### table + +```yaml +- type: table + mapping: + columns: + - { key: "name", label: "Nombre" } + - { key: "amount", label: "Monto", format: "$,.2f" } + - { key: "created_at", label: "Fecha", format: "datetime" } + options: + heatmap_columns: ["go", "python", "bash"] # colorea celdas por intensidad +``` + +Si no defines `columns`, se auto-detectan del resultado SQL. + +**Heatmap**: `options.heatmap_columns` acepta un array de nombres de columna. Las celdas se colorean de azul oscuro (valor minimo) a azul brillante (valor maximo), calculado por columna. + +--- + +## Formatos de valor + +| Formato | Ejemplo | Resultado | +|---------|---------|-----------| +| `$,.2f` | 1234.5 | $1,234.50 | +| `,.2f` | 1234.5 | 1,234.50 | +| `.2f` | 1234.5 | 1234.50 | +| `,` | 1234 | 1,234 | +| `datetime` | ISO string | locale datetime | + +--- + +## Variables de entorno + +Cualquier campo del YAML soporta `${VARIABLE}`: + +```bash +DB_HOST=prod.example.com DB_PASSWORD=secret \ + ./rapid-dashboards --dashboard prod.yaml +``` + +--- + +## Puntos clave (aprendizajes) + +### Wails dev no pasa CLI args al binario + +`wails dev -- --flag value` no funciona: Wails compila y ejecuta el binario como proceso hijo sin pasar los args despues de `--`. Tampoco propaga variables de entorno al binario. La solucion es usar un archivo `dashboard.yaml` en el cwd que el binario detecta automaticamente. + +### Wails genera bindings ejecutando el binario sin args + +Durante `wails dev`, el primer paso es generar bindings TypeScript. Para esto Wails ejecuta el binario sin argumentos. Si el binario requiere flags obligatorios y hace `os.Exit(1)`, la generacion falla. Solucion: detectar cuando no hay args y usar un config dummy con slices vacios (no nil) para que los tipos se generen correctamente. + +### Slices nil de Go se serializan como null en JSON + +`[]SectionDef(nil)` se serializa como `null`, no como `[]`. Si el frontend hace `.map()` sobre null, crash. Inicializar siempre con `[]SectionDef{}` para el config dummy, y usar `(config.sections ?? [])` en el frontend. + +### Tipos de Wails vs tipos manuales de TypeScript + +Wails genera `models.ts` con tipos `string` genericos. Si el frontend tiene tipos mas estrictos (union types como `"select" | "date_range"`), hay incompatibilidad. Solucion: castear los imports con `as unknown as typeof X`. + +### Paths relativos se resuelven desde el cwd, no desde el YAML + +`SQLiteOpen(path)` resuelve paths relativos al cwd del proceso. Si el YAML esta en `examples/` y el binario corre desde `apps/rapid_dashboards/`, un path `../../registry.db` es correcto pero `../../../registry.db` (relativo al YAML) no. Siempre pensar los paths desde el cwd del binario. + +### Recharts Pie necesita valores numericos + +SQLite puede devolver numeros como strings en algunos contextos. Recharts Pie no renderiza nada si `dataKey` apunta a un string. Solucion: coercer a `Number()` antes de pasar a Pie. + +### Recharts PieLabelRenderProps es estricto + +El tipo `label` de `` no acepta callbacks con tipos genericos como `Record`. Hay que importar `PieLabelRenderProps` de `recharts` y usar una funcion con nombre tipada, no un lambda inline. + +### Animaciones de Recharts + +Las animaciones por defecto de Recharts Pie son lentas en dashboards que refrescan frecuentemente. Usar `isAnimationActive={false}` para render instantaneo. + +--- + +## Scripts de lanzamiento + +Cada dashboard YAML debe tener forma de lanzarse rapido desde `scripts/`. La carpeta `scripts/` contiene dos scripts genericos que reciben el nombre del dashboard como argumento: + +```bash +# Listar dashboards disponibles +./scripts/dev.sh + +# Desarrollo — crea symlink + wails dev +./scripts/dev.sh fn_registry_overview + +# Produccion — compila si es necesario + ejecuta +./scripts/prod.sh fn_registry_apps + +# Produccion con variables de entorno +DB_PASSWORD=secret ./scripts/prod.sh mi_dashboard +``` + +### Convencion + +Al crear un nuevo YAML en `examples/`, no hace falta crear scripts adicionales — `dev.sh` y `prod.sh` descubren automaticamente los `.yaml` de `examples/`. Solo ejecutar con el nombre (sin extension): + +```bash +# Nuevo dashboard +vim examples/mi_nuevo_dashboard.yaml + +# Lanzar inmediatamente +./scripts/dev.sh mi_nuevo_dashboard +``` + +`dev.sh` crea el symlink `dashboard.yaml` y lanza `wails dev`. `prod.sh` compila con `CGO_ENABLED=1 wails build -tags fts5` si el binario no existe o el YAML es mas reciente, y ejecuta con `--dashboard`. + +--- + +## Compilacion multiplataforma + +### Linux (default) + +```bash +CGO_ENABLED=1 wails build -tags fts5 +``` + +Genera `build/bin/rapid-dashboards` con soporte para SQLite, PostgreSQL, DuckDB y ClickHouse. + +### Windows (cross-compile desde Linux) + +```bash +CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 \ + wails build -tags "fts5 noduckdb noclickhouse" -platform windows/amd64 -skipbindings +``` + +Genera `build/bin/rapid-dashboards.exe`. Requiere `mingw-w64` instalado (`apt install gcc-mingw-w64-x86-64`). + +**Limitacion:** DuckDB y ClickHouse se excluyen con los tags `noduckdb` y `noclickhouse` porque `go-duckdb` depende de libstdc++ y no cross-compila bien desde Linux. El `.exe` solo soporta SQLite y PostgreSQL. Si un YAML usa esos drivers, dara error: `"duckdb support not compiled"`. + +### Windows nativo (con DuckDB y ClickHouse) + +Para compilar con todos los drivers, compilar directamente en Windows con Go y MinGW/MSYS2 instalados: + +```powershell +# En Windows con Go + MinGW (MSYS2) +$env:CGO_ENABLED=1 +wails build -tags fts5 +``` + +Esto genera un `.exe` con soporte completo: SQLite, PostgreSQL, DuckDB y ClickHouse. + +### Resumen de soporte por plataforma + +| Driver | Linux | Windows (cross-compile) | Windows (nativo) | +|---|---|---|---| +| SQLite | si | si | si | +| PostgreSQL | si | si | si | +| DuckDB | si | no | si | +| ClickHouse | si | no | si | + +### Rutas en Windows + +Las rutas no se rompen entre plataformas. El codigo usa `filepath.Join` y `filepath.IsAbs` de Go, que adaptan los separadores automaticamente (`/` en Linux, `\` en Windows). Los paths relativos en el YAML (ej: `../../registry.db`) funcionan igual en ambos OS. + +--- + +## Extensibilidad + +Para anadir un tipo de widget nuevo: + +1. Crear `frontend/src/components/widgets/MiWidget.tsx` implementando `WidgetProps` +2. Registrar en `frontend/src/components/widgets/register.ts`: `registerWidget('mi_tipo', MiWidget)` +3. Usar en YAML: `type: mi_tipo` diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 0000000..40a7098 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,202 @@ +# Schema YAML — Referencia completa + +Referencia rapida de todos los campos del YAML de dashboard. + +## Top level + +| Campo | Tipo | Requerido | Default | Descripcion | +|-------|------|-----------|---------|-------------| +| `settings` | object | si | — | Configuracion global | +| `theme` | string | no | `"dark"` | Nombre del tema | +| `connections` | map | si | — | Conexiones a bases de datos | +| `queries` | map | si | — | Definiciones de queries SQL | +| `filters` | map | no | `{}` | Filtros interactivos | +| `sections` | array | si | — | Secciones con widgets | + +## settings + +| Campo | Tipo | Default | Descripcion | +|-------|------|---------|-------------| +| `title` | string | — | Titulo de la ventana (requerido) | +| `refresh` | duration | `"30s"` | Refresh global por defecto | +| `width` | int | `1280` | Ancho de ventana en px | +| `height` | int | `800` | Alto de ventana en px | +| `columns` | int | `12` | Columnas del grid CSS | + +## connections[nombre] + +| Campo | Tipo | Drivers | Descripcion | +|-------|------|---------|-------------| +| `driver` | string | todos | `sqlite` \| `postgres` \| `duckdb` \| `clickhouse` | +| `path` | string | sqlite, duckdb | Ruta al archivo de BD | +| `host` | string | postgres, clickhouse | Hostname | +| `port` | int | postgres, clickhouse | Puerto (default: 5432/9000) | +| `user` | string | postgres, clickhouse | Usuario | +| `password` | string | postgres, clickhouse | Password (soporta `${ENV}`) | +| `database` | string | postgres, clickhouse | Nombre de la base de datos | +| `sslmode` | string | postgres | Modo SSL (default: `"disable"`) | + +## queries[nombre] + +| Campo | Tipo | Default | Descripcion | +|-------|------|---------|-------------| +| `connection` | string | — | Nombre de la conexion (requerido) | +| `sql` | string | — | Query SQL (requerido) | +| `refresh` | duration | global | Intervalo de re-ejecucion | +| `stale_time` | duration | refresh/2 | Tiempo antes de considerar dato viejo | +| `params` | map | `{}` | Parametros: `nombre: valor_o_$filter.ref` | + +## filters[nombre] + +| Campo | Tipo | Tipos de filtro | Descripcion | +|-------|------|-----------------|-------------| +| `type` | string | todos | `select` \| `date_range` \| `text` | +| `label` | string | todos | Etiqueta visible | +| `default` | any | todos | Valor por defecto | +| `options` | array | select | `[{ label, value }]` | +| `presets` | array | date_range | `[{ label, from, to }]` | +| `placeholder` | string | text | Placeholder del input | +| `debounce` | int | text | Delay en ms (default: 300) | + +## sections[] + +| Campo | Tipo | Default | Descripcion | +|-------|------|---------|-------------| +| `id` | string | — | Identificador unico (requerido) | +| `title` | string | — | Titulo visible | +| `collapsible` | bool | `false` | Permite colapsar | +| `columns` | int | global | Override de columnas del grid | +| `widgets` | array | — | Widgets de esta seccion | + +## widgets[] + +| Campo | Tipo | Default | Descripcion | +|-------|------|---------|-------------| +| `id` | string | — | Identificador unico (requerido) | +| `type` | string | — | Tipo de componente (requerido) | +| `title` | string | — | Titulo del widget | +| `query` | string | — | Nombre de la query (requerido) | +| `mapping` | object | — | Mapeo de campos SQL a props | +| `options` | object | `{}` | Opciones del componente | +| `span` | int | `1` | Columnas que ocupa | +| `row_span` | int | `1` | Filas que ocupa | + +## Tipos de widget y sus mappings + +### kpi +``` +mapping.value → campo del resultado (string) +mapping.format → formato de visualizacion (string, opcional) +``` + +### line_chart / bar_chart / area_chart +``` +mapping.x → campo del eje X (string) +mapping.y → campo del eje Y — single series (string) +mapping.series → multi-series: [{ key, name, color? }] +``` + +### sparkline +``` +mapping.value → campo numerico (string) +``` + +### table +``` +mapping.columns → [{ key, label, format? }] + Si no se define, auto-detecta del resultado. +``` + +## Options por tipo + +### line_chart +| Option | Tipo | Default | Descripcion | +|--------|------|---------|-------------| +| `curve` | string | `"monotone"` | linear, monotone, step, stepBefore, stepAfter | +| `show_grid` | bool | `true` | Mostrar cuadricula | +| `show_legend` | bool | `false` | Mostrar leyenda | +| `zoomable` | bool | `false` | Habilitar brush zoom | +| `height` | int | `300` | Altura en px | + +### bar_chart +| Option | Tipo | Default | Descripcion | +|--------|------|---------|-------------| +| `horizontal` | bool | `false` | Barras horizontales | +| `show_grid` | bool | `true` | Mostrar cuadricula | +| `show_legend` | bool | `false` | Mostrar leyenda | +| `height` | int | `300` | Altura en px | + +### area_chart +| Option | Tipo | Default | Descripcion | +|--------|------|---------|-------------| +| `stacked` | bool | `false` | Apilar areas | +| `show_grid` | bool | `true` | Mostrar cuadricula | +| `show_legend` | bool | `false` | Mostrar leyenda | +| `height` | int | `300` | Altura en px | + +### sparkline +| Option | Tipo | Default | Descripcion | +|--------|------|---------|-------------| +| `variant` | string | `"area"` | line, area, bar | +| `width` | int | `200` | Ancho en px | +| `height` | int | `40` | Altura en px | + +## Duraciones validas + +| Formato | Ejemplo | +|---------|---------| +| milisegundos | `100ms`, `200ms`, `500ms` | +| segundos | `1s`, `5s`, `30s` | +| minutos | `1m`, `5m`, `30m` | +| horas | `1h` | +| combinado | `1m30s` | + +## Fechas relativas + +| Formato | Resultado | +|---------|-----------| +| `now` | momento actual | +| `now-Ns` | hace N segundos | +| `now-Nm` | hace N minutos | +| `now-Nh` | hace N horas | +| `now-Nd` | hace N dias | + +## KPI mappings extendidos (v2) + +Ademas de `value` y `format`, el widget KPI soporta: + +| Mapping | Tipo | Descripcion | +|---------|------|-------------| +| `value` | string | Campo SQL del valor principal | +| `format` | string | Formato: `$,.2f`, `,`, `datetime` | +| `unit` | string | Campo SQL para la unidad (ej: "k", "ms") | +| `unitLabel` | string | Unidad fija (alternativa a `unit` dinamico) | +| `delta` | string | Campo SQL para el cambio porcentual | +| `deltaLabel` | string | Texto antes del delta: "Increased by" | +| `deltaSuffix` | string | Texto despues del delta: "vs yesterday" | +| `sparkline` | string | Campo SQL con valores para mini barras | +| `sparklineColors` | string[] | Colores por barra | + +Ejemplo: + +```yaml +- type: kpi + mapping: + value: "total" + format: "," + unitLabel: "k" + delta: "pct_change" + deltaLabel: "Increased by" + deltaSuffix: "vs yesterday" + sparkline: "daily_value" + sparklineColors: ["#3b82f6", "#8b5cf6", "#f59e0b", "#10b981"] +``` + +## Scripts de lanzamiento + +```bash +./scripts/dev.sh # symlink + wails dev +./scripts/prod.sh # build + ejecutar +``` + +Sin argumento lista los dashboards disponibles en `examples/`. diff --git a/examples/demo_dashboard.yaml b/examples/demo_dashboard.yaml new file mode 100644 index 0000000..4624f36 --- /dev/null +++ b/examples/demo_dashboard.yaml @@ -0,0 +1,144 @@ +settings: + title: "fn_registry — Dashboard Demo" + refresh: 5s + width: 1280 + height: 800 + columns: 12 + +theme: "dark" + +connections: + registry: + driver: sqlite + path: ../../../registry.db + +queries: + total_functions: + connection: registry + sql: "SELECT COUNT(*) as value FROM functions" + refresh: 30s + + total_types: + connection: registry + sql: "SELECT COUNT(*) as value FROM types" + refresh: 30s + + total_proposals: + connection: registry + sql: "SELECT COUNT(*) as value FROM proposals" + refresh: 30s + + functions_by_domain: + connection: registry + sql: "SELECT domain, COUNT(*) as count FROM functions GROUP BY domain ORDER BY count DESC" + refresh: 30s + + functions_by_kind: + connection: registry + sql: "SELECT kind, COUNT(*) as count FROM functions GROUP BY kind ORDER BY count DESC" + refresh: 30s + + functions_by_purity: + connection: registry + sql: "SELECT purity, COUNT(*) as count FROM functions GROUP BY purity ORDER BY count DESC" + refresh: 30s + + functions_by_lang: + connection: registry + sql: "SELECT lang, COUNT(*) as count FROM functions GROUP BY lang ORDER BY count DESC" + refresh: 30s + + recent_functions: + connection: registry + sql: "SELECT id, kind, lang, domain, purity, description FROM functions ORDER BY updated_at DESC LIMIT 20" + refresh: 10s + + tested_ratio: + connection: registry + sql: "SELECT tested, COUNT(*) as count FROM functions GROUP BY tested" + refresh: 30s + +filters: {} + +sections: + - id: kpis + title: "Overview" + widgets: + - id: total_fn + type: kpi + title: "Functions" + query: total_functions + mapping: { value: "value" } + span: 3 + + - id: total_tp + type: kpi + title: "Types" + query: total_types + mapping: { value: "value" } + span: 3 + + - id: total_pr + type: kpi + title: "Proposals" + query: total_proposals + mapping: { value: "value" } + span: 3 + + - id: tested_kpi + type: kpi + title: "Tested Ratio" + query: tested_ratio + mapping: { value: "count" } + span: 3 + + - id: distribution + title: "Distribution" + columns: 4 + widgets: + - id: by_domain + type: bar_chart + title: "By Domain" + query: functions_by_domain + mapping: { x: "domain", y: "count" } + options: { show_grid: true } + span: 1 + + - id: by_lang + type: bar_chart + title: "By Language" + query: functions_by_lang + mapping: { x: "lang", y: "count" } + span: 1 + + - id: by_kind + type: bar_chart + title: "By Kind" + query: functions_by_kind + mapping: { x: "kind", y: "count" } + span: 1 + + - id: by_purity + type: bar_chart + title: "By Purity" + query: functions_by_purity + mapping: { x: "purity", y: "count" } + span: 1 + + - id: detail + title: "Recent Functions" + collapsible: true + widgets: + - id: recent_table + type: table + title: "Last 20 Updated" + query: recent_functions + mapping: + columns: + - { key: "id", label: "ID" } + - { key: "kind", label: "Kind" } + - { key: "lang", label: "Lang" } + - { key: "domain", label: "Domain" } + - { key: "purity", label: "Purity" } + - { key: "description", label: "Description" } + span: 12 diff --git a/examples/fn_registry_apps.yaml b/examples/fn_registry_apps.yaml new file mode 100644 index 0000000..fa518c7 --- /dev/null +++ b/examples/fn_registry_apps.yaml @@ -0,0 +1,202 @@ +settings: + title: "fn-registry Apps" + refresh: 30s + width: 1440 + height: 900 + columns: 12 + +theme: "emerald" + +connections: + registry: + driver: sqlite + path: ../../registry.db + +queries: + # --- KPIs --- + total_apps: + connection: registry + sql: "SELECT COUNT(*) AS value FROM apps" + + apps_go: + connection: registry + sql: "SELECT COUNT(*) AS value FROM apps WHERE lang = 'go'" + + apps_python: + connection: registry + sql: "SELECT COUNT(*) AS value FROM apps WHERE lang = 'py'" + + domains_with_apps: + connection: registry + sql: "SELECT COUNT(DISTINCT domain) AS value FROM apps" + + # --- Distribucion --- + apps_by_lang: + connection: registry + sql: "SELECT lang, COUNT(*) AS cantidad FROM apps GROUP BY lang ORDER BY cantidad DESC" + + apps_by_domain: + connection: registry + sql: "SELECT domain, COUNT(*) AS cantidad FROM apps GROUP BY domain ORDER BY cantidad DESC" + + apps_by_framework: + connection: registry + sql: | + SELECT + CASE WHEN framework = '' OR framework IS NULL THEN '(sin framework)' ELSE framework END AS framework, + COUNT(*) AS cantidad + FROM apps + GROUP BY framework + ORDER BY cantidad DESC + + # --- Dependencias --- + apps_most_deps: + connection: registry + sql: | + SELECT + name || ' (' || lang || ')' AS app, + (LENGTH(uses_functions) - LENGTH(REPLACE(uses_functions, ',', '')) + + CASE WHEN uses_functions != '[]' AND uses_functions != '' THEN 1 ELSE 0 END) AS dependencias + FROM apps + WHERE uses_functions != '[]' AND uses_functions != '' + ORDER BY dependencias DESC + LIMIT 15 + + functions_most_used_in_apps: + connection: registry + sql: | + WITH RECURSIVE split_uses(app_id, rest, val) AS ( + SELECT id, uses_functions || ',', NULL + FROM apps + WHERE uses_functions != '[]' AND uses_functions != '' + UNION ALL + SELECT app_id, + SUBSTR(rest, INSTR(rest, ',') + 1), + TRIM(SUBSTR(rest, 1, INSTR(rest, ',') - 1), ' "[]') + FROM split_uses WHERE rest != '' + ) + SELECT val AS funcion, COUNT(*) AS veces + FROM split_uses + WHERE val IS NOT NULL AND val != '' AND val != ']' + GROUP BY val + ORDER BY veces DESC + LIMIT 15 + + # --- Catalogo --- + apps_catalog: + connection: registry + sql: | + SELECT + id, + name, + lang, + domain, + CASE WHEN framework = '' THEN '-' ELSE framework END AS framework, + description, + entry_point, + updated_at + FROM apps + ORDER BY domain, name + +filters: {} + +sections: + # ---- KPIs ---- + - id: kpis + title: "Overview" + columns: 4 + widgets: + - id: total_apps + type: kpi + title: "Total Apps" + query: total_apps + mapping: { value: "value" } + span: 1 + + - id: apps_go + type: kpi + title: "Apps Go" + query: apps_go + mapping: { value: "value" } + span: 1 + + - id: apps_py + type: kpi + title: "Apps Python" + query: apps_python + mapping: { value: "value" } + span: 1 + + - id: domains + type: kpi + title: "Dominios" + query: domains_with_apps + mapping: { value: "value" } + span: 1 + + # ---- Distribucion ---- + - id: distribution + title: "Distribucion" + columns: 3 + widgets: + - id: by_lang + type: pie_chart + title: "Apps por Lenguaje" + query: apps_by_lang + mapping: { name: "lang", value: "cantidad" } + span: 1 + + - id: by_domain + type: pie_chart + title: "Apps por Dominio" + query: apps_by_domain + mapping: { name: "domain", value: "cantidad" } + span: 1 + + - id: by_framework + type: bar_chart + title: "Apps por Framework" + query: apps_by_framework + mapping: { x: "framework", y: "cantidad" } + span: 1 + + # ---- Dependencias ---- + - id: dependencies + title: "Dependencias" + columns: 2 + widgets: + - id: most_deps + type: bar_chart + title: "Apps con Mas Dependencias" + query: apps_most_deps + mapping: { x: "app", y: "dependencias" } + options: { horizontal: true, height: 350 } + span: 1 + + - id: most_used_fns + type: bar_chart + title: "Funciones Mas Usadas en Apps" + query: functions_most_used_in_apps + mapping: { x: "funcion", y: "veces" } + options: { horizontal: true, height: 350 } + span: 1 + + # ---- Catalogo ---- + - id: catalog + title: "Catalogo de Apps" + widgets: + - id: catalog_table + type: table + title: "Todas las Apps" + query: apps_catalog + mapping: + columns: + - { key: "id", label: "ID" } + - { key: "name", label: "Nombre" } + - { key: "lang", label: "Lang" } + - { key: "domain", label: "Dominio" } + - { key: "framework", label: "Framework" } + - { key: "description", label: "Descripcion" } + - { key: "entry_point", label: "Entry Point" } + - { key: "updated_at", label: "Actualizado" } + span: 12 diff --git a/examples/fn_registry_overview.yaml b/examples/fn_registry_overview.yaml new file mode 100644 index 0000000..6e43157 --- /dev/null +++ b/examples/fn_registry_overview.yaml @@ -0,0 +1,285 @@ +settings: + title: "fn-registry Overview" + refresh: 30s + width: 1440 + height: 900 + columns: 12 + +theme: "dark" + +connections: + registry: + driver: sqlite + path: ../../registry.db + +queries: + # --- KPIs --- + total_functions: + connection: registry + sql: "SELECT COUNT(*) AS value FROM functions" + + functions_with_tests: + connection: registry + sql: "SELECT COUNT(*) AS value FROM functions WHERE tested = 1" + + functions_without_tests: + connection: registry + sql: "SELECT COUNT(*) AS value FROM functions WHERE tested = 0" + + total_types: + connection: registry + sql: "SELECT COUNT(*) AS value FROM types" + + pending_proposals: + connection: registry + sql: "SELECT COUNT(*) AS value FROM proposals WHERE status = 'pending'" + + # --- Distribucion general --- + by_lang: + connection: registry + sql: "SELECT lang, COUNT(*) AS cantidad FROM functions GROUP BY lang ORDER BY cantidad DESC" + + by_domain: + connection: registry + sql: "SELECT domain, COUNT(*) AS cantidad FROM functions GROUP BY domain ORDER BY cantidad DESC" + + by_kind: + connection: registry + sql: "SELECT kind, COUNT(*) AS cantidad FROM functions GROUP BY kind ORDER BY cantidad DESC" + + # --- Analisis profundo --- + purity: + connection: registry + sql: "SELECT purity, COUNT(*) AS cantidad FROM functions GROUP BY purity ORDER BY cantidad DESC" + + most_used: + connection: registry + sql: | + WITH RECURSIVE split_uses(fn_id, rest, val) AS ( + SELECT id, uses_functions || ',', NULL FROM functions WHERE uses_functions != '[]' AND uses_functions != '' + UNION ALL + SELECT fn_id, + SUBSTR(rest, INSTR(rest, ',') + 1), + TRIM(SUBSTR(rest, 1, INSTR(rest, ',') - 1), ' "[]') + FROM split_uses WHERE rest != '' + ) + SELECT val AS funcion, COUNT(*) AS veces + FROM split_uses + WHERE val IS NOT NULL AND val != '' AND val != ']' + GROUP BY val + ORDER BY veces DESC + LIMIT 15 + + most_complex: + connection: registry + sql: | + SELECT + name || ' (' || lang || ')' AS funcion, + (LENGTH(uses_functions) - LENGTH(REPLACE(uses_functions, ',', '')) + + CASE WHEN uses_functions != '[]' AND uses_functions != '' THEN 1 ELSE 0 END) AS dependencias + FROM functions + WHERE uses_functions != '[]' AND uses_functions != '' + ORDER BY dependencias DESC + LIMIT 15 + + # --- Cobertura y cross-table --- + test_coverage_by_domain: + connection: registry + sql: | + SELECT + domain, + SUM(CASE WHEN tested = 1 THEN 1 ELSE 0 END) AS con_tests, + SUM(CASE WHEN tested = 0 THEN 1 ELSE 0 END) AS sin_tests + FROM functions + GROUP BY domain + ORDER BY domain + + lang_by_domain: + connection: registry + sql: | + SELECT + domain, + SUM(CASE WHEN lang = 'go' THEN 1 ELSE 0 END) AS go, + SUM(CASE WHEN lang = 'py' THEN 1 ELSE 0 END) AS python, + SUM(CASE WHEN lang = 'bash' THEN 1 ELSE 0 END) AS bash, + SUM(CASE WHEN lang = 'ts' THEN 1 ELSE 0 END) AS typescript, + COUNT(*) AS total + FROM functions + GROUP BY domain + ORDER BY total DESC + + # --- Tipos y recientes --- + types_by_domain: + connection: registry + sql: | + SELECT domain, algebraic, COUNT(*) AS cantidad + FROM types + GROUP BY domain, algebraic + ORDER BY domain, cantidad DESC + + recent_functions: + connection: registry + sql: | + SELECT name, lang, domain, kind, purity, created_at + FROM functions + ORDER BY created_at DESC + LIMIT 20 + +filters: {} + +sections: + # ---- Fila 0: KPIs ---- + - id: kpis + title: "Overview" + widgets: + - id: total_fn + type: kpi + title: "Total Funciones" + query: total_functions + mapping: { value: "value" } + span: 2 + + - id: fn_tested + type: kpi + title: "Con Tests" + query: functions_with_tests + mapping: { value: "value" } + span: 3 + + - id: fn_untested + type: kpi + title: "Sin Tests" + query: functions_without_tests + mapping: { value: "value" } + span: 2 + + - id: total_tp + type: kpi + title: "Total Tipos" + query: total_types + mapping: { value: "value" } + span: 3 + + - id: pending_pr + type: kpi + title: "Proposals Pendientes" + query: pending_proposals + mapping: { value: "value" } + span: 2 + + # ---- Fila 1: Distribucion general ---- + - id: distribution + title: "Distribucion" + columns: 3 + widgets: + - id: by_lang + type: pie_chart + title: "Funciones por Lenguaje" + query: by_lang + mapping: { name: "lang", value: "cantidad" } + span: 1 + + - id: by_domain + type: pie_chart + title: "Funciones por Dominio" + query: by_domain + mapping: { name: "domain", value: "cantidad" } + span: 1 + + - id: by_kind + type: bar_chart + title: "Funciones por Kind" + query: by_kind + mapping: { x: "kind", y: "cantidad" } + span: 1 + + # ---- Fila 2: Analisis profundo ---- + - id: analysis + title: "Analisis" + columns: 3 + widgets: + - id: purity_chart + type: bar_chart + title: "Puras vs Impuras" + query: purity + mapping: { x: "purity", y: "cantidad" } + span: 1 + + - id: most_used + type: bar_chart + title: "Funciones Mas Usadas por Otras" + query: most_used + mapping: { x: "funcion", y: "veces" } + options: { horizontal: true, height: 400 } + span: 1 + + - id: most_complex + type: bar_chart + title: "Mas Complejas (mas dependencias)" + query: most_complex + mapping: { x: "funcion", y: "dependencias" } + options: { horizontal: true, height: 400 } + span: 1 + + # ---- Fila 3: Cobertura + cross-table ---- + - id: coverage + title: "Cobertura y Lenguajes" + columns: 2 + widgets: + - id: test_coverage + type: bar_chart + title: "Cobertura de Tests por Dominio" + query: test_coverage_by_domain + mapping: + x: "domain" + series: + - { key: "con_tests", name: "Con Tests", color: "#10b981" } + - { key: "sin_tests", name: "Sin Tests", color: "#ef4444" } + options: { show_legend: true } + span: 1 + + - id: lang_domain_table + type: table + title: "Funciones por Lenguaje y Dominio" + query: lang_by_domain + mapping: + columns: + - { key: "domain", label: "Dominio" } + - { key: "go", label: "Go" } + - { key: "python", label: "Python" } + - { key: "bash", label: "Bash" } + - { key: "typescript", label: "TypeScript" } + - { key: "total", label: "Total" } + options: + heatmap_columns: ["go", "python", "bash", "typescript", "total"] + span: 1 + + # ---- Fila 4: Tipos + recientes ---- + - id: bottom + title: "Tipos y Funciones Recientes" + columns: 2 + widgets: + - id: types_table + type: table + title: "Tipos por Dominio y Algebraic" + query: types_by_domain + mapping: + columns: + - { key: "domain", label: "Dominio" } + - { key: "algebraic", label: "Algebraic" } + - { key: "cantidad", label: "Cantidad" } + span: 1 + + - id: recent_table + type: table + title: "Funciones Recientes (ultimas 20)" + query: recent_functions + mapping: + columns: + - { key: "name", label: "Nombre" } + - { key: "lang", label: "Lang" } + - { key: "domain", label: "Dominio" } + - { key: "kind", label: "Kind" } + - { key: "purity", label: "Purity" } + - { key: "created_at", label: "Creado" } + span: 1 diff --git a/filter_engine.go b/filter_engine.go new file mode 100644 index 0000000..fc654c2 --- /dev/null +++ b/filter_engine.go @@ -0,0 +1,40 @@ +package main + +import ( + "regexp" + "strconv" + "time" +) + +var relDateRe = regexp.MustCompile(`^now(?:-(\d+)([smhd]))?$`) + +// ResolveRelativeDate converts relative date strings like "now", "now-7d", "now-30d" +// to RFC3339 timestamps. +func ResolveRelativeDate(s string) (string, bool) { + m := relDateRe.FindStringSubmatch(s) + if m == nil { + return s, false + } + + now := time.Now() + + if m[1] == "" { + return now.Format(time.RFC3339), true + } + + n, _ := strconv.Atoi(m[1]) + var d time.Duration + + switch m[2] { + case "s": + d = time.Duration(n) * time.Second + case "m": + d = time.Duration(n) * time.Minute + case "h": + d = time.Duration(n) * time.Hour + case "d": + d = time.Duration(n) * 24 * time.Hour + } + + return now.Add(-d).Format(time.RFC3339), true +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c98eaae --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Rapid Dashboards + + +

+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..09d1801 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "rapid-dashboards", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "tsc -b && vite build", + "preview": "vite preview --host" + }, + "dependencies": { +"@base-ui/react": "^1.3.0", + "@phosphor-icons/react": "^2.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-slider": "^1.3.6", + "@tanstack/react-table": "^8.21.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.577.0", + "react": "^19.2.4", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.4", + "recharts": "^3.8.0", + "tailwind-merge": "^3.5.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "tailwindcss": "^4.2.2", + "typescript": "~5.9.3", + "vite": "^8.0.0" + } +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..4592c0a --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +052e44bcd719cd7db765d6c7fb248074 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..6f603fa --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,1628 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@base-ui/react': + specifier: ^1.3.0 + version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slider': + specifier: ^1.3.6 + version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + lucide-react: + specifier: ^0.577.0 + version: 0.577.0(react@19.2.4) + react: + specifier: ^19.2.4 + version: 19.2.4 + react-day-picker: + specifier: ^9.14.0 + version: 9.14.0(react@19.2.4) + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + recharts: + specifier: ^3.8.0 + version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)) + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.0 + version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^8.0.0 + version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1) + +packages: + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.3.0': + resolution: {integrity: sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@base-ui/utils@0.2.6': + resolution: {integrity: sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tabby_ai/hijri-converter@1.0.5': + resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} + engines: {node: '>=16.0.0'} + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lucide-react@0.577.0: + resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + react-day-picker@9.14.0: + resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-is@19.2.4: + resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@babel/runtime@7.29.2': {} + + '@base-ui/react@1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.2 + '@base-ui/utils': 0.2.6(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@base-ui/utils@0.2.6(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.29.2 + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + + '@date-fns/tz@1.4.1': {} + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.122.0': {} + + '@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@tabby_ai/hijri-converter@1.0.5': {} + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1) + + '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@tanstack/table-core@8.21.3': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/use-sync-external-store@0.0.6': {} + + '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1) + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + date-fns-jalali@4.1.0-0: {} + + date-fns@4.1.0: {} + + decimal.js-light@2.5.1: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + es-toolkit@1.45.1: {} + + eventemitter3@5.0.4: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + immer@10.2.0: {} + + immer@11.1.4: {} + + internmap@2.0.3: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lucide-react@0.577.0(react@19.2.4): + dependencies: + react: 19.2.4 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-day-picker@9.14.0(react@19.2.4): + dependencies: + '@date-fns/tz': 1.4.1 + '@tabby_ai/hijri-converter': 1.0.5 + date-fns: 4.1.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.4 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-is@19.2.4: {} + + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + + react@19.2.4: {} + + recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + reselect@5.1.1: {} + + rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + scheduler@0.27.0: {} + + source-map-js@1.2.1: {} + + tabbable@6.4.0: {} + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.6.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..69a156e --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState, useCallback } from 'react' +import { DashboardShell } from './components/DashboardShell' +import { useFilterState } from './hooks/useFilterState' +import type { DashboardConfig } from './types' +import type { FilterValues } from './hooks/useFilterState' + +// Register all built-in widgets. +import './components/widgets/register' + +// Wails-generated bindings. +let GetDashboardConfig: () => Promise +let GetWidgetData: (widgetID: string, filters: FilterValues) => Promise[]> +let ListDashboards: () => Promise +let SwitchDashboard: (fileName: string) => Promise + +interface DashboardInfo { + name: string + file: string + title: string + theme: string + current: boolean +} + +const dummyConfig: DashboardConfig = { settings: { title: 'Dev Mode', refresh: '30s', width: 1280, height: 800, columns: 12 }, theme: 'dark', queries: {}, filters: {}, sections: [] } + +// Import bindings — they call window.go which only exists inside Wails WebView +const bindings = await import('./wailsjs/go/main/App').catch(() => null) +GetDashboardConfig = bindings?.GetDashboardConfig as typeof GetDashboardConfig ?? (async () => dummyConfig) +GetWidgetData = bindings?.GetWidgetData as typeof GetWidgetData ?? (async () => []) +ListDashboards = bindings?.ListDashboards as typeof ListDashboards ?? (async () => []) +SwitchDashboard = bindings?.SwitchDashboard as typeof SwitchDashboard ?? (async () => dummyConfig) + +export default function App() { + const [config, setConfig] = useState(null) + const [dashboards, setDashboards] = useState([]) + const [error, setError] = useState(null) + const [switching, setSwitching] = useState(false) + + useEffect(() => { + GetDashboardConfig() + .then(cfg => { + setConfig(cfg) + // ListDashboards may fail if runtime not ready yet — retry once + ListDashboards() + .then(setDashboards) + .catch(() => setTimeout(() => ListDashboards().then(setDashboards).catch(() => {}), 1000)) + }) + .catch(err => setError(err.message ?? String(err))) + }, []) + + const handleSwitch = useCallback(async (fileName: string) => { + setSwitching(true) + try { + const newCfg = await SwitchDashboard(fileName) + setConfig(newCfg) + const list = await ListDashboards() + setDashboards(list) + } catch (err) { + setError((err as Error).message ?? String(err)) + } finally { + setSwitching(false) + } + }, []) + + if (error) { + return ( +
+
+

Dashboard Error

+
+            {error}
+          
+
+
+ ) + } + + if (!config || switching) { + return ( +
+

{switching ? 'Switching dashboard...' : 'Loading dashboard...'}

+
+ ) + } + + return +} + +function DashboardInner({ config, dashboards, onSwitch }: { + config: DashboardConfig + dashboards: DashboardInfo[] + onSwitch: (fileName: string) => void +}) { + const { values, setValue, reset } = useFilterState(config.filters ?? {}) + + useEffect(() => { + document.documentElement.setAttribute('data-theme', config.theme || 'dark') + }, [config.theme]) + + const getData = useCallback(async (widgetID: string, filters: FilterValues) => { + return GetWidgetData(widgetID, filters) + }, []) + + return ( + + ) +} diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..a506abc --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,176 @@ +@import "tailwindcss"; + +/* ========== DARK (default) — deep blue-grey ========== */ +:root, +[data-theme="dark"] { + --background: oklch(8% 0.015 260); + --foreground: oklch(95% 0.01 260); + --muted: oklch(18% 0.02 260); + --muted-foreground: oklch(60% 0.02 260); + --border: oklch(15% 0.01 260); + --primary: oklch(65% 0.22 260); + --primary-foreground: oklch(98% 0.01 260); + --secondary: oklch(20% 0.02 260); + --secondary-foreground: oklch(95% 0.01 260); + --accent: oklch(18% 0.03 260); + --accent-foreground: oklch(95% 0.01 260); + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 260); + --card: oklch(11% 0.015 260); + --card-foreground: oklch(95% 0.01 260); + --popover: oklch(12% 0.015 260); + --popover-foreground: oklch(95% 0.01 260); + --ring: oklch(65% 0.22 260); + --input: oklch(22% 0.02 260); + --radius: 0.5rem; + --success: oklch(65% 0.2 145); + --success-foreground: oklch(98% 0.01 145); + --chart-1: #3b82f6; + --chart-2: #10b981; + --chart-3: #f59e0b; + --chart-4: #ef4444; + --chart-5: #8b5cf6; +} + +/* ========== EMERALD — rich green, gold accents ========== */ +[data-theme="emerald"] { + --background: oklch(6% 0.03 155); + --foreground: oklch(92% 0.03 155); + --muted: oklch(15% 0.04 155); + --muted-foreground: oklch(58% 0.04 155); + --border: oklch(13% 0.03 155); + --primary: oklch(70% 0.22 155); + --primary-foreground: oklch(10% 0.03 155); + --secondary: oklch(18% 0.04 155); + --secondary-foreground: oklch(90% 0.03 155); + --accent: oklch(16% 0.05 155); + --accent-foreground: oklch(90% 0.03 155); + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 260); + --card: oklch(9% 0.03 155); + --card-foreground: oklch(92% 0.03 155); + --popover: oklch(10% 0.04 155); + --popover-foreground: oklch(92% 0.03 155); + --ring: oklch(70% 0.22 155); + --input: oklch(20% 0.04 155); + --success: oklch(70% 0.22 155); + --success-foreground: oklch(10% 0.03 155); + --chart-1: #10b981; + --chart-2: #f59e0b; + --chart-3: #06b6d4; + --chart-4: #8b5cf6; + --chart-5: #ec4899; +} + +/* ========== AMBER — warm dark, orange/amber accent ========== */ +[data-theme="amber"] { + --background: oklch(7% 0.02 55); + --foreground: oklch(93% 0.02 55); + --muted: oklch(16% 0.03 55); + --muted-foreground: oklch(58% 0.03 55); + --border: oklch(14% 0.025 55); + --primary: oklch(72% 0.2 65); + --primary-foreground: oklch(10% 0.02 55); + --secondary: oklch(18% 0.03 55); + --secondary-foreground: oklch(90% 0.02 55); + --accent: oklch(16% 0.04 55); + --accent-foreground: oklch(90% 0.02 55); + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 260); + --card: oklch(10% 0.025 55); + --card-foreground: oklch(93% 0.02 55); + --popover: oklch(11% 0.03 55); + --popover-foreground: oklch(93% 0.02 55); + --ring: oklch(72% 0.2 65); + --input: oklch(20% 0.03 55); + --success: oklch(65% 0.2 145); + --success-foreground: oklch(98% 0.01 145); + --chart-1: #f59e0b; + --chart-2: #ef4444; + --chart-3: #3b82f6; + --chart-4: #10b981; + --chart-5: #ec4899; +} + +/* ========== ROSE — dark with pink/rose accent ========== */ +[data-theme="rose"] { + --background: oklch(7% 0.02 350); + --foreground: oklch(93% 0.015 350); + --muted: oklch(16% 0.03 350); + --muted-foreground: oklch(58% 0.025 350); + --border: oklch(14% 0.025 350); + --primary: oklch(65% 0.22 350); + --primary-foreground: oklch(98% 0.01 350); + --secondary: oklch(18% 0.03 350); + --secondary-foreground: oklch(90% 0.015 350); + --accent: oklch(16% 0.04 350); + --accent-foreground: oklch(90% 0.015 350); + --destructive: oklch(55% 0.22 25); + --destructive-foreground: oklch(98% 0.01 260); + --card: oklch(10% 0.025 350); + --card-foreground: oklch(93% 0.015 350); + --popover: oklch(11% 0.03 350); + --popover-foreground: oklch(93% 0.015 350); + --ring: oklch(65% 0.22 350); + --input: oklch(20% 0.03 350); + --success: oklch(65% 0.2 145); + --success-foreground: oklch(98% 0.01 145); + --chart-1: #ec4899; + --chart-2: #8b5cf6; + --chart-3: #3b82f6; + --chart-4: #f59e0b; + --chart-5: #10b981; +} + +/* ========== LIGHT — clean light theme ========== */ +[data-theme="light"] { + --background: oklch(98% 0.005 260); + --foreground: oklch(15% 0.01 260); + --muted: oklch(93% 0.01 260); + --muted-foreground: oklch(45% 0.01 260); + --border: oklch(88% 0.01 260); + --primary: oklch(50% 0.22 260); + --primary-foreground: oklch(98% 0.01 260); + --secondary: oklch(94% 0.01 260); + --secondary-foreground: oklch(20% 0.01 260); + --accent: oklch(95% 0.01 260); + --accent-foreground: oklch(20% 0.01 260); + --destructive: oklch(50% 0.22 25); + --destructive-foreground: oklch(98% 0.01 260); + --card: oklch(100% 0 0); + --card-foreground: oklch(15% 0.01 260); + --popover: oklch(100% 0 0); + --popover-foreground: oklch(15% 0.01 260); + --ring: oklch(50% 0.22 260); + --input: oklch(88% 0.01 260); + --success: oklch(50% 0.2 145); + --success-foreground: oklch(98% 0.01 145); + --chart-1: #2563eb; + --chart-2: #059669; + --chart-3: #d97706; + --chart-4: #dc2626; + --chart-5: #7c3aed; +} + +body { + background-color: var(--background); + color: var(--foreground); + font-family: 'Geist Variable', system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* Remove card borders for seamless look on dark themes */ +[data-theme="dark"] [data-slot="card"], +[data-theme="emerald"] [data-slot="card"], +[data-theme="amber"] [data-slot="card"], +[data-theme="rose"] [data-slot="card"] { + border: none; + box-shadow: none; +} + +/* Light theme keeps subtle card shadows */ +[data-theme="light"] [data-slot="card"] { + border: 1px solid var(--border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); +} + diff --git a/frontend/src/components/DashboardShell.tsx b/frontend/src/components/DashboardShell.tsx new file mode 100644 index 0000000..423a61f --- /dev/null +++ b/frontend/src/components/DashboardShell.tsx @@ -0,0 +1,62 @@ +import { FilterBar } from './FilterBar' +import { Section } from './Section' +import { SimpleSelect } from '@fn_library' +import type { DashboardConfig } from '../types' +import type { FilterValues } from '../hooks/useFilterState' + +interface DashboardInfo { + name: string + file: string + title: string + theme: string + current: boolean +} + +interface DashboardShellProps { + config: DashboardConfig + filters: FilterValues + onFilterChange: (key: string, value: unknown) => void + onFilterReset: () => void + getData: (widgetID: string, filters: FilterValues) => Promise[]> + dashboards?: DashboardInfo[] + onSwitchDashboard?: (fileName: string) => void +} + +export function DashboardShell({ config, filters, onFilterChange, onFilterReset, getData, dashboards, onSwitchDashboard }: DashboardShellProps) { + return ( +
+ {/* Header */} +
+
+

{config.settings.title}

+ {dashboards && dashboards.length > 1 && onSwitchDashboard && ( + d.current)?.file ?? ''} + onValueChange={(v) => onSwitchDashboard(v)} + options={dashboards.map(d => ({ value: d.file, label: `${d.title} (${d.theme})` }))} + /> + )} +
+ +
+ + {/* Sections */} + {(config.sections ?? []).map(section => ( +
+ ))} +
+ ) +} diff --git a/frontend/src/components/FilterBar.tsx b/frontend/src/components/FilterBar.tsx new file mode 100644 index 0000000..ed0532d --- /dev/null +++ b/frontend/src/components/FilterBar.tsx @@ -0,0 +1,119 @@ +import { useCallback, useRef, useEffect, useState } from 'react' +import { SimpleSelect } from '@fn_library' +import type { FilterDef, FilterPreset } from '../types' +import type { FilterValues } from '../hooks/useFilterState' + +interface FilterBarProps { + filters: Record + values: FilterValues + onChange: (key: string, value: unknown) => void + onReset: () => void +} + +export function FilterBar({ filters, values, onChange, onReset }: FilterBarProps) { + const entries = Object.entries(filters) + if (entries.length === 0) return null + + return ( +
+ {entries.map(([key, def]) => ( + + ))} + +
+ ) +} + +interface FilterControlProps { + id: string + def: FilterDef + value: unknown + onChange: (key: string, value: unknown) => void +} + +function FilterControl({ id, def, value, onChange }: FilterControlProps) { + switch (def.type) { + case 'select': + return + case 'date_range': + return } onChange={onChange} /> + case 'text': + return + default: + return null + } +} + +function SelectFilter({ id, def, value, onChange }: { id: string; def: FilterDef; value: string; onChange: (k: string, v: unknown) => void }) { + return ( +
+ + onChange(id, v)} + options={def.options?.map(opt => ({ value: opt.value, label: opt.label })) ?? []} + /> +
+ ) +} + +function DateRangeFilter({ id, def, value, onChange }: { id: string; def: FilterDef; value: Record; onChange: (k: string, v: unknown) => void }) { + const presets = def.presets ?? [] + const currentFrom = value?.from ?? '' + + return ( +
+ +
+ {presets.map((preset: FilterPreset) => ( + + ))} +
+
+ ) +} + +function TextFilter({ id, def, value, onChange }: { id: string; def: FilterDef; value: string; onChange: (k: string, v: unknown) => void }) { + const [localValue, setLocalValue] = useState(value ?? '') + const timerRef = useRef>(undefined) + const debounce = def.debounce ?? 300 + + useEffect(() => { + setLocalValue(value ?? '') + }, [value]) + + const handleChange = useCallback((e: React.ChangeEvent) => { + const v = e.target.value + setLocalValue(v) + clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => onChange(id, v), debounce) + }, [id, onChange, debounce]) + + return ( +
+ + +
+ ) +} diff --git a/frontend/src/components/Section.tsx b/frontend/src/components/Section.tsx new file mode 100644 index 0000000..dba559a --- /dev/null +++ b/frontend/src/components/Section.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react' +import { ChevronDown, ChevronRight } from 'lucide-react' +import { WidgetRenderer } from './WidgetRenderer' +import type { SectionDef, QueryDef } from '../types' +import type { FilterValues } from '../hooks/useFilterState' + +interface SectionProps { + section: SectionDef + queries: Record + filters: FilterValues + globalColumns: number + getData: (widgetID: string, filters: FilterValues) => Promise[]> +} + +export function Section({ section, queries, filters, globalColumns, getData }: SectionProps) { + const [collapsed, setCollapsed] = useState(false) + const columns = section.columns || globalColumns + + return ( +
+ + + {!collapsed && ( +
+ {section.widgets.map(widget => ( +
+ +
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/src/components/WidgetRenderer.tsx b/frontend/src/components/WidgetRenderer.tsx new file mode 100644 index 0000000..40b4315 --- /dev/null +++ b/frontend/src/components/WidgetRenderer.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState, useCallback, useRef } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from '@fn_library' +import { getWidget } from './widget-registry' +import type { WidgetDef, QueryDef } from '../types' +import type { FilterValues } from '../hooks/useFilterState' + +interface WidgetRendererProps { + widget: WidgetDef + queryDef: QueryDef + filters: FilterValues + getData: (widgetID: string, filters: FilterValues) => Promise[]> +} + +export function WidgetRenderer({ widget, queryDef, filters, getData }: WidgetRendererProps) { + const [data, setData] = useState[] | null>(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [countdown, setCountdown] = useState(0) + const mountedRef = useRef(true) + const lastDataRef = useRef('') + + const refreshSec = Math.max(Math.round((queryDef.refreshMs ?? 30000) / 1000), 1) + + const fetch = useCallback(async () => { + try { + const result = await getData(widget.id, filters) + if (!mountedRef.current) return + + const serialized = JSON.stringify(result) + if (serialized !== lastDataRef.current) { + lastDataRef.current = serialized + setData(result) + } + setError(null) + } catch (err) { + if (!mountedRef.current) return + setError(err as Error) + } finally { + if (mountedRef.current) { + setLoading(false) + setCountdown(refreshSec) + } + } + }, [widget.id, filters, getData, refreshSec]) + + // Initial fetch. + useEffect(() => { + mountedRef.current = true + setLoading(true) + fetch() + return () => { mountedRef.current = false } + }, [fetch]) + + // Refetch on interval. + useEffect(() => { + if (!queryDef.refreshMs || queryDef.refreshMs <= 0) return + const interval = setInterval(fetch, queryDef.refreshMs) + return () => clearInterval(interval) + }, [fetch, queryDef.refreshMs]) + + // Countdown tick. + useEffect(() => { + const tick = setInterval(() => { + setCountdown(c => (c > 0 ? c - 1 : 0)) + }, 1000) + return () => clearInterval(tick) + }, []) + + const Component = getWidget(widget.type) + if (!Component) { + return ( + + + {widget.title} + + +

+ Unknown widget type: {widget.type} +

+
+
+ ) + } + + return ( +
+ + + {countdown}s + +
+ ) +} diff --git a/frontend/src/components/widget-registry.ts b/frontend/src/components/widget-registry.ts new file mode 100644 index 0000000..05f23f3 --- /dev/null +++ b/frontend/src/components/widget-registry.ts @@ -0,0 +1,18 @@ +import type { ComponentType } from 'react' +import type { WidgetProps } from '../types' + +const WIDGET_REGISTRY: Record> = {} + +export function registerWidget(type: string, component: ComponentType) { + WIDGET_REGISTRY[type] = component +} + +export function getWidget(type: string): ComponentType | undefined { + return WIDGET_REGISTRY[type] +} + +export function getRegisteredTypes(): string[] { + return Object.keys(WIDGET_REGISTRY) +} + +export { WIDGET_REGISTRY } diff --git a/frontend/src/components/widgets/AreaChartWidget.tsx b/frontend/src/components/widgets/AreaChartWidget.tsx new file mode 100644 index 0000000..06be241 --- /dev/null +++ b/frontend/src/components/widgets/AreaChartWidget.tsx @@ -0,0 +1,73 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@fn_library' +import { + AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, + Legend, ResponsiveContainer, +} from 'recharts' +import type { WidgetProps } from '../../types' + +const COLORS = [ + 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', + 'var(--chart-4)', 'var(--chart-5)', 'var(--chart-6, #ec4899)', +] + +export function AreaChartWidget({ widget, data, loading, error }: WidgetProps) { + const mapping = widget.mapping ?? {} + const xKey = mapping.x as string ?? 'x' + const options = widget.options ?? {} + + const series = Array.isArray(mapping.series) + ? (mapping.series as Array<{ key: string; name: string; color?: string }>) + : undefined + + const yKey = !series ? (mapping.y as string) : undefined + const areas = series + ? series.map((s, i) => ({ dataKey: s.key, name: s.name, color: s.color ?? COLORS[i % COLORS.length] })) + : yKey ? [{ dataKey: yKey, name: yKey, color: COLORS[0] }] : [] + + return ( + + + + {widget.title} + + + + {loading && !data ? ( +
Loading...
+ ) : error ? ( +
{error.message}
+ ) : ( + + + {options.show_grid !== false && } + + + + {!!options.show_legend && } + + {areas.map(area => ( + + + + + ))} + + {areas.map(area => ( + + ))} + + + )} +
+
+ ) +} diff --git a/frontend/src/components/widgets/BarChartWidget.tsx b/frontend/src/components/widgets/BarChartWidget.tsx new file mode 100644 index 0000000..85cb5a0 --- /dev/null +++ b/frontend/src/components/widgets/BarChartWidget.tsx @@ -0,0 +1,79 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@fn_library' +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, + Legend, ResponsiveContainer, +} from 'recharts' +import type { WidgetProps } from '../../types' + +const COLORS = [ + 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', + 'var(--chart-4)', 'var(--chart-5)', 'var(--chart-6, #ec4899)', +] + +export function BarChartWidget({ widget, data, loading, error }: WidgetProps) { + const mapping = widget.mapping ?? {} + const xKey = mapping.x as string ?? 'x' + const options = widget.options ?? {} + + const series = Array.isArray(mapping.series) + ? (mapping.series as Array<{ key: string; name: string; color?: string }>) + : undefined + + const yKey = !series ? (mapping.y as string) : undefined + const bars = series + ? series.map((s, i) => ({ dataKey: s.key, name: s.name, fill: s.color ?? COLORS[i % COLORS.length] })) + : yKey ? [{ dataKey: yKey, name: yKey, fill: COLORS[0] }] : [] + + const isHorizontal = !!options.horizontal + + return ( + + + + {widget.title} + + + + {loading && !data ? ( +
Loading...
+ ) : error ? ( +
{error.message}
+ ) : ( + + + {options.show_grid !== false && } + {isHorizontal ? ( + <> + + + + ) : ( + <> + + + + )} + + {!!options.show_legend && } + {bars.map(bar => ( + + ))} + + + )} +
+
+ ) +} diff --git a/frontend/src/components/widgets/KPIWidget.tsx b/frontend/src/components/widgets/KPIWidget.tsx new file mode 100644 index 0000000..0d66157 --- /dev/null +++ b/frontend/src/components/widgets/KPIWidget.tsx @@ -0,0 +1,81 @@ +import { KPICard, Sparkline } from '@fn_library' +import type { WidgetProps } from '../../types' + +export function KPIWidget({ widget, data, loading }: WidgetProps) { + const mapping = widget.mapping ?? {} + const valueKey = mapping.value as string ?? 'value' + const format = mapping.format as string + const unitKey = mapping.unit as string | undefined + const deltaKey = mapping.delta as string | undefined + const deltaLabel = mapping.deltaLabel as string | undefined + const deltaSuffix = mapping.deltaSuffix as string | undefined + const sparklineKey = mapping.sparkline as string | undefined + const sparklineColors = mapping.sparklineColors as string[] | undefined + + let displayValue: string | number = '—' + let displayUnit: string | undefined + let delta: { value: number; isPositive: boolean; label?: string; suffix?: string } | undefined + let sparklineData: number[] | undefined + + if (data && data.length > 0) { + const row = data[0] + displayValue = formatValue(row[valueKey], format) + + if (unitKey && row[unitKey] != null) { + displayUnit = String(row[unitKey]) + } else if (mapping.unitLabel) { + displayUnit = mapping.unitLabel as string + } + + if (deltaKey && row[deltaKey] != null) { + const dv = Number(row[deltaKey]) + if (!isNaN(dv)) { + delta = { value: dv, isPositive: dv >= 0, label: deltaLabel, suffix: deltaSuffix } + } + } + + if (sparklineKey) { + sparklineData = data.map(r => Number(r[sparklineKey]) || 0) + } + } + + return ( + 0 ? ( + + ) : undefined} + size="default" + className="border-0 shadow-none" + /> + ) +} + +function formatValue(raw: unknown, format?: string): string | number { + if (raw == null) return '—' + const num = Number(raw) + if (isNaN(num)) return String(raw) + if (!format) return num + + if (format.includes('f')) { + const match = format.match(/\.(\d+)f/) + const decimals = match ? parseInt(match[1]) : 0 + let str = num.toFixed(decimals) + if (format.includes(',')) { + str = Number(str).toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + } + if (format.startsWith('$')) str = '$' + str + return str + } + if (format === ',') return num.toLocaleString() + return num +} diff --git a/frontend/src/components/widgets/LineChartWidget.tsx b/frontend/src/components/widgets/LineChartWidget.tsx new file mode 100644 index 0000000..10d18a7 --- /dev/null +++ b/frontend/src/components/widgets/LineChartWidget.tsx @@ -0,0 +1,66 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@fn_library' +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, + Legend, Brush, ResponsiveContainer, +} from 'recharts' +import type { WidgetProps } from '../../types' + +const COLORS = [ + 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', + 'var(--chart-4)', 'var(--chart-5)', 'var(--chart-6, #ec4899)', +] + +export function LineChartWidget({ widget, data, loading, error }: WidgetProps) { + const mapping = widget.mapping ?? {} + const xKey = mapping.x as string ?? 'x' + const options = widget.options ?? {} + + const series = Array.isArray(mapping.series) + ? (mapping.series as Array<{ key: string; name: string; color?: string }>) + : undefined + + const yKey = !series ? (mapping.y as string) : undefined + const lines = series + ? series.map((s, i) => ({ dataKey: s.key, name: s.name, stroke: s.color ?? COLORS[i % COLORS.length] })) + : yKey ? [{ dataKey: yKey, name: yKey, stroke: COLORS[0] }] : [] + + return ( + + + + {widget.title} + + + + {loading && !data ? ( +
Loading...
+ ) : error ? ( +
{error.message}
+ ) : ( + + + {options.show_grid !== false && } + + + + {!!options.show_legend && } + {lines.map(line => ( + + ))} + {!!options.zoomable && } + + + )} +
+
+ ) +} diff --git a/frontend/src/components/widgets/PieChartWidget.tsx b/frontend/src/components/widgets/PieChartWidget.tsx new file mode 100644 index 0000000..d000dc3 --- /dev/null +++ b/frontend/src/components/widgets/PieChartWidget.tsx @@ -0,0 +1,78 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@fn_library' +import { + PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, +} from 'recharts' +import type { PieLabelRenderProps } from 'recharts' +import type { WidgetProps } from '../../types' + +const COLORS = [ + 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', + 'var(--chart-4)', 'var(--chart-5)', 'var(--chart-6, #ec4899)', + 'var(--chart-7, #06b6d4)', 'var(--chart-8, #f97316)', +] + +function renderLabel(props: PieLabelRenderProps): string { + const name = props.name ?? '' + const pct = typeof props.percent === 'number' ? (props.percent * 100).toFixed(0) : '0' + return `${name} ${pct}%` +} + +export function PieChartWidget({ widget, data, loading, error }: WidgetProps) { + const mapping = widget.mapping ?? {} + const nameKey = mapping.name as string ?? mapping.x as string ?? 'name' + const valueKey = mapping.value as string ?? mapping.y as string ?? 'value' + const options = widget.options ?? {} + + // Ensure numeric values for Recharts Pie + const pieData = (data ?? []).map(row => ({ + ...row, + [valueKey]: Number(row[valueKey]) || 0, + })) + + return ( + + + + {widget.title} + + + + {loading && !data ? ( +
Loading...
+ ) : error ? ( +
{error.message}
+ ) : ( + + + + {pieData.map((_, i) => ( + + ))} + + + {options.show_legend !== false && ( + + )} + + + )} +
+
+ ) +} diff --git a/frontend/src/components/widgets/SparklineWidget.tsx b/frontend/src/components/widgets/SparklineWidget.tsx new file mode 100644 index 0000000..ea1bf7b --- /dev/null +++ b/frontend/src/components/widgets/SparklineWidget.tsx @@ -0,0 +1,29 @@ +import { Card, CardContent } from '@fn_library' +import { Sparkline } from '@fn_library' +import type { WidgetProps } from '../../types' + +export function SparklineWidget({ widget, data, loading }: WidgetProps) { + const mapping = widget.mapping ?? {} + const valueKey = mapping.value as string ?? 'value' + const options = widget.options ?? {} + + const values = (data ?? []).map(row => Number(row[valueKey]) || 0) + + return ( + + +

{widget.title}

+ {loading && values.length === 0 ? ( +
Loading...
+ ) : ( + + )} +
+
+ ) +} diff --git a/frontend/src/components/widgets/TableWidget.tsx b/frontend/src/components/widgets/TableWidget.tsx new file mode 100644 index 0000000..ac0bcd4 --- /dev/null +++ b/frontend/src/components/widgets/TableWidget.tsx @@ -0,0 +1,114 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@fn_library' +import type { WidgetProps } from '../../types' + +interface ColumnDef { + key: string + label: string + format?: string +} + +export function TableWidget({ widget, data, loading, error }: WidgetProps) { + const mapping = widget.mapping ?? {} + const columns = (mapping.columns as ColumnDef[]) ?? [] + const options = widget.options ?? {} + const heatmapKeys = (options.heatmap_columns as string[]) ?? [] + + // Auto-detect columns from first row if not specified. + const effectiveColumns: ColumnDef[] = columns.length > 0 + ? columns + : data && data.length > 0 + ? Object.keys(data[0]).map(k => ({ key: k, label: k })) + : [] + + // Compute heatmap ranges per column + const heatmapRanges: Record = {} + if (heatmapKeys.length > 0 && data && data.length > 0) { + for (const key of heatmapKeys) { + const values = data.map(r => Number(r[key])).filter(n => !isNaN(n)) + if (values.length > 0) { + heatmapRanges[key] = { min: Math.min(...values), max: Math.max(...values) } + } + } + } + + function heatmapStyle(key: string, value: unknown): React.CSSProperties | undefined { + const range = heatmapRanges[key] + if (!range || range.max === range.min) return undefined + const num = Number(value) + if (isNaN(num)) return undefined + const t = (num - range.min) / (range.max - range.min) + const alpha = 0.1 + t * 0.55 + return { backgroundColor: `color-mix(in srgb, var(--chart-1) ${Math.round(alpha * 100)}%, transparent)` } + } + + return ( + + + + {widget.title} + + + + {loading && !data ? ( +
Loading...
+ ) : error ? ( +
{error.message}
+ ) : ( +
+ + + + {effectiveColumns.map(col => ( + + ))} + + + + {(data ?? []).map((row, i) => ( + + {effectiveColumns.map(col => ( + + ))} + + ))} + +
+ {col.label} +
+ {formatCell(row[col.key], col.format)} +
+ {(!data || data.length === 0) && ( +

No data

+ )} +
+ )} +
+
+ ) +} + +function formatCell(value: unknown, format?: string): string { + if (value == null) return '—' + if (!format) return String(value) + + const num = Number(value) + if (format === 'datetime' && !isNaN(Date.parse(String(value)))) { + return new Date(String(value)).toLocaleString() + } + if (!isNaN(num)) { + if (format.includes('f')) { + const match = format.match(/\.(\d+)f/) + const d = match ? parseInt(match[1]) : 0 + let str = num.toFixed(d) + if (format.includes(',')) str = Number(str).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }) + if (format.startsWith('$')) str = '$' + str + return str + } + if (format === ',') return num.toLocaleString() + } + return String(value) +} diff --git a/frontend/src/components/widgets/register.ts b/frontend/src/components/widgets/register.ts new file mode 100644 index 0000000..8a4c47b --- /dev/null +++ b/frontend/src/components/widgets/register.ts @@ -0,0 +1,19 @@ +// Registers all built-in widget types. +// To add a new widget: create the component, then add registerWidget() here. + +import { registerWidget } from '../widget-registry' +import { KPIWidget } from './KPIWidget' +import { LineChartWidget } from './LineChartWidget' +import { BarChartWidget } from './BarChartWidget' +import { AreaChartWidget } from './AreaChartWidget' +import { PieChartWidget } from './PieChartWidget' +import { SparklineWidget } from './SparklineWidget' +import { TableWidget } from './TableWidget' + +registerWidget('kpi', KPIWidget) +registerWidget('line_chart', LineChartWidget) +registerWidget('bar_chart', BarChartWidget) +registerWidget('area_chart', AreaChartWidget) +registerWidget('pie_chart', PieChartWidget) +registerWidget('sparkline', SparklineWidget) +registerWidget('table', TableWidget) diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..1b81eff --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,18 @@ +/// + +// Types for @fn_library — resolved at build time via Vite alias to frontend/functions/ui/ +declare module '@fn_library' { + import type { FC } from 'react' + export const Card: FC + export const CardContent: FC + export const CardHeader: FC + export const CardTitle: FC + export const KPICard: FC + export const Sparkline: FC + export const Badge: FC + export const Button: FC + export const Input: FC + export const Skeleton: FC + export interface SimpleSelectOption { value: string; label: string; disabled?: boolean } + export function SimpleSelect(props: { value: string; onValueChange: (value: string) => void; options: SimpleSelectOption[]; placeholder?: string; disabled?: boolean; size?: 'sm' | 'default'; className?: string }): React.ReactElement +} diff --git a/frontend/src/hooks/useFilterState.ts b/frontend/src/hooks/useFilterState.ts new file mode 100644 index 0000000..fc6764c --- /dev/null +++ b/frontend/src/hooks/useFilterState.ts @@ -0,0 +1,27 @@ +import { useState, useCallback, useMemo } from 'react' +import type { FilterDef } from '../types' + +export type FilterValues = Record + +function initializeDefaults(filters: Record): FilterValues { + const values: FilterValues = {} + for (const [key, def] of Object.entries(filters)) { + values[key] = def.default ?? (def.type === 'text' ? '' : null) + } + return values +} + +export function useFilterState(filters: Record) { + const defaults = useMemo(() => initializeDefaults(filters), [filters]) + const [values, setValues] = useState(defaults) + + const setValue = useCallback((key: string, value: unknown) => { + setValues(prev => ({ ...prev, [key]: value })) + }, []) + + const reset = useCallback(() => { + setValues(defaults) + }, [defaults]) + + return { values, setValue, reset } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..7d02351 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './app.css' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..10154f1 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,74 @@ +// Types mirroring Go config — sent via GetDashboardConfig() IPC. + +export interface DashboardConfig { + settings: Settings + theme: string + queries: Record + filters: Record + sections: SectionDef[] +} + +export interface Settings { + title: string + refresh: string + width: number + height: number + columns: number +} + +export interface QueryDef { + connection: string + refresh: string + staleTime: string + params: Record | null + refreshMs: number + staleMs: number +} + +export interface FilterDef { + type: 'select' | 'date_range' | 'text' + label: string + default: unknown + options?: FilterOption[] + presets?: FilterPreset[] + placeholder?: string + debounce?: number +} + +export interface FilterOption { + label: string + value: string +} + +export interface FilterPreset { + label: string + from: string + to: string +} + +export interface SectionDef { + id: string + title: string + collapsible: boolean + columns?: number + widgets: WidgetDef[] +} + +export interface WidgetDef { + id: string + type: string + title: string + query: string + mapping: Record + options?: Record + span: number + rowSpan?: number +} + +// Widget component props — all widget components receive this. +export interface WidgetProps { + widget: WidgetDef + data: Record[] | null + loading: boolean + error: Error | null +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..d610747 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@wails/*": ["./src/wailsjs/*"], + "@fn_library": ["./src/env.d.ts"] + } + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..afa62ca --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import { resolve } from 'path' + + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + '@wails': resolve(__dirname, './wailsjs'), + '@fn_library': resolve(__dirname, '../../../frontend/functions/ui'), + }, + dedupe: ['react', 'react-dom'], + }, + server: { + port: 5173, + }, +}) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2923ac5 --- /dev/null +++ b/go.mod @@ -0,0 +1,74 @@ +module rapid-dashboards + +go 1.25.0 + +require ( + fn-registry v0.0.0 + github.com/wailsapp/wails/v2 v2.11.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/ClickHouse/ch-go v0.71.0 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.44.0 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/apache/arrow-go/v18 v18.1.0 // indirect + github.com/bep/debounce v1.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/flatbuffers v25.1.24+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect + github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/labstack/echo/v4 v4.13.3 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leaanthony/go-ansi-parser v1.6.1 // indirect + github.com/leaanthony/gosod v1.0.4 // indirect + github.com/leaanthony/slicer v1.6.0 // indirect + github.com/leaanthony/u v1.1.1 // indirect + github.com/marcboeker/go-duckdb v1.8.5 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.37 // indirect + github.com/paulmach/orb v0.12.0 // indirect + github.com/pierrec/lz4/v4 v4.1.25 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/tkrajina/go-reflector v0.5.8 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wailsapp/go-webview2 v1.0.22 // indirect + github.com/wailsapp/mimetype v1.4.1 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.41.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect +) + +replace fn-registry => /home/lucas/fn_registry diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eb7a3b2 --- /dev/null +++ b/go.sum @@ -0,0 +1,231 @@ +github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= +github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apache/arrow-go/v18 v18.1.0 h1:agLwJUiVuwXZdwPYVrlITfx7bndULJ/dggbnLFgDp/Y= +github.com/apache/arrow-go/v18 v18.1.0/go.mod h1:tigU/sIgKNXaesf5d7Y95jBBKS5KsxTqYBKXFsvKzo0= +github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= +github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= +github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= +github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= +github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= +github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= +github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= +github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI= +github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw= +github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= +github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= +github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= +github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= +github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= +github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= +github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg= +github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= +github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58= +github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= +github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ= +github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo= +golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c7a3b90 --- /dev/null +++ b/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "database/sql" + "embed" + "flag" + "fmt" + "log" + "os" + "path/filepath" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend/dist +var assets embed.FS + +// examplesDir returns the path to the examples/ directory. +// Checks cwd first (wails dev), then next to the executable (production). +func examplesDir() string { + // 1. examples/ in cwd (wails dev runs from project root) + if info, err := os.Stat("examples"); err == nil && info.IsDir() { + return "examples" + } + + // 2. examples/ next to the executable (production binary) + if exePath, err := os.Executable(); err == nil { + dir := filepath.Join(filepath.Dir(exePath), "examples") + if info, err := os.Stat(dir); err == nil && info.IsDir() { + return dir + } + } + + return "" +} + +func firstYAML(dir string) string { + entries, err := os.ReadDir(dir) + if err != nil { + return "" + } + for _, e := range entries { + if !e.IsDir() && (filepath.Ext(e.Name()) == ".yaml" || filepath.Ext(e.Name()) == ".yml") { + return filepath.Join(dir, e.Name()) + } + } + return "" +} + +func main() { + // File logger for debugging + logFile, err := os.OpenFile("rapid_dashboards.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err == nil { + log.SetOutput(logFile) + defer logFile.Close() + } + + log.Printf("=== rapid_dashboards starting ===") + log.Printf("os.Args: %v", os.Args) + log.Printf("DASHBOARD env: %q", os.Getenv("DASHBOARD")) + log.Printf("cwd: %s", func() string { d, _ := os.Getwd(); return d }()) + + dashboardPath := flag.String("dashboard", "", "path to dashboard YAML file") + flag.Parse() + + log.Printf("flag --dashboard: %q", *dashboardPath) + + // Resolve examples directory — always used for listing/switching dashboards. + dashDir := examplesDir() + log.Printf("examples dir: %q", dashDir) + + // Fallback: CLI flag → env var → first YAML in examples/ + if *dashboardPath == "" { + if envPath := os.Getenv("DASHBOARD"); envPath != "" { + *dashboardPath = envPath + log.Printf("using DASHBOARD env var: %q", envPath) + } else if dashDir != "" { + if first := firstYAML(dashDir); first != "" { + *dashboardPath = first + log.Printf("auto-discovered dashboard: %q", first) + } + } + } + + var cfg *DashboardConfig + var pool map[string]*sql.DB + + if *dashboardPath == "" { + log.Printf("NO dashboard path — using dummy config (bindings mode)") + cfg = &DashboardConfig{ + Settings: Settings{Title: "rapid-dashboards", Width: 1280, Height: 800, Columns: 12}, + Queries: map[string]QueryDef{}, + Filters: map[string]FilterDef{}, + Sections: []SectionDef{}, + Connections: map[string]ConnConfig{}, + } + } else { + log.Printf("loading dashboard from: %q", *dashboardPath) + var loadErr error + cfg, loadErr = LoadDashboard(*dashboardPath) + if loadErr != nil { + log.Printf("ERROR loading dashboard: %v", loadErr) + fmt.Fprintf(os.Stderr, "error loading dashboard: %v\n", loadErr) + os.Exit(1) + } + log.Printf("dashboard loaded OK — title=%q sections=%d queries=%d", + cfg.Settings.Title, len(cfg.Sections), len(cfg.Queries)) + + yamlDir := filepath.Dir(*dashboardPath) + pool, loadErr = OpenConnections(cfg.Connections, yamlDir) + if loadErr != nil { + log.Printf("ERROR opening connections: %v", loadErr) + fmt.Fprintf(os.Stderr, "error opening connections: %v\n", loadErr) + os.Exit(1) + } + defer CloseConnections(pool) + log.Printf("connections opened: %d", len(pool)) + } + + engine := NewQueryEngine(cfg, pool) + app := NewApp(cfg, engine, pool, dashDir, *dashboardPath) + + log.Printf("starting wails — title=%q width=%d height=%d dashDir=%q", cfg.Settings.Title, cfg.Settings.Width, cfg.Settings.Height, dashDir) + + runErr := wails.Run(&options.App{ + Title: cfg.Settings.Title, + Width: cfg.Settings.Width, + Height: cfg.Settings.Height, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + BackgroundColour: &options.RGBA{R: 10, G: 10, B: 15, A: 1}, + OnStartup: app.startup, + Bind: []interface{}{ + app, + }, + }) + + if runErr != nil { + log.Printf("ERROR wails.Run: %v", runErr) + fmt.Fprintf(os.Stderr, "error: %v\n", runErr) + os.Exit(1) + } + log.Printf("=== rapid_dashboards exited cleanly ===") +} diff --git a/query_engine.go b/query_engine.go new file mode 100644 index 0000000..103953e --- /dev/null +++ b/query_engine.go @@ -0,0 +1,156 @@ +package main + +import ( + "database/sql" + "fmt" + "regexp" + "strings" + + "fn-registry/functions/infra" +) + +var namedParamRe = regexp.MustCompile(`:([a-zA-Z_][a-zA-Z0-9_]*)`) + +// QueryEngine executes queries with parameter resolution. +type QueryEngine struct { + config *DashboardConfig + pool map[string]*sql.DB +} + +func NewQueryEngine(cfg *DashboardConfig, pool map[string]*sql.DB) *QueryEngine { + return &QueryEngine{config: cfg, pool: pool} +} + +// Execute runs the query for a widget, resolving filter references and named params. +func (qe *QueryEngine) Execute(widgetID string, filters map[string]any) ([]map[string]any, error) { + // Find the widget and its query. + widget, err := qe.findWidget(widgetID) + if err != nil { + return nil, err + } + + qdef, ok := qe.config.Queries[widget.Query] + if !ok { + return nil, fmt.Errorf("query %q not found", widget.Query) + } + + db, ok := qe.pool[qdef.Connection] + if !ok { + return nil, fmt.Errorf("connection %q not found", qdef.Connection) + } + + // Resolve parameter values: static or $filter.xxx references. + resolvedParams := qe.resolveParams(qdef.Params, filters) + + // Convert :name placeholders to driver-appropriate positional params. + driver := qe.config.Connections[qdef.Connection].Driver + query, args := convertNamedParams(qdef.SQL, resolvedParams, driver) + + return infra.DBQuery(db, query, args...) +} + +func (qe *QueryEngine) findWidget(id string) (*WidgetDef, error) { + for _, sec := range qe.config.Sections { + for i := range sec.Widgets { + if sec.Widgets[i].ID == id { + return &sec.Widgets[i], nil + } + } + } + return nil, fmt.Errorf("widget %q not found", id) +} + +// resolveParams replaces $filter.xxx references with actual filter values +// and resolves relative dates. +func (qe *QueryEngine) resolveParams(params map[string]string, filters map[string]any) map[string]string { + resolved := make(map[string]string, len(params)) + + for name, ref := range params { + val := ref + + // $filter.date_range.from → filters["date_range"]["from"] + if strings.HasPrefix(ref, "$filter.") { + val = resolveFilterRef(ref, filters) + } + + // Resolve relative dates (now-7d, etc.) + if resolved, ok := ResolveRelativeDate(val); ok { + val = resolved + } + + resolved[name] = val + } + + return resolved +} + +// resolveFilterRef extracts a value from the filters map using dot notation. +// "$filter.date_range.from" → filters["date_range"] → map["from"] +func resolveFilterRef(ref string, filters map[string]any) string { + // Strip "$filter." prefix. + path := strings.TrimPrefix(ref, "$filter.") + parts := strings.SplitN(path, ".", 2) + + v, ok := filters[parts[0]] + if !ok { + return "" + } + + // Simple value (e.g. $filter.category). + if len(parts) == 1 { + return fmt.Sprint(v) + } + + // Nested value (e.g. $filter.date_range.from). + if m, ok := v.(map[string]any); ok { + if val, ok := m[parts[1]]; ok { + return fmt.Sprint(val) + } + } + + return "" +} + +// convertNamedParams replaces :name placeholders with positional params ($1, ?, etc.) +// and builds the args slice in the correct order. +func convertNamedParams(query string, params map[string]string, driver string) (string, []any) { + matches := namedParamRe.FindAllStringSubmatch(query, -1) + if len(matches) == 0 { + return query, nil + } + + // Deduplicate and order params as they appear. + seen := make(map[string]int) + var ordered []string + for _, m := range matches { + name := m[1] + if _, ok := seen[name]; !ok { + seen[name] = len(ordered) + ordered = append(ordered, name) + } + } + + // Build args slice. + args := make([]any, len(ordered)) + for i, name := range ordered { + args[i] = params[name] + } + + // Replace :name with positional placeholder. + result := namedParamRe.ReplaceAllStringFunc(query, func(match string) string { + name := match[1:] // strip leading ':' + idx := seen[name] + return placeholder(driver, idx) + }) + + return result, args +} + +func placeholder(driver string, idx int) string { + switch driver { + case "postgres": + return fmt.Sprintf("$%d", idx+1) + default: + return "?" + } +} diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..35bce54 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Lanza rapid_dashboards en modo desarrollo (wails dev). +# El dashboard inicial se auto-descubre desde examples/. +# El usuario puede cambiar de dashboard desde la UI. +set -euo pipefail + +APP_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$APP_DIR" + +echo "Dashboards disponibles en examples/:" +for f in examples/*.yaml; do + echo " $(basename "$f" .yaml)" +done +echo "" +echo "-> wails dev (selecciona dashboard desde la UI)" +wails dev diff --git a/scripts/prod.sh b/scripts/prod.sh new file mode 100755 index 0000000..c86c08b --- /dev/null +++ b/scripts/prod.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Compila y lanza rapid_dashboards en modo produccion. +# Uso: ./scripts/prod.sh [env vars...] +# Ejemplo: DB_PASSWORD=secret ./scripts/prod.sh fn_registry_overview +set -euo pipefail + +APP_DIR="$(cd "$(dirname "$0")/.." && pwd)" +EXAMPLES_DIR="$APP_DIR/examples" +BIN="$APP_DIR/build/bin/rapid-dashboards" + +if [ $# -eq 0 ]; then + echo "Dashboards disponibles:" + for f in "$EXAMPLES_DIR"/*.yaml; do + echo " $(basename "$f" .yaml)" + done + echo "" + echo "Uso: $0 (sin .yaml)" + exit 0 +fi + +YAML="$EXAMPLES_DIR/$1.yaml" +if [ ! -f "$YAML" ]; then + echo "Error: no existe $YAML" + exit 1 +fi + +cd "$APP_DIR" + +# Compilar si el binario no existe o el YAML es mas reciente +if [ ! -f "$BIN" ] || [ "$YAML" -nt "$BIN" ]; then + echo "-> compilando..." + CGO_ENABLED=1 wails build -tags fts5 +fi + +echo "-> $BIN --dashboard $YAML" +exec "$BIN" --dashboard "$YAML" diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..45fb36f --- /dev/null +++ b/wails.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://wails.io/schemas/config.v2.json", + "name": "rapid-dashboards", + "outputfilename": "rapid-dashboards", + "frontend:dir": "./frontend", + "frontend:install": "pnpm install", + "frontend:build": "pnpm run build", + "frontend:dev:watcher": "pnpm run dev", + "frontend:dev:serverUrl": "auto", + "wailsjsdir": "./frontend/src", + "author": { + "name": "Egutierrez" + } +}