From b6fec02b55a4227b6ecb47c2bc40a5a025af2b8a Mon Sep 17 00:00:00 2001 From: Shulhan Date: Thu, 31 Jul 2025 00:27:20 +0700 Subject: all: refactoring web page to render using template Instead of using HTML and JavaScript, generate the web page using template. This minimize number of works we do in the future (creating client in JavaScript and HTTP APIs). --- _www/index.tmpl | 15 +++++++++++++ cmd/lilin/main.go | 46 ++++++++++++++++++++++++++++++++++++++ lilin.go | 8 ++++++- lilin_test.go | 14 ------------ reports.go | 10 +++++++++ server.go | 67 +++++++++++++++++++++++++++++++++++++------------------ server_options.go | 4 ++++ service_report.go | 11 +++++++++ worker.go | 13 ----------- 9 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 _www/index.tmpl create mode 100644 cmd/lilin/main.go create mode 100644 reports.go create mode 100644 service_report.go diff --git a/_www/index.tmpl b/_www/index.tmpl new file mode 100644 index 0000000..250843d --- /dev/null +++ b/_www/index.tmpl @@ -0,0 +1,15 @@ + + + + + + {{.Title}} + + + {{range .Services}} +
+
{{.Name}}
+
+ {{end}} + + diff --git a/cmd/lilin/main.go b/cmd/lilin/main.go new file mode 100644 index 0000000..21c40ec --- /dev/null +++ b/cmd/lilin/main.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2025 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-only + +package main + +import ( + "context" + "log" + "os" + "os/signal" + "time" + + "git.sr.ht/~shulhan/lilin" +) + +func main() { + var serverOpts = lilin.ServerOptions{ + BaseDir: `testdata`, + IsDevelopment: true, + } + + var server *lilin.Server + var err error + server, err = lilin.NewServer(serverOpts) + if err != nil { + log.Fatal(err) + } + + go func() { + err = server.ListenAndServe() + if err != nil { + log.Fatal(err) + } + }() + + var sigq = make(chan os.Signal, 1) + signal.Notify(sigq, os.Interrupt) + <-sigq + + var ctx = context.Background() + var timeout = 5 * time.Second + var cancelf context.CancelFunc + ctx, cancelf = context.WithTimeout(ctx, timeout) + defer cancelf() + _ = server.Shutdown(ctx) +} diff --git a/lilin.go b/lilin.go index 1fa04c1..1febf86 100644 --- a/lilin.go +++ b/lilin.go @@ -3,7 +3,13 @@ package lilin -import "time" +import ( + "embed" + "time" +) // defTimeout define default timeout for service and client connection. const defTimeout = 5 * time.Second + +//go:embed _www +var wwwFS embed.FS diff --git a/lilin_test.go b/lilin_test.go index fe951a0..3e1899d 100644 --- a/lilin_test.go +++ b/lilin_test.go @@ -14,7 +14,6 @@ import ( "git.sr.ht/~shulhan/lilin" "git.sr.ht/~shulhan/lilin/internal" "git.sr.ht/~shulhan/pakakeh.go/lib/net" - "git.sr.ht/~shulhan/pakakeh.go/lib/test" ) var client *lilin.Client @@ -104,16 +103,3 @@ func dummyHTTPService() { log.Fatal(err) } } - -func TestServer_handleServicesSummary(t *testing.T) { - var gotSummary []lilin.Service - var err error - - gotSummary, err = client.ServicesSummary() - if err != nil { - t.Fatal(err) - } - - var expSummary = []lilin.Service{{}, {}, {}} - test.Assert(t, `ServicesSummary`, expSummary, gotSummary) -} diff --git a/reports.go b/reports.go new file mode 100644 index 0000000..1ffbd43 --- /dev/null +++ b/reports.go @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-only + +package lilin + +// Reports contains the report for all services. +type Reports struct { + Services map[string]ServiceReport + Title string +} diff --git a/server.go b/server.go index bd272ec..620c36f 100644 --- a/server.go +++ b/server.go @@ -4,13 +4,13 @@ package lilin import ( + "bytes" "context" - "encoding/json" "fmt" + "html/template" + "log" "net/http" "time" - - libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" ) const ( @@ -25,13 +25,21 @@ type Server struct { worker *worker Options ServerOptions + Reports Reports + + pageIndexTmpl *template.Template + pageIndexBody bytes.Buffer } +// NewServer create new server to serve the content of _www and HTTP APIs. func NewServer(opts ServerOptions) (srv *Server, err error) { var logp = `NewServer` srv = &Server{ Options: opts, + Reports: Reports{ + Services: map[string]ServiceReport{}, + }, } err = srv.Options.init() @@ -44,8 +52,23 @@ func NewServer(opts ServerOptions) (srv *Server, err error) { return nil, fmt.Errorf(`%s: %w`, logp, err) } + for name := range srv.worker.Services { + srv.Reports.Services[name] = ServiceReport{ + Name: name, + } + } + + srv.pageIndexTmpl, err = template.ParseFS(wwwFS, `_www/index.tmpl`) + if err != nil { + return nil, err + } + err = srv.pageIndexTmpl.Execute(&srv.pageIndexBody, &srv.Reports) + if err != nil { + return nil, err + } + var mux = http.NewServeMux() - mux.HandleFunc(`GET `+pathAPIServicesSummary, srv.handleServicesSummary) + mux.Handle(`GET /`, srv) srv.httpd = &http.Server{ Addr: srv.Options.Address, @@ -58,6 +81,8 @@ func NewServer(opts ServerOptions) (srv *Server, err error) { // ListenAndServe start handling request on incoming connections. func (srv *Server) ListenAndServe() (err error) { + log.Printf(`lilin: starting HTTP server at %s`, srv.Options.Address) + err = srv.httpd.ListenAndServe() if err != nil { return fmt.Errorf(`ListenAndServe: %w`, err) @@ -75,27 +100,25 @@ func (srv *Server) Shutdown(ctx context.Context) (err error) { return nil } -func (srv *Server) handleServicesSummary(resw http.ResponseWriter, req *http.Request) { - var summary = srv.worker.summary() - var resp = libhttp.EndpointResponse{ - Data: summary, - } - var respBody []byte +func (srv *Server) ServeHTTP(resw http.ResponseWriter, req *http.Request) { var err error - respBody, err = json.Marshal(resp) - if err != nil { - goto fail + switch req.URL.Path { + case `/`, `/index.html`: + _, err = resw.Write(srv.pageIndexBody.Bytes()) + if err != nil { + srv.internalError(resw, err) + } + return } - resw.WriteHeader(http.StatusOK) - resw.Write(respBody) - return -fail: + http.Redirect(resw, req, `/`, http.StatusSeeOther) +} + +func (srv *Server) internalError(resw http.ResponseWriter, err error) { resw.WriteHeader(http.StatusInternalServerError) - respBody = []byte(fmt.Sprintf(responseTemplateJSON, - http.StatusInternalServerError, - `ERR_INTERNAL`, - err.Error())) - resw.Write(respBody) + _, err = resw.Write([]byte(err.Error())) + if err != nil { + log.Println(`internalError:`, err.Error()) + } } diff --git a/server_options.go b/server_options.go index 5197ffd..4a033fc 100644 --- a/server_options.go +++ b/server_options.go @@ -24,6 +24,10 @@ type ServerOptions struct { // The address to listen for HTTP server and APIs. Address string `ini:"server::address"` + + // IsDevelopment run the server in development mode with direct access + // to file system in _www instead of using [embed.FS]. + IsDevelopment bool } // init initialize the server by reading the configuration from diff --git a/service_report.go b/service_report.go new file mode 100644 index 0000000..2627bc2 --- /dev/null +++ b/service_report.go @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-only + +package lilin + +// ServiceReport contains the scan report for single service. +type ServiceReport struct { + Name string + Last ScanReport + History []ScanReport +} diff --git a/worker.go b/worker.go index d86852d..f58363c 100644 --- a/worker.go +++ b/worker.go @@ -4,10 +4,8 @@ package lilin import ( - "maps" "os" "path/filepath" - "slices" "strings" "sync" @@ -85,14 +83,3 @@ func (wrk *worker) loadServiceDir(configDir string) (err error) { } return nil } - -// summary return all services status ordered by name. -func (wrk *worker) summary() (list []Service) { - wrk.Lock() - var keys = slices.Sorted(maps.Keys(wrk.Services)) - for _, name := range keys { - list = append(list, *wrk.Services[name]) - } - wrk.Unlock() - return list -} -- cgit v1.3