detached dfcd2ef

Version 0.02 from scratch

commit
dfcd2efb39add2618adf315934c9dd285950fff3
author
bnema · · 2023-09-11 16:44 UTC
committer
bnema · · 2023-09-11 19:05 UTC
parent
1df4e7b
signature
unsigned
69 files · +0 -4018

this diff is incomplete: 2 files are too large to show. the totals above count everything; clone the repository to read the rest.

D cmd/main.go +0 -46
@@ -1,46 +0,0 @@
1-package main
2-
3-import (
4- "os"
5-
6- "github.com/labstack/echo/v4"
7- root "gogs.bnema.dev/gordon-echo"
8- "gogs.bnema.dev/gordon-echo/internal/app"
9- "gogs.bnema.dev/gordon-echo/internal/http/routes"
10- "gogs.bnema.dev/gordon-echo/pkg/utils"
11-)
12-
13-func main() {
14- // Port = os.Getenv("PORT") or fallback to default port (1323)
15- port := os.Getenv("PORT")
16- if port == "" {
17- port = "1323"
18- }
19- // Initialize Gordon
20- gordon, err := app.NewApp(
21- &app.Config{
22- TemplateFS: root.TemplateFS,
23- PublicFS: root.PublicFS,
24- ModelFS: root.ModelFS,
25- },
26- )
27- if err != nil {
28- // Handle initialization error and exit
29- gordon.AppLogger.Error().Err(err).Msg("Failed to initialize app")
30- return
31- }
32- utils.CaptureSTDOUT(gordon.AppLogger)
33-
34- e := echo.New()
35- e.HideBanner = true
36- e.HidePort = true
37- e.Logger = utils.NewEchoLoggerWrapper(gordon.AppLogger)
38- e = routes.ConfigureRouter(e, gordon, gordon.Config)
39-
40- startFunc := func() error {
41- gordon.AppLogger.Info().Msgf("Server is listening on port %s", port)
42- return e.Start(":" + port)
43- }
44- utils.RunAppCatchSIGINT(startFunc, &gordon.AppLogger.Logger)
45-
46-}
@@ -1,46 +0,0 @@
1-package main
2-
3-import (
4- "os"
5-
6- "github.com/labstack/echo/v4"
7- root "gogs.bnema.dev/gordon-echo"
8- "gogs.bnema.dev/gordon-echo/internal/app"
9- "gogs.bnema.dev/gordon-echo/internal/http/routes"
10- "gogs.bnema.dev/gordon-echo/pkg/utils"
11-)
12-
13-func main() {
14- // Port = os.Getenv("PORT") or fallback to default port (1323)
15- port := os.Getenv("PORT")
16- if port == "" {
17- port = "1323"
18- }
19- // Initialize Gordon
20- gordon, err := app.NewApp(
21- &app.Config{
22- TemplateFS: root.TemplateFS,
23- PublicFS: root.PublicFS,
24- ModelFS: root.ModelFS,
25- },
26- )
27- if err != nil {
28- // Handle initialization error and exit
29- gordon.AppLogger.Error().Err(err).Msg("Failed to initialize app")
30- return
31- }
32- utils.CaptureSTDOUT(gordon.AppLogger)
33-
34- e := echo.New()
35- e.HideBanner = true
36- e.HidePort = true
37- e.Logger = utils.NewEchoLoggerWrapper(gordon.AppLogger)
38- e = routes.ConfigureRouter(e, gordon, gordon.Config)
39-
40- startFunc := func() error {
41- gordon.AppLogger.Info().Msgf("Server is listening on port %s", port)
42- return e.Start(":" + port)
43- }
44- utils.RunAppCatchSIGINT(startFunc, &gordon.AppLogger.Logger)
45-
46-}
D embed.go +0 -22
@@ -1,22 +0,0 @@
1-package embed
2-
3-import (
4- "embed"
5-
6- "github.com/labstack/echo/v4"
7-)
8-
9-// Embedding the public and templates directories
10-
11-//go:embed internal/ui/public/*
12-var public embed.FS
13-
14-//go:embed internal/ui/templates/*
15-var templates embed.FS
16-
17-//go:embed pkg/templating/models/*
18-var models embed.FS
19-
20-var PublicFS = echo.MustSubFS(public, "internal/ui/public")
21-var TemplateFS = echo.MustSubFS(templates, "internal/ui/templates")
22-var ModelFS = echo.MustSubFS(models, "pkg/templating/models")
@@ -1,22 +0,0 @@
1-package embed
2-
3-import (
4- "embed"
5-
6- "github.com/labstack/echo/v4"
7-)
8-
9-// Embedding the public and templates directories
10-
11-//go:embed internal/ui/public/*
12-var public embed.FS
13-
14-//go:embed internal/ui/templates/*
15-var templates embed.FS
16-
17-//go:embed pkg/templating/models/*
18-var models embed.FS
19-
20-var PublicFS = echo.MustSubFS(public, "internal/ui/public")
21-var TemplateFS = echo.MustSubFS(templates, "internal/ui/templates")
22-var ModelFS = echo.MustSubFS(models, "pkg/templating/models")
D go.mod +0 -58
@@ -1,58 +0,0 @@
1-module gogs.bnema.dev/gordon-echo
2-
3-go 1.21.0
4-
5-require (
6- github.com/PuerkitoBio/goquery v1.8.1
7- github.com/docker/docker v24.0.5+incompatible
8- github.com/labstack/echo/v4 v4.11.1
9- github.com/microcosm-cc/bluemonday v1.0.25
10- github.com/tidwall/gjson v1.16.0
11- gopkg.in/yaml.v3 v3.0.1
12-)
13-
14-require (
15- github.com/Microsoft/go-winio v0.6.1 // indirect
16- github.com/andybalholm/cascadia v1.3.1 // indirect
17- github.com/aymerick/douceur v0.2.0 // indirect
18- github.com/containerd/containerd v1.7.5 // indirect
19- github.com/docker/distribution v2.8.2+incompatible // indirect
20- github.com/docker/go-connections v0.4.0 // indirect
21- github.com/docker/go-units v0.5.0 // indirect
22- github.com/gogo/protobuf v1.3.2 // indirect
23- github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
24- github.com/gorilla/context v1.1.1 // indirect
25- github.com/gorilla/css v1.0.0 // indirect
26- github.com/gorilla/securecookie v1.1.1 // indirect
27- github.com/gorilla/sessions v1.2.1 // indirect
28- github.com/klauspost/compress v1.16.7 // indirect
29- github.com/moby/patternmatcher v0.6.0 // indirect
30- github.com/moby/sys/sequential v0.5.0 // indirect
31- github.com/moby/term v0.5.0 // indirect
32- github.com/morikuni/aec v1.0.0 // indirect
33- github.com/opencontainers/go-digest v1.0.0 // indirect
34- github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
35- github.com/opencontainers/runc v1.1.9 // indirect
36- github.com/pkg/errors v0.9.1 // indirect
37- github.com/sirupsen/logrus v1.9.0 // indirect
38- github.com/tidwall/match v1.1.1 // indirect
39- github.com/tidwall/pretty v1.2.1 // indirect
40- golang.org/x/mod v0.9.0 // indirect
41- golang.org/x/time v0.3.0 // indirect
42- golang.org/x/tools v0.7.0 // indirect
43- gotest.tools/v3 v3.5.0 // indirect
44-)
45-
46-require (
47- github.com/labstack/echo-contrib v0.15.0
48- github.com/labstack/gommon v0.4.0
49- github.com/mattn/go-colorable v0.1.13 // indirect
50- github.com/mattn/go-isatty v0.0.19 // indirect
51- github.com/rs/zerolog v1.30.0
52- github.com/valyala/bytebufferpool v1.0.0 // indirect
53- github.com/valyala/fasttemplate v1.2.2 // indirect
54- golang.org/x/crypto v0.11.0 // indirect
55- golang.org/x/net v0.12.0 // indirect
56- golang.org/x/sys v0.10.0 // indirect
57- golang.org/x/text v0.11.0 // indirect
58-)
@@ -1,58 +0,0 @@
1-module gogs.bnema.dev/gordon-echo
2-
3-go 1.21.0
4-
5-require (
6- github.com/PuerkitoBio/goquery v1.8.1
7- github.com/docker/docker v24.0.5+incompatible
8- github.com/labstack/echo/v4 v4.11.1
9- github.com/microcosm-cc/bluemonday v1.0.25
10- github.com/tidwall/gjson v1.16.0
11- gopkg.in/yaml.v3 v3.0.1
12-)
13-
14-require (
15- github.com/Microsoft/go-winio v0.6.1 // indirect
16- github.com/andybalholm/cascadia v1.3.1 // indirect
17- github.com/aymerick/douceur v0.2.0 // indirect
18- github.com/containerd/containerd v1.7.5 // indirect
19- github.com/docker/distribution v2.8.2+incompatible // indirect
20- github.com/docker/go-connections v0.4.0 // indirect
21- github.com/docker/go-units v0.5.0 // indirect
22- github.com/gogo/protobuf v1.3.2 // indirect
23- github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
24- github.com/gorilla/context v1.1.1 // indirect
25- github.com/gorilla/css v1.0.0 // indirect
26- github.com/gorilla/securecookie v1.1.1 // indirect
27- github.com/gorilla/sessions v1.2.1 // indirect
28- github.com/klauspost/compress v1.16.7 // indirect
29- github.com/moby/patternmatcher v0.6.0 // indirect
30- github.com/moby/sys/sequential v0.5.0 // indirect
31- github.com/moby/term v0.5.0 // indirect
32- github.com/morikuni/aec v1.0.0 // indirect
33- github.com/opencontainers/go-digest v1.0.0 // indirect
34- github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
35- github.com/opencontainers/runc v1.1.9 // indirect
36- github.com/pkg/errors v0.9.1 // indirect
37- github.com/sirupsen/logrus v1.9.0 // indirect
38- github.com/tidwall/match v1.1.1 // indirect
39- github.com/tidwall/pretty v1.2.1 // indirect
40- golang.org/x/mod v0.9.0 // indirect
41- golang.org/x/time v0.3.0 // indirect
42- golang.org/x/tools v0.7.0 // indirect
43- gotest.tools/v3 v3.5.0 // indirect
44-)
45-
46-require (
47- github.com/labstack/echo-contrib v0.15.0
48- github.com/labstack/gommon v0.4.0
49- github.com/mattn/go-colorable v0.1.13 // indirect
50- github.com/mattn/go-isatty v0.0.19 // indirect
51- github.com/rs/zerolog v1.30.0
52- github.com/valyala/bytebufferpool v1.0.0 // indirect
53- github.com/valyala/fasttemplate v1.2.2 // indirect
54- golang.org/x/crypto v0.11.0 // indirect
55- golang.org/x/net v0.12.0 // indirect
56- golang.org/x/sys v0.10.0 // indirect
57- golang.org/x/text v0.11.0 // indirect
58-)
D go.sum +0 -180 generated file: collapsed
D internal/app/app.go +0 -122
@@ -1,122 +0,0 @@
1-package app
2-
3-import (
4- "fmt"
5- "io"
6- "io/fs"
7- "os"
8- "path/filepath"
9-
10- "github.com/rs/zerolog"
11- "gogs.bnema.dev/gordon-echo/pkg/utils"
12-)
13-
14-const (
15- DefaultBuildDir = "tmp"
16- DefaultLogDir = "logs"
17- EnvVarName = "ENV"
18- ProdEnvValue = "prod"
19- BuildDirEnvVarName = "BUILD_DIR"
20- LogDirEnvVarName = "LOG_DIR"
21-)
22-
23-type App struct {
24- AppLogger *utils.Logger
25- HttpLogger *utils.Logger
26- Config *Config
27-}
28-type Config struct {
29- BuildDir string
30- LogDir string
31- TemplateFS fs.FS
32- PublicFS fs.FS
33- ModelFS fs.FS
34-}
35-
36-func (c *Config) GetTemplateFS() fs.FS {
37- return c.TemplateFS
38-}
39-func (c *Config) GetPublicFS() fs.FS {
40- return c.PublicFS
41-}
42-
43-func (c *Config) GetModelFS() fs.FS {
44- return c.ModelFS
45-}
46-
47-func defineEnv(config *Config) {
48- // Get the environment variable
49- env := os.Getenv(EnvVarName)
50- // If the environment variable is empty, consider it is dev -> (/tmp)
51- buildDir := os.Getenv(BuildDirEnvVarName)
52- if buildDir == "" {
53- buildDir = DefaultBuildDir
54- }
55- logDir := os.Getenv(LogDirEnvVarName)
56- if logDir == "" {
57- logDir = DefaultLogDir
58- }
59-
60- switch env {
61- case ProdEnvValue:
62- // If it is prod we want everything at the same level of the binary
63- buildDir = "."
64- logDir = "./" + logDir
65- default:
66- // If it is dev we want everything in /tmp
67- buildDir = "./" + buildDir
68- logDir = filepath.Join(buildDir, logDir)
69- }
70- config.BuildDir = buildDir
71- config.LogDir = logDir
72-}
73-
74-func NewApp(config *Config) (*App, error) {
75- defineEnv(config)
76- SetupLogging(config)
77- app := &App{
78- AppLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "app.log")),
79- HttpLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "http.log")),
80- Config: config,
81- }
82- return app, nil
83-}
84-
85-func initializeLoggerOutput(cfg *Config, logFile string) io.Writer {
86- logPath := filepath.Join(cfg.LogDir, logFile)
87- dir := filepath.Dir(logPath)
88- if _, err := os.Stat(dir); os.IsNotExist(err) {
89- if mkdirErr := os.MkdirAll(dir, 0755); mkdirErr != nil {
90- panic(mkdirErr)
91- }
92- }
93-
94- file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
95- if err != nil {
96- panic(err)
97- }
98- consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
99- return zerolog.MultiLevelWriter(file, consoleWriter)
100-}
101-func SetupLogging(config *Config) {
102- // Check if the log directory exists
103- if _, err := os.Stat(config.LogDir); os.IsNotExist(err) {
104- // If not, create it
105- err := os.MkdirAll(config.LogDir, 0755)
106- if err != nil {
107- panic(fmt.Sprintf("Failed to create log directory at path: %s, error: %s", config.LogDir, err))
108- }
109- }
110-}
111-
112-func GetBuildDir() string {
113- env := os.Getenv(EnvVarName)
114- buildDir := os.Getenv(BuildDirEnvVarName)
115- if buildDir == "" {
116- buildDir = DefaultBuildDir
117- }
118- if env == ProdEnvValue {
119- return "."
120- }
121- return "./" + buildDir
122-}
@@ -1,122 +0,0 @@
1-package app
2-
3-import (
4- "fmt"
5- "io"
6- "io/fs"
7- "os"
8- "path/filepath"
9-
10- "github.com/rs/zerolog"
11- "gogs.bnema.dev/gordon-echo/pkg/utils"
12-)
13-
14-const (
15- DefaultBuildDir = "tmp"
16- DefaultLogDir = "logs"
17- EnvVarName = "ENV"
18- ProdEnvValue = "prod"
19- BuildDirEnvVarName = "BUILD_DIR"
20- LogDirEnvVarName = "LOG_DIR"
21-)
22-
23-type App struct {
24- AppLogger *utils.Logger
25- HttpLogger *utils.Logger
26- Config *Config
27-}
28-type Config struct {
29- BuildDir string
30- LogDir string
31- TemplateFS fs.FS
32- PublicFS fs.FS
33- ModelFS fs.FS
34-}
35-
36-func (c *Config) GetTemplateFS() fs.FS {
37- return c.TemplateFS
38-}
39-func (c *Config) GetPublicFS() fs.FS {
40- return c.PublicFS
41-}
42-
43-func (c *Config) GetModelFS() fs.FS {
44- return c.ModelFS
45-}
46-
47-func defineEnv(config *Config) {
48- // Get the environment variable
49- env := os.Getenv(EnvVarName)
50- // If the environment variable is empty, consider it is dev -> (/tmp)
51- buildDir := os.Getenv(BuildDirEnvVarName)
52- if buildDir == "" {
53- buildDir = DefaultBuildDir
54- }
55- logDir := os.Getenv(LogDirEnvVarName)
56- if logDir == "" {
57- logDir = DefaultLogDir
58- }
59-
60- switch env {
61- case ProdEnvValue:
62- // If it is prod we want everything at the same level of the binary
63- buildDir = "."
64- logDir = "./" + logDir
65- default:
66- // If it is dev we want everything in /tmp
67- buildDir = "./" + buildDir
68- logDir = filepath.Join(buildDir, logDir)
69- }
70- config.BuildDir = buildDir
71- config.LogDir = logDir
72-}
73-
74-func NewApp(config *Config) (*App, error) {
75- defineEnv(config)
76- SetupLogging(config)
77- app := &App{
78- AppLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "app.log")),
79- HttpLogger: utils.NewLogger().SetOutput(initializeLoggerOutput(config, "http.log")),
80- Config: config,
81- }
82- return app, nil
83-}
84-
85-func initializeLoggerOutput(cfg *Config, logFile string) io.Writer {
86- logPath := filepath.Join(cfg.LogDir, logFile)
87- dir := filepath.Dir(logPath)
88- if _, err := os.Stat(dir); os.IsNotExist(err) {
89- if mkdirErr := os.MkdirAll(dir, 0755); mkdirErr != nil {
90- panic(mkdirErr)
91- }
92- }
93-
94- file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
95- if err != nil {
96- panic(err)
97- }
98- consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
99- return zerolog.MultiLevelWriter(file, consoleWriter)
100-}
101-func SetupLogging(config *Config) {
102- // Check if the log directory exists
103- if _, err := os.Stat(config.LogDir); os.IsNotExist(err) {
104- // If not, create it
105- err := os.MkdirAll(config.LogDir, 0755)
106- if err != nil {
107- panic(fmt.Sprintf("Failed to create log directory at path: %s, error: %s", config.LogDir, err))
108- }
109- }
110-}
111-
112-func GetBuildDir() string {
113- env := os.Getenv(EnvVarName)
114- buildDir := os.Getenv(BuildDirEnvVarName)
115- if buildDir == "" {
116- buildDir = DefaultBuildDir
117- }
118- if env == ProdEnvValue {
119- return "."
120- }
121- return "./" + buildDir
122-}
D internal/http/handlers/errors.go +0 -24
@@ -1,24 +0,0 @@
1-package handlers
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8-)
9-
10-func ErrorNumberHandler(err error, c echo.Context) {
11- code := http.StatusInternalServerError
12- if he, ok := err.(*echo.HTTPError); ok {
13- code = he.Code
14- }
15-
16- switch code {
17- case http.StatusNotFound:
18- c.String(code, fmt.Sprintf("An error occurred: %v", err))
19- case http.StatusForbidden:
20- c.String(code, fmt.Sprintf("An error occurred: %v", err))
21- default:
22- c.String(code, fmt.Sprintf("An error occurred: %v", err))
23- }
24-}
@@ -1,24 +0,0 @@
1-package handlers
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8-)
9-
10-func ErrorNumberHandler(err error, c echo.Context) {
11- code := http.StatusInternalServerError
12- if he, ok := err.(*echo.HTTPError); ok {
13- code = he.Code
14- }
15-
16- switch code {
17- case http.StatusNotFound:
18- c.String(code, fmt.Sprintf("An error occurred: %v", err))
19- case http.StatusForbidden:
20- c.String(code, fmt.Sprintf("An error occurred: %v", err))
21- default:
22- c.String(code, fmt.Sprintf("An error occurred: %v", err))
23- }
24-}
D internal/http/handlers/htmx-fragment-header.go +0 -60
@@ -1,60 +0,0 @@
1-package handlers
2-
3-import (
4- "net/http"
5-
6- "github.com/labstack/echo/v4"
7- "gogs.bnema.dev/gordon-echo/internal/app"
8- "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
9- "gogs.bnema.dev/gordon-echo/pkg/htmx"
10- "gogs.bnema.dev/gordon-echo/pkg/utils"
11-)
12-
13-func HTMXHandler(c echo.Context, a *app.App, ac *app.Config) error {
14- logger := a.AppLogger
15- // Detect and extract HTMX data from the request
16- htmxRequest, err := htmx.GetRequest(c)
17- if err != nil {
18- logger.Debug().Err(err).Msg("Failed to extract HTMX data")
19- return err
20- }
21-
22- // If the request is not an HTMX request, return an error
23- if !htmxRequest.Enabled {
24- logger.Error().Msg("Request is not an HTMX request")
25- return c.String(http.StatusBadRequest, "Not an HTMX request")
26- }
27-
28- // If it's a GET request, handle the fragment
29- if c.Request().Method == http.MethodGet {
30- return htmxFragmentHandler(c, a, ac)
31- }
32-
33- // For other HTTP methods, return an error
34- return c.String(http.StatusBadRequest, "Invalid request method")
35-}
36-
37-func htmxFragmentHandler(c echo.Context, a *app.App, ac *app.Config) error {
38- logger := a.AppLogger
39- fragment := c.Request().Header.Get("HX-Fragment")
40- if fragment == "" {
41- logger.Error().Msg("Missing fragment header")
42- return c.String(http.StatusBadRequest, "Missing fragment header")
43- }
44-
45- lang := c.Get(middlewares.LangKey).(string)
46- var data utils.StringsYamlData
47- err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data)
48- if err != nil {
49- logger.Error().Err(err).Msg("Failed to populate data from YAML")
50- return err
51- }
52-
53- content, err := htmx.GetHTMLFragmentByID(fragment, data.CurrentLang, a, ac)
54- if err != nil { // <-- This check was missing
55- logger.Error().Err(err).Msg("Failed to get fragment")
56- return c.String(http.StatusInternalServerError, err.Error())
57- }
58-
59- return c.HTML(http.StatusOK, content)
60-}
@@ -1,60 +0,0 @@
1-package handlers
2-
3-import (
4- "net/http"
5-
6- "github.com/labstack/echo/v4"
7- "gogs.bnema.dev/gordon-echo/internal/app"
8- "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
9- "gogs.bnema.dev/gordon-echo/pkg/htmx"
10- "gogs.bnema.dev/gordon-echo/pkg/utils"
11-)
12-
13-func HTMXHandler(c echo.Context, a *app.App, ac *app.Config) error {
14- logger := a.AppLogger
15- // Detect and extract HTMX data from the request
16- htmxRequest, err := htmx.GetRequest(c)
17- if err != nil {
18- logger.Debug().Err(err).Msg("Failed to extract HTMX data")
19- return err
20- }
21-
22- // If the request is not an HTMX request, return an error
23- if !htmxRequest.Enabled {
24- logger.Error().Msg("Request is not an HTMX request")
25- return c.String(http.StatusBadRequest, "Not an HTMX request")
26- }
27-
28- // If it's a GET request, handle the fragment
29- if c.Request().Method == http.MethodGet {
30- return htmxFragmentHandler(c, a, ac)
31- }
32-
33- // For other HTTP methods, return an error
34- return c.String(http.StatusBadRequest, "Invalid request method")
35-}
36-
37-func htmxFragmentHandler(c echo.Context, a *app.App, ac *app.Config) error {
38- logger := a.AppLogger
39- fragment := c.Request().Header.Get("HX-Fragment")
40- if fragment == "" {
41- logger.Error().Msg("Missing fragment header")
42- return c.String(http.StatusBadRequest, "Missing fragment header")
43- }
44-
45- lang := c.Get(middlewares.LangKey).(string)
46- var data utils.StringsYamlData
47- err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data)
48- if err != nil {
49- logger.Error().Err(err).Msg("Failed to populate data from YAML")
50- return err
51- }
52-
53- content, err := htmx.GetHTMLFragmentByID(fragment, data.CurrentLang, a, ac)
54- if err != nil { // <-- This check was missing
55- logger.Error().Err(err).Msg("Failed to get fragment")
56- return c.String(http.StatusInternalServerError, err.Error())
57- }
58-
59- return c.HTML(http.StatusOK, content)
60-}
D internal/http/handlers/htmx-installer.go +0 -75
@@ -1,75 +0,0 @@
1-package handlers
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8- "gogs.bnema.dev/gordon-echo/internal/app"
9- "gogs.bnema.dev/gordon-echo/pkg/scripts"
10-)
11-
12-var eventsChan = make(chan string)
13-
14-// TraefikInstallerHandler handles the installation of Traefik
15-func TraefikInstallerHandler(c echo.Context, a *app.App) error {
16- logger := a.AppLogger
17- eventsChan <- "Starting Traefik installation..."
18- // Retrieve the logger from the context
19- // Reject POST requests if they are not allowed with the RejectPOSTPolicy function
20- if err := RejectPOSTPolicy(c); err != nil {
21- logger.Error().Err(err).Msg("POST request rejected")
22- return err
23- }
24-
25- // Get data from the form install-traefik-form (input fields : topdomain, adminemail) and sanitize it
26- topDomain, err := SanitizeUserInput(c.FormValue("topdomain"))
27- if err != nil {
28- logger.Error().Err(err).Msg("Failed to sanitize user input")
29- return err
30- }
31- adminEmail, err := SanitizeUserInput(c.FormValue("adminemail"))
32- if err != nil {
33- logger.Error().Err(err).Msg("Failed to sanitize user input")
34- return err
35- }
36-
37- // Call the InstallTraefik function from pkg/scripts/install-traefik.go
38- if success, err := scripts.InstallTraefik(topDomain, adminEmail, a); err != nil {
39- logger.Error().Err(err).Msg("Failed to install Traefik")
40- return err
41- } else if !success {
42- logger.Error().Msg("Failed to install Traefik")
43- return err
44- } else {
45- eventsChan <- "Traefik installation completed!"
46- return c.String(200, "Traefik installed successfully")
47- }
48-}
49-
50-func TraefikInstallerSSEHandler(c echo.Context, a *app.App) error {
51- w := c.Response().Writer
52- flusher, ok := w.(http.Flusher)
53- if !ok {
54- return c.String(http.StatusInternalServerError, "Streaming unsupported!")
55- }
56-
57- c.Response().Header().Set("Content-Type", "text/event-stream")
58- c.Response().Header().Set("Cache-Control", "no-cache")
59- c.Response().Header().Set("Connection", "keep-alive")
60-
61- for {
62- // Listen for a new message to be sent to the client
63- msg, open := <-eventsChan
64- if !open {
65- fmt.Println("Connection closed")
66- break
67- }
68- _, err := fmt.Fprintf(w, "data: %s\n\n", msg)
69- if err != nil {
70- return err
71- }
72- flusher.Flush()
73- }
74- return nil
75-}
@@ -1,75 +0,0 @@
1-package handlers
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8- "gogs.bnema.dev/gordon-echo/internal/app"
9- "gogs.bnema.dev/gordon-echo/pkg/scripts"
10-)
11-
12-var eventsChan = make(chan string)
13-
14-// TraefikInstallerHandler handles the installation of Traefik
15-func TraefikInstallerHandler(c echo.Context, a *app.App) error {
16- logger := a.AppLogger
17- eventsChan <- "Starting Traefik installation..."
18- // Retrieve the logger from the context
19- // Reject POST requests if they are not allowed with the RejectPOSTPolicy function
20- if err := RejectPOSTPolicy(c); err != nil {
21- logger.Error().Err(err).Msg("POST request rejected")
22- return err
23- }
24-
25- // Get data from the form install-traefik-form (input fields : topdomain, adminemail) and sanitize it
26- topDomain, err := SanitizeUserInput(c.FormValue("topdomain"))
27- if err != nil {
28- logger.Error().Err(err).Msg("Failed to sanitize user input")
29- return err
30- }
31- adminEmail, err := SanitizeUserInput(c.FormValue("adminemail"))
32- if err != nil {
33- logger.Error().Err(err).Msg("Failed to sanitize user input")
34- return err
35- }
36-
37- // Call the InstallTraefik function from pkg/scripts/install-traefik.go
38- if success, err := scripts.InstallTraefik(topDomain, adminEmail, a); err != nil {
39- logger.Error().Err(err).Msg("Failed to install Traefik")
40- return err
41- } else if !success {
42- logger.Error().Msg("Failed to install Traefik")
43- return err
44- } else {
45- eventsChan <- "Traefik installation completed!"
46- return c.String(200, "Traefik installed successfully")
47- }
48-}
49-
50-func TraefikInstallerSSEHandler(c echo.Context, a *app.App) error {
51- w := c.Response().Writer
52- flusher, ok := w.(http.Flusher)
53- if !ok {
54- return c.String(http.StatusInternalServerError, "Streaming unsupported!")
55- }
56-
57- c.Response().Header().Set("Content-Type", "text/event-stream")
58- c.Response().Header().Set("Cache-Control", "no-cache")
59- c.Response().Header().Set("Connection", "keep-alive")
60-
61- for {
62- // Listen for a new message to be sent to the client
63- msg, open := <-eventsChan
64- if !open {
65- fmt.Println("Connection closed")
66- break
67- }
68- _, err := fmt.Fprintf(w, "data: %s\n\n", msg)
69- if err != nil {
70- return err
71- }
72- flusher.Flush()
73- }
74- return nil
75-}
D internal/http/handlers/post.go +0 -38
@@ -1,38 +0,0 @@
1-package handlers
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8- "gogs.bnema.dev/gordon-echo/pkg/templating"
9-)
10-
11-func RejectPOSTPolicy(c echo.Context) error {
12- // Maximum POST size in bytes
13- const maxPOSTSize = 1024 * 1024 // = 1 MB
14- // Check the request method
15- if c.Request().Method != http.MethodPost {
16- return fmt.Errorf("invalid request method")
17- }
18- // Check for unusually large POST bodies, which might indicate an attempt at a DoS attack
19- if c.Request().ContentLength > maxPOSTSize {
20- return fmt.Errorf("request too large: %d bytes", c.Request().ContentLength)
21- }
22-
23- // Check for other suspicious behaviors, e.g., missing user agent, too many request headers, etc.
24- if c.Request().UserAgent() == "" {
25- return fmt.Errorf("missing user agent")
26- }
27-
28- // If all checks pass
29- return nil
30-}
31-
32-func SanitizeUserInput(input string) (string, error) {
33- sanitized, err := templating.SanitizeHTML(input)
34- if err != nil {
35- return "", fmt.Errorf("Failed to sanitize user input: %w", err)
36- }
37- return sanitized, nil
38-}
@@ -1,38 +0,0 @@
1-package handlers
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8- "gogs.bnema.dev/gordon-echo/pkg/templating"
9-)
10-
11-func RejectPOSTPolicy(c echo.Context) error {
12- // Maximum POST size in bytes
13- const maxPOSTSize = 1024 * 1024 // = 1 MB
14- // Check the request method
15- if c.Request().Method != http.MethodPost {
16- return fmt.Errorf("invalid request method")
17- }
18- // Check for unusually large POST bodies, which might indicate an attempt at a DoS attack
19- if c.Request().ContentLength > maxPOSTSize {
20- return fmt.Errorf("request too large: %d bytes", c.Request().ContentLength)
21- }
22-
23- // Check for other suspicious behaviors, e.g., missing user agent, too many request headers, etc.
24- if c.Request().UserAgent() == "" {
25- return fmt.Errorf("missing user agent")
26- }
27-
28- // If all checks pass
29- return nil
30-}
31-
32-func SanitizeUserInput(input string) (string, error) {
33- sanitized, err := templating.SanitizeHTML(input)
34- if err != nil {
35- return "", fmt.Errorf("Failed to sanitize user input: %w", err)
36- }
37- return sanitized, nil
38-}
D internal/http/middlewares/detect-color-scheme.go +0 -38
@@ -1,38 +0,0 @@
1-package middlewares
2-
3-import (
4- "github.com/labstack/echo-contrib/session"
5- "github.com/labstack/echo/v4"
6-)
7-
8-// ColorSchemeKey is a key for setting and getting the color scheme from the context
9-const ColorSchemeKey = "CurrentColorScheme"
10-
11-// ColorSchemeDetection detects the current color scheme and sets it in the context
12-func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc {
13- return func(c echo.Context) error {
14- colorScheme := detectCurrentColorScheme(c)
15- c.Set(ColorSchemeKey, colorScheme)
16- return next(c)
17- }
18-}
19-
20-func detectCurrentColorScheme(c echo.Context) string {
21- // Check if the color scheme key is set in the session storage
22- sess, err := session.Get("session", c)
23- if err != nil {
24- // If session storage is not available, proceed with "Sec-CH-Prefers-Color-Scheme" header detection
25- return "light"
26- }
27-
28- if colorSchemeValue, ok := sess.Values["colorScheme"]; ok && colorSchemeValue != nil {
29- return colorSchemeValue.(string) // Return the color scheme value from the session storage
30- }
31-
32- // If color scheme key is not set, proceed with "Sec-CH-Prefers-Color-Scheme" header detection
33- header := c.Request().Header.Get("Sec-CH-Prefers-Color-Scheme")
34- if header == "dark" {
35- return "dark"
36- }
37- return "light" // Default to light mode
38-}
@@ -1,38 +0,0 @@
1-package middlewares
2-
3-import (
4- "github.com/labstack/echo-contrib/session"
5- "github.com/labstack/echo/v4"
6-)
7-
8-// ColorSchemeKey is a key for setting and getting the color scheme from the context
9-const ColorSchemeKey = "CurrentColorScheme"
10-
11-// ColorSchemeDetection detects the current color scheme and sets it in the context
12-func ColorSchemeDetection(next echo.HandlerFunc) echo.HandlerFunc {
13- return func(c echo.Context) error {
14- colorScheme := detectCurrentColorScheme(c)
15- c.Set(ColorSchemeKey, colorScheme)
16- return next(c)
17- }
18-}
19-
20-func detectCurrentColorScheme(c echo.Context) string {
21- // Check if the color scheme key is set in the session storage
22- sess, err := session.Get("session", c)
23- if err != nil {
24- // If session storage is not available, proceed with "Sec-CH-Prefers-Color-Scheme" header detection
25- return "light"
26- }
27-
28- if colorSchemeValue, ok := sess.Values["colorScheme"]; ok && colorSchemeValue != nil {
29- return colorSchemeValue.(string) // Return the color scheme value from the session storage
30- }
31-
32- // If color scheme key is not set, proceed with "Sec-CH-Prefers-Color-Scheme" header detection
33- header := c.Request().Header.Get("Sec-CH-Prefers-Color-Scheme")
34- if header == "dark" {
35- return "dark"
36- }
37- return "light" // Default to light mode
38-}
D internal/http/middlewares/detect-lang.go +0 -40
@@ -1,40 +0,0 @@
1-package middlewares
2-
3-import (
4- "strings"
5-
6- "github.com/labstack/echo-contrib/session"
7- "github.com/labstack/echo/v4"
8-)
9-
10-// LangKey is a key for setting and getting the language from the context
11-const LangKey = "CurrentLang"
12-
13-// LanguageDetectiondetects the current language and sets it in the context
14-func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc {
15- return func(c echo.Context) error {
16- lang := detectCurrentLanguage(c)
17- c.Set(LangKey, lang)
18- return next(c)
19- }
20-}
21-
22-func detectCurrentLanguage(c echo.Context) string {
23- // Check if the lang key is set in the session storage
24- sess, err := session.Get("session", c)
25- if err != nil {
26- // If session storage is not available, proceed with "Accept-Language" header detection
27- return "en"
28- }
29-
30- if langValue, ok := sess.Values["lang"]; ok && langValue != nil {
31- return langValue.(string) // Return the lang value from the session storage
32- }
33-
34- // If lang key is not set, proceed with "Accept-Language" header detection
35- header := c.Request().Header.Get("Accept-Language")
36- if strings.Contains(header, "fr") {
37- return "fr"
38- }
39- return "en" // Default to English
40-}
@@ -1,40 +0,0 @@
1-package middlewares
2-
3-import (
4- "strings"
5-
6- "github.com/labstack/echo-contrib/session"
7- "github.com/labstack/echo/v4"
8-)
9-
10-// LangKey is a key for setting and getting the language from the context
11-const LangKey = "CurrentLang"
12-
13-// LanguageDetectiondetects the current language and sets it in the context
14-func LanguageDetection(next echo.HandlerFunc) echo.HandlerFunc {
15- return func(c echo.Context) error {
16- lang := detectCurrentLanguage(c)
17- c.Set(LangKey, lang)
18- return next(c)
19- }
20-}
21-
22-func detectCurrentLanguage(c echo.Context) string {
23- // Check if the lang key is set in the session storage
24- sess, err := session.Get("session", c)
25- if err != nil {
26- // If session storage is not available, proceed with "Accept-Language" header detection
27- return "en"
28- }
29-
30- if langValue, ok := sess.Values["lang"]; ok && langValue != nil {
31- return langValue.(string) // Return the lang value from the session storage
32- }
33-
34- // If lang key is not set, proceed with "Accept-Language" header detection
35- header := c.Request().Header.Get("Accept-Language")
36- if strings.Contains(header, "fr") {
37- return "fr"
38- }
39- return "en" // Default to English
40-}
D internal/http/middlewares/logger.go +0 -40
@@ -1,40 +0,0 @@
1-package middlewares
2-
3-import (
4- "strings"
5-
6- "github.com/labstack/echo/v4"
7- "github.com/labstack/echo/v4/middleware"
8- "gogs.bnema.dev/gordon-echo/pkg/utils"
9-)
10-
11-// HTTPAccessLogger creates a new middleware to log HTTP requests using the provided HttpLogger.
12-func HTTPAccessLogger(logger *utils.Logger) echo.MiddlewareFunc {
13- return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
14- LogURI: true,
15- LogStatus: true,
16- LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
17- // Filter out any requests that contain the substring "sse" in the URI
18- if strings.Contains(v.URI, "sse") {
19- return nil
20- }
21- logger.Info().
22- Str("type", "http").
23- Str("remote_ip", c.RealIP()).
24- Str("method", c.Request().Method).
25- Str("URI", v.URI).
26- Int("status", v.Status).
27- Msg("request")
28- return nil
29- },
30- })
31-}
32-
33-func SetLogger(logger *utils.Logger) echo.MiddlewareFunc {
34- return func(next echo.HandlerFunc) echo.HandlerFunc {
35- return func(c echo.Context) error {
36- c.Set("logger", logger)
37- return next(c)
38- }
39- }
40-}
@@ -1,40 +0,0 @@
1-package middlewares
2-
3-import (
4- "strings"
5-
6- "github.com/labstack/echo/v4"
7- "github.com/labstack/echo/v4/middleware"
8- "gogs.bnema.dev/gordon-echo/pkg/utils"
9-)
10-
11-// HTTPAccessLogger creates a new middleware to log HTTP requests using the provided HttpLogger.
12-func HTTPAccessLogger(logger *utils.Logger) echo.MiddlewareFunc {
13- return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
14- LogURI: true,
15- LogStatus: true,
16- LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
17- // Filter out any requests that contain the substring "sse" in the URI
18- if strings.Contains(v.URI, "sse") {
19- return nil
20- }
21- logger.Info().
22- Str("type", "http").
23- Str("remote_ip", c.RealIP()).
24- Str("method", c.Request().Method).
25- Str("URI", v.URI).
26- Int("status", v.Status).
27- Msg("request")
28- return nil
29- },
30- })
31-}
32-
33-func SetLogger(logger *utils.Logger) echo.MiddlewareFunc {
34- return func(next echo.HandlerFunc) echo.HandlerFunc {
35- return func(c echo.Context) error {
36- c.Set("logger", logger)
37- return next(c)
38- }
39- }
40-}
D internal/http/middlewares/secure.go +0 -18
@@ -1,18 +0,0 @@
1-package middlewares
2-
3-import (
4- "github.com/labstack/echo/v4"
5- "github.com/labstack/echo/v4/middleware"
6-)
7-
8-func SecureRoutes() echo.MiddlewareFunc {
9- return middleware.SecureWithConfig(middleware.SecureConfig{
10- XSSProtection: "1; mode=block",
11- ContentTypeNosniff: "nosniff",
12- XFrameOptions: "SAMEORIGIN",
13- HSTSMaxAge: 3600,
14- HSTSExcludeSubdomains: false,
15- ContentSecurityPolicy: "default-src 'self'",
16- // Skipper: YourCustomSkipperFunction, // If you have a custom skipper
17- })
18-}
@@ -1,18 +0,0 @@
1-package middlewares
2-
3-import (
4- "github.com/labstack/echo/v4"
5- "github.com/labstack/echo/v4/middleware"
6-)
7-
8-func SecureRoutes() echo.MiddlewareFunc {
9- return middleware.SecureWithConfig(middleware.SecureConfig{
10- XSSProtection: "1; mode=block",
11- ContentTypeNosniff: "nosniff",
12- XFrameOptions: "SAMEORIGIN",
13- HSTSMaxAge: 3600,
14- HSTSExcludeSubdomains: false,
15- ContentSecurityPolicy: "default-src 'self'",
16- // Skipper: YourCustomSkipperFunction, // If you have a custom skipper
17- })
18-}
D internal/http/routes/admin-routes.go +0 -76
@@ -1,76 +0,0 @@
1-package routes
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8- "gogs.bnema.dev/gordon-echo/internal/app"
9- "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
10- "gogs.bnema.dev/gordon-echo/pkg/templating"
11- "gogs.bnema.dev/gordon-echo/pkg/utils"
12-)
13-
14-func AdminRoute(c echo.Context, ac *app.Config, a *app.App) error {
15- // Check if it's the first launch of the app
16- if utils.IsConfigFilePresent() {
17- fmt.Println("Fresh installation detected, redirecting to /admin/install")
18- return c.Redirect(http.StatusFound, "/admin/install")
19- }
20-
21- // Retrieve the current language from the context
22- lang := c.Get(middlewares.LangKey).(string)
23-
24- // Assuming the filename is "strings.yml"
25- var data utils.StringsYamlData
26- err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data)
27- if err != nil {
28- return err
29- }
30-
31- renderer, err := templating.GetHTMLRenderer("index.gohtml", ac.GetPublicFS(), a)
32- if err != nil {
33- return err
34- }
35-
36- html, err := renderer.Render(data.CurrentLang, a)
37- if err != nil {
38- // Return the error into NewLogger() to log it
39- utils.NewLogger().Error()
40- return err
41- }
42-
43- return c.HTML(http.StatusOK, html)
44-}
45-
46-func InstallRoute(c echo.Context, ac *app.Config, a *app.App) error {
47- // Check if it's the first launch of the app
48- if !utils.IsConfigFilePresent() {
49- fmt.Println("Config file already present, redirecting to /admin")
50- return c.Redirect(http.StatusFound, "/admin")
51- }
52-
53- // Retrieve the current language from the context
54- lang := c.Get(middlewares.LangKey).(string)
55-
56- var data utils.StringsYamlData
57- err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data)
58- if err != nil {
59- return fmt.Errorf("failed to populate data from YAML: %w", err)
60- }
61-
62- renderer, err := templating.GetHTMLRenderer("install.gohtml", ac.GetTemplateFS(), a)
63- fmt.Println(renderer)
64- if err != nil {
65- return err
66- }
67-
68- html, err := renderer.Render(data.CurrentLang, a)
69- if err != nil {
70- // Return the error into NewLogger() to log it
71- utils.NewLogger().Error()
72- return err
73- }
74-
75- return c.HTML(http.StatusOK, html)
76-}
@@ -1,76 +0,0 @@
1-package routes
2-
3-import (
4- "fmt"
5- "net/http"
6-
7- "github.com/labstack/echo/v4"
8- "gogs.bnema.dev/gordon-echo/internal/app"
9- "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
10- "gogs.bnema.dev/gordon-echo/pkg/templating"
11- "gogs.bnema.dev/gordon-echo/pkg/utils"
12-)
13-
14-func AdminRoute(c echo.Context, ac *app.Config, a *app.App) error {
15- // Check if it's the first launch of the app
16- if utils.IsConfigFilePresent() {
17- fmt.Println("Fresh installation detected, redirecting to /admin/install")
18- return c.Redirect(http.StatusFound, "/admin/install")
19- }
20-
21- // Retrieve the current language from the context
22- lang := c.Get(middlewares.LangKey).(string)
23-
24- // Assuming the filename is "strings.yml"
25- var data utils.StringsYamlData
26- err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data)
27- if err != nil {
28- return err
29- }
30-
31- renderer, err := templating.GetHTMLRenderer("index.gohtml", ac.GetPublicFS(), a)
32- if err != nil {
33- return err
34- }
35-
36- html, err := renderer.Render(data.CurrentLang, a)
37- if err != nil {
38- // Return the error into NewLogger() to log it
39- utils.NewLogger().Error()
40- return err
41- }
42-
43- return c.HTML(http.StatusOK, html)
44-}
45-
46-func InstallRoute(c echo.Context, ac *app.Config, a *app.App) error {
47- // Check if it's the first launch of the app
48- if !utils.IsConfigFilePresent() {
49- fmt.Println("Config file already present, redirecting to /admin")
50- return c.Redirect(http.StatusFound, "/admin")
51- }
52-
53- // Retrieve the current language from the context
54- lang := c.Get(middlewares.LangKey).(string)
55-
56- var data utils.StringsYamlData
57- err := utils.PopulateDataFromYAML(lang, ac.GetTemplateFS(), "strings.yml", &data)
58- if err != nil {
59- return fmt.Errorf("failed to populate data from YAML: %w", err)
60- }
61-
62- renderer, err := templating.GetHTMLRenderer("install.gohtml", ac.GetTemplateFS(), a)
63- fmt.Println(renderer)
64- if err != nil {
65- return err
66- }
67-
68- html, err := renderer.Render(data.CurrentLang, a)
69- if err != nil {
70- // Return the error into NewLogger() to log it
71- utils.NewLogger().Error()
72- return err
73- }
74-
75- return c.HTML(http.StatusOK, html)
76-}
D internal/http/routes/router.go +0 -66
@@ -1,66 +0,0 @@
1-package routes
2-
3-import (
4- "github.com/labstack/echo/v4"
5- "gogs.bnema.dev/gordon-echo/internal/app"
6- "gogs.bnema.dev/gordon-echo/internal/http/handlers"
7- "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
8-)
9-
10-func ConfigureRouter(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo {
11-
12- // SetLogger is a middleware that sets the logger in the context
13- e.Use(middlewares.SetLogger(a.HttpLogger))
14- // HTTPAccessLogger is a middleware that logs HTTP requests
15- e.Use(middlewares.HTTPAccessLogger(a.HttpLogger))
16- e.Use(middlewares.LanguageDetection)
17- e.Use(middlewares.ColorSchemeDetection)
18- //e.Use(middlewares.SecureMiddleware()) // Need testing
19-
20- // Register routes
21- e = bindAdminRoutes(e, a, ac)
22- e = bindHtmxRoutes(e, a, ac)
23- e = bindStaticRoutes(e, a.Config)
24- e.HTTPErrorHandler = handlers.ErrorNumberHandler
25- return e
26-}
27-func bindAdminRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo {
28- // We pass the embed config to the routes so we can parse the strings.yml file
29- e.GET("/admin", func(c echo.Context) error {
30- return AdminRoute(c, ac, a)
31- })
32- e.GET("/admin/install", func(c echo.Context) error {
33- return InstallRoute(c, ac, a)
34- })
35-
36- e.POST("/admin/install/traefik", func(c echo.Context) error {
37- return handlers.TraefikInstallerHandler(c, a)
38- })
39- e.GET("/admin/install/traefik/sse", func(c echo.Context) error {
40- return handlers.TraefikInstallerSSEHandler(c, a)
41- })
42- return e
43-}
44-
45-func bindHtmxRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo {
46- e.GET("/htmx", func(c echo.Context) error {
47- return handlers.HTMXHandler(c, a, ac)
48- })
49- e.GET("/htmx/fragment", func(c echo.Context) error {
50- return handlers.HTMXHandler(c, a, ac)
51- })
52- e.POST("/htmx", func(c echo.Context) error {
53- return handlers.HTMXHandler(c, a, ac)
54- })
55- e.POST("/htmx/fragment", func(c echo.Context) error {
56- return handlers.HTMXHandler(c, a, ac)
57- })
58- return e
59-}
60-
61-func bindStaticRoutes(e *echo.Echo, config *app.Config) *echo.Echo {
62- e.GET("/*", func(c echo.Context) error {
63- return StaticRoute(c, config)
64- })
65- return e
66-}
@@ -1,66 +0,0 @@
1-package routes
2-
3-import (
4- "github.com/labstack/echo/v4"
5- "gogs.bnema.dev/gordon-echo/internal/app"
6- "gogs.bnema.dev/gordon-echo/internal/http/handlers"
7- "gogs.bnema.dev/gordon-echo/internal/http/middlewares"
8-)
9-
10-func ConfigureRouter(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo {
11-
12- // SetLogger is a middleware that sets the logger in the context
13- e.Use(middlewares.SetLogger(a.HttpLogger))
14- // HTTPAccessLogger is a middleware that logs HTTP requests
15- e.Use(middlewares.HTTPAccessLogger(a.HttpLogger))
16- e.Use(middlewares.LanguageDetection)
17- e.Use(middlewares.ColorSchemeDetection)
18- //e.Use(middlewares.SecureMiddleware()) // Need testing
19-
20- // Register routes
21- e = bindAdminRoutes(e, a, ac)
22- e = bindHtmxRoutes(e, a, ac)
23- e = bindStaticRoutes(e, a.Config)
24- e.HTTPErrorHandler = handlers.ErrorNumberHandler
25- return e
26-}
27-func bindAdminRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo {
28- // We pass the embed config to the routes so we can parse the strings.yml file
29- e.GET("/admin", func(c echo.Context) error {
30- return AdminRoute(c, ac, a)
31- })
32- e.GET("/admin/install", func(c echo.Context) error {
33- return InstallRoute(c, ac, a)
34- })
35-
36- e.POST("/admin/install/traefik", func(c echo.Context) error {
37- return handlers.TraefikInstallerHandler(c, a)
38- })
39- e.GET("/admin/install/traefik/sse", func(c echo.Context) error {
40- return handlers.TraefikInstallerSSEHandler(c, a)
41- })
42- return e
43-}
44-
45-func bindHtmxRoutes(e *echo.Echo, a *app.App, ac *app.Config) *echo.Echo {
46- e.GET("/htmx", func(c echo.Context) error {
47- return handlers.HTMXHandler(c, a, ac)
48- })
49- e.GET("/htmx/fragment", func(c echo.Context) error {
50- return handlers.HTMXHandler(c, a, ac)
51- })
52- e.POST("/htmx", func(c echo.Context) error {
53- return handlers.HTMXHandler(c, a, ac)
54- })
55- e.POST("/htmx/fragment", func(c echo.Context) error {
56- return handlers.HTMXHandler(c, a, ac)
57- })
58- return e
59-}
60-
61-func bindStaticRoutes(e *echo.Echo, config *app.Config) *echo.Echo {
62- e.GET("/*", func(c echo.Context) error {
63- return StaticRoute(c, config)
64- })
65- return e
66-}
D internal/http/routes/static-routes.go +0 -25
@@ -1,25 +0,0 @@
1-package routes
2-
3-import (
4- "io/fs"
5- "net/http"
6- "os"
7-
8- "github.com/labstack/echo/v4"
9- "gogs.bnema.dev/gordon-echo/internal/app"
10-)
11-
12-func StaticRoute(c echo.Context, config *app.Config) error {
13-
14- // Set the cache-control header based on PROD environment variable
15- if os.Getenv("PROD") == "true" {
16- c.Response().Header().Set("Cache-Control", "public, max-age=86400")
17- } else {
18- c.Response().Header().Set("Cache-Control", "no-cache")
19- }
20-
21- // Serve the file from the embedded filesystem
22- httpFs := http.FileServer(http.FS(fs.FS(config.GetPublicFS())))
23- httpFs.ServeHTTP(c.Response(), c.Request())
24- return nil
25-}
@@ -1,25 +0,0 @@
1-package routes
2-
3-import (
4- "io/fs"
5- "net/http"
6- "os"
7-
8- "github.com/labstack/echo/v4"
9- "gogs.bnema.dev/gordon-echo/internal/app"
10-)
11-
12-func StaticRoute(c echo.Context, config *app.Config) error {
13-
14- // Set the cache-control header based on PROD environment variable
15- if os.Getenv("PROD") == "true" {
16- c.Response().Header().Set("Cache-Control", "public, max-age=86400")
17- } else {
18- c.Response().Header().Set("Cache-Control", "no-cache")
19- }
20-
21- // Serve the file from the embedded filesystem
22- httpFs := http.FileServer(http.FS(fs.FS(config.GetPublicFS())))
23- httpFs.ServeHTTP(c.Response(), c.Request())
24- return nil
25-}
D internal/ui/public/404.gohtml +0 -12
@@ -1,12 +0,0 @@
1-<!DOCTYPE html>
2-<html lang="en">
3- <head>
4- <meta charset="UTF-8" />
5- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6- <title>Error 404</title>
7- </head>
8- <body>
9- <h1>Error 404</h1>
10- <p>Page not found</p>
11- </body>
12-</html>
@@ -1,12 +0,0 @@
1-<!DOCTYPE html>
2-<html lang="en">
3- <head>
4- <meta charset="UTF-8" />
5- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6- <title>Error 404</title>
7- </head>
8- <body>
9- <h1>Error 404</h1>
10- <p>Page not found</p>
11- </body>
12-</html>
D internal/ui/public/500.gohtml +0 -12
@@ -1,12 +0,0 @@
1-<!DOCTYPE html>
2-<html lang="en">
3- <head>
4- <meta charset="UTF-8" />
5- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6- <title>Error 500</title>
7- </head>
8- <body>
9- <h1>500 Internal Server Error</h1>
10- <p>Something went really wrong o/</p>
11- </body>
12-</html>
@@ -1,12 +0,0 @@
1-<!DOCTYPE html>
2-<html lang="en">
3- <head>
4- <meta charset="UTF-8" />
5- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6- <title>Error 500</title>
7- </head>
8- <body>
9- <h1>500 Internal Server Error</h1>
10- <p>Something went really wrong o/</p>
11- </body>
12-</html>
D internal/ui/public/android-chrome-192x192.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/android-chrome-512x512.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/apple-touch-icon.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/css/custom.css +0 -116
@@ -1,116 +0,0 @@
1-/**
2- * Path: internal/ui/public/assets/css/custom.css
3- *
4- * @format
5- */
6-
7-/* Embeded path : assets/css/cutsom.css */
8-
9-@tailwind base;
10-@tailwind components;
11-@tailwind utilities;
12-
13-/* Import IBM Plex Fonts */
14-@font-face {
15- font-family: "IBMPlexSans-Bold";
16- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2");
17- font-weight: bold;
18- font-style: normal;
19-}
20-
21-@font-face {
22- font-family: "IBMPlexSans-Regular";
23- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2");
24- font-weight: normal;
25- font-style: normal;
26-}
27-
28-/* Import Jetbrains Mono Nerd Font */
29-@font-face {
30- font-family: "JetBrainsMono-Nerd-Font-Regular";
31- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2")
32- format("woff2");
33- font-weight: normal;
34- font-style: normal;
35-}
36-
37-@font-face {
38- font-family: "LilitaOne-Regular";
39- src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype");
40- font-weight: normal;
41- font-style: normal;
42-}
43-
44-.fade-me-out.htmx-swapping {
45- opacity: 0;
46- transition: opacity 1s ease-out;
47-}
48-
49-/* View transition */
50-@keyframes fade-in {
51- from {
52- opacity: 0;
53- }
54-}
55-
56-@keyframes fade-out {
57- to {
58- opacity: 0;
59- }
60-}
61-
62-@keyframes slide-from-right {
63- from {
64- transform: translateX(90px);
65- }
66-}
67-
68-@keyframes slide-to-left {
69- to {
70- transform: translateX(-90px);
71- }
72-}
73-
74-.slide-it {
75- view-transition-name: slide-it;
76-}
77-
78-::view-transition-old(slide-it) {
79- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
80- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
81-}
82-::view-transition-new(slide-it) {
83- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
84- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
85-}
86-
87-.gordon-title {
88- font-family: "LilitaOne-Regular", sans-serif;
89-
90-}
91-
92-.IBMPlexSans {
93- font-family: "IBMPlexSans-Regular", sans-serif;
94-}
95-
96-.circle-shadow {
97- width: 100%;
98- position: relative;
99-}
100-
101-.circle-shadow::after {
102- position: absolute;
103- width: 50%;
104- height: 0.4rem;
105- margin: auto;
106- background: rgba(0, 0, 0, 0.1);
107- border-radius: 50%;
108- z-index: -1;
109- bottom: 2%;
110- content: "";
111- box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1);
112-}
113-
114-.iconoir {
115- margin-right: 8px;
116-}
@@ -1,116 +0,0 @@
1-/**
2- * Path: internal/ui/public/assets/css/custom.css
3- *
4- * @format
5- */
6-
7-/* Embeded path : assets/css/cutsom.css */
8-
9-@tailwind base;
10-@tailwind components;
11-@tailwind utilities;
12-
13-/* Import IBM Plex Fonts */
14-@font-face {
15- font-family: "IBMPlexSans-Bold";
16- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2");
17- font-weight: bold;
18- font-style: normal;
19-}
20-
21-@font-face {
22- font-family: "IBMPlexSans-Regular";
23- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2");
24- font-weight: normal;
25- font-style: normal;
26-}
27-
28-/* Import Jetbrains Mono Nerd Font */
29-@font-face {
30- font-family: "JetBrainsMono-Nerd-Font-Regular";
31- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2")
32- format("woff2");
33- font-weight: normal;
34- font-style: normal;
35-}
36-
37-@font-face {
38- font-family: "LilitaOne-Regular";
39- src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype");
40- font-weight: normal;
41- font-style: normal;
42-}
43-
44-.fade-me-out.htmx-swapping {
45- opacity: 0;
46- transition: opacity 1s ease-out;
47-}
48-
49-/* View transition */
50-@keyframes fade-in {
51- from {
52- opacity: 0;
53- }
54-}
55-
56-@keyframes fade-out {
57- to {
58- opacity: 0;
59- }
60-}
61-
62-@keyframes slide-from-right {
63- from {
64- transform: translateX(90px);
65- }
66-}
67-
68-@keyframes slide-to-left {
69- to {
70- transform: translateX(-90px);
71- }
72-}
73-
74-.slide-it {
75- view-transition-name: slide-it;
76-}
77-
78-::view-transition-old(slide-it) {
79- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
80- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
81-}
82-::view-transition-new(slide-it) {
83- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
84- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
85-}
86-
87-.gordon-title {
88- font-family: "LilitaOne-Regular", sans-serif;
89-
90-}
91-
92-.IBMPlexSans {
93- font-family: "IBMPlexSans-Regular", sans-serif;
94-}
95-
96-.circle-shadow {
97- width: 100%;
98- position: relative;
99-}
100-
101-.circle-shadow::after {
102- position: absolute;
103- width: 50%;
104- height: 0.4rem;
105- margin: auto;
106- background: rgba(0, 0, 0, 0.1);
107- border-radius: 50%;
108- z-index: -1;
109- bottom: 2%;
110- content: "";
111- box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1);
112-}
113-
114-.iconoir {
115- margin-right: 8px;
116-}
D internal/ui/public/assets/css/tailwind.css +0 -1046
@@ -1,1046 +0,0 @@
1-/**
2- * Path: internal/ui/public/assets/css/custom.css
3- *
4- * @format
5- */
6-
7-/* Embeded path : assets/css/cutsom.css */
8-
9-/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
10- */
11-
12-/*
13-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
14-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
15-*/
16-
17-*,
18-::before,
19-::after {
20- box-sizing: border-box;
21- /* 1 */
22- border-width: 0;
23- /* 2 */
24- border-style: solid;
25- /* 2 */
26- border-color: #e5e7eb;
27- /* 2 */
28-}
29-
30-::before,
31-::after {
32- --tw-content: '';
33-}
34-
35-/*
36-1. Use a consistent sensible line-height in all browsers.
37-2. Prevent adjustments of font size after orientation changes in iOS.
38-3. Use a more readable tab size.
39-4. Use the user's configured `sans` font-family by default.
40-5. Use the user's configured `sans` font-feature-settings by default.
41-6. Use the user's configured `sans` font-variation-settings by default.
42-*/
43-
44-html {
45- line-height: 1.5;
46- /* 1 */
47- -webkit-text-size-adjust: 100%;
48- /* 2 */
49- -moz-tab-size: 4;
50- /* 3 */
51- -o-tab-size: 4;
52- tab-size: 4;
53- /* 3 */
54- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
55- /* 4 */
56- font-feature-settings: normal;
57- /* 5 */
58- font-variation-settings: normal;
59- /* 6 */
60-}
61-
62-/*
63-1. Remove the margin in all browsers.
64-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
65-*/
66-
67-body {
68- margin: 0;
69- /* 1 */
70- line-height: inherit;
71- /* 2 */
72-}
73-
74-/*
75-1. Add the correct height in Firefox.
76-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
77-3. Ensure horizontal rules are visible by default.
78-*/
79-
80-hr {
81- height: 0;
82- /* 1 */
83- color: inherit;
84- /* 2 */
85- border-top-width: 1px;
86- /* 3 */
87-}
88-
89-/*
90-Add the correct text decoration in Chrome, Edge, and Safari.
91-*/
92-
93-abbr:where([title]) {
94- -webkit-text-decoration: underline dotted;
95- text-decoration: underline dotted;
96-}
97-
98-/*
99-Remove the default font size and weight for headings.
100-*/
101-
102-h1,
103-h2,
104-h3,
105-h4,
106-h5,
107-h6 {
108- font-size: inherit;
109- font-weight: inherit;
110-}
111-
112-/*
113-Reset links to optimize for opt-in styling instead of opt-out.
114-*/
115-
116-a {
117- color: inherit;
118- text-decoration: inherit;
119-}
120-
121-/*
122-Add the correct font weight in Edge and Safari.
123-*/
124-
125-b,
126-strong {
127- font-weight: bolder;
128-}
129-
130-/*
131-1. Use the user's configured `mono` font family by default.
132-2. Correct the odd `em` font sizing in all browsers.
133-*/
134-
135-code,
136-kbd,
137-samp,
138-pre {
139- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
140- /* 1 */
141- font-size: 1em;
142- /* 2 */
143-}
144-
145-/*
146-Add the correct font size in all browsers.
147-*/
148-
149-small {
150- font-size: 80%;
151-}
152-
153-/*
154-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
155-*/
156-
157-sub,
158-sup {
159- font-size: 75%;
160- line-height: 0;
161- position: relative;
162- vertical-align: baseline;
163-}
164-
165-sub {
166- bottom: -0.25em;
167-}
168-
169-sup {
170- top: -0.5em;
171-}
172-
173-/*
174-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
175-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
176-3. Remove gaps between table borders by default.
177-*/
178-
179-table {
180- text-indent: 0;
181- /* 1 */
182- border-color: inherit;
183- /* 2 */
184- border-collapse: collapse;
185- /* 3 */
186-}
187-
188-/*
189-1. Change the font styles in all browsers.
190-2. Remove the margin in Firefox and Safari.
191-3. Remove default padding in all browsers.
192-*/
193-
194-button,
195-input,
196-optgroup,
197-select,
198-textarea {
199- font-family: inherit;
200- /* 1 */
201- font-feature-settings: inherit;
202- /* 1 */
203- font-variation-settings: inherit;
204- /* 1 */
205- font-size: 100%;
206- /* 1 */
207- font-weight: inherit;
208- /* 1 */
209- line-height: inherit;
210- /* 1 */
211- color: inherit;
212- /* 1 */
213- margin: 0;
214- /* 2 */
215- padding: 0;
216- /* 3 */
217-}
218-
219-/*
220-Remove the inheritance of text transform in Edge and Firefox.
221-*/
222-
223-button,
224-select {
225- text-transform: none;
226-}
227-
228-/*
229-1. Correct the inability to style clickable types in iOS and Safari.
230-2. Remove default button styles.
231-*/
232-
233-button,
234-[type='button'],
235-[type='reset'],
236-[type='submit'] {
237- -webkit-appearance: button;
238- /* 1 */
239- background-color: transparent;
240- /* 2 */
241- background-image: none;
242- /* 2 */
243-}
244-
245-/*
246-Use the modern Firefox focus style for all focusable elements.
247-*/
248-
249-:-moz-focusring {
250- outline: auto;
251-}
252-
253-/*
254-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
255-*/
256-
257-:-moz-ui-invalid {
258- box-shadow: none;
259-}
260-
261-/*
262-Add the correct vertical alignment in Chrome and Firefox.
263-*/
264-
265-progress {
266- vertical-align: baseline;
267-}
268-
269-/*
270-Correct the cursor style of increment and decrement buttons in Safari.
271-*/
272-
273-::-webkit-inner-spin-button,
274-::-webkit-outer-spin-button {
275- height: auto;
276-}
277-
278-/*
279-1. Correct the odd appearance in Chrome and Safari.
280-2. Correct the outline style in Safari.
281-*/
282-
283-[type='search'] {
284- -webkit-appearance: textfield;
285- /* 1 */
286- outline-offset: -2px;
287- /* 2 */
288-}
289-
290-/*
291-Remove the inner padding in Chrome and Safari on macOS.
292-*/
293-
294-::-webkit-search-decoration {
295- -webkit-appearance: none;
296-}
297-
298-/*
299-1. Correct the inability to style clickable types in iOS and Safari.
300-2. Change font properties to `inherit` in Safari.
301-*/
302-
303-::-webkit-file-upload-button {
304- -webkit-appearance: button;
305- /* 1 */
306- font: inherit;
307- /* 2 */
308-}
309-
310-/*
311-Add the correct display in Chrome and Safari.
312-*/
313-
314-summary {
315- display: list-item;
316-}
317-
318-/*
319-Removes the default spacing and border for appropriate elements.
320-*/
321-
322-blockquote,
323-dl,
324-dd,
325-h1,
326-h2,
327-h3,
328-h4,
329-h5,
330-h6,
331-hr,
332-figure,
333-p,
334-pre {
335- margin: 0;
336-}
337-
338-fieldset {
339- margin: 0;
340- padding: 0;
341-}
342-
343-legend {
344- padding: 0;
345-}
346-
347-ol,
348-ul,
349-menu {
350- list-style: none;
351- margin: 0;
352- padding: 0;
353-}
354-
355-/*
356-Reset default styling for dialogs.
357-*/
358-
359-dialog {
360- padding: 0;
361-}
362-
363-/*
364-Prevent resizing textareas horizontally by default.
365-*/
366-
367-textarea {
368- resize: vertical;
369-}
370-
371-/*
372-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
373-2. Set the default placeholder color to the user's configured gray 400 color.
374-*/
375-
376-input::-moz-placeholder, textarea::-moz-placeholder {
377- opacity: 1;
378- /* 1 */
379- color: #9ca3af;
380- /* 2 */
381-}
382-
383-input::placeholder,
384-textarea::placeholder {
385- opacity: 1;
386- /* 1 */
387- color: #9ca3af;
388- /* 2 */
389-}
390-
391-/*
392-Set the default cursor for buttons.
393-*/
394-
395-button,
396-[role="button"] {
397- cursor: pointer;
398-}
399-
400-/*
401-Make sure disabled buttons don't get the pointer cursor.
402-*/
403-
404-:disabled {
405- cursor: default;
406-}
407-
408-/*
409-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
410-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
411- This can trigger a poorly considered lint error in some tools but is included by design.
412-*/
413-
414-img,
415-svg,
416-video,
417-canvas,
418-audio,
419-iframe,
420-embed,
421-object {
422- display: block;
423- /* 1 */
424- vertical-align: middle;
425- /* 2 */
426-}
427-
428-/*
429-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
430-*/
431-
432-img,
433-video {
434- max-width: 100%;
435- height: auto;
436-}
437-
438-/* Make elements with the HTML hidden attribute stay hidden by default */
439-
440-[hidden] {
441- display: none;
442-}
443-
444-*, ::before, ::after {
445- --tw-border-spacing-x: 0;
446- --tw-border-spacing-y: 0;
447- --tw-translate-x: 0;
448- --tw-translate-y: 0;
449- --tw-rotate: 0;
450- --tw-skew-x: 0;
451- --tw-skew-y: 0;
452- --tw-scale-x: 1;
453- --tw-scale-y: 1;
454- --tw-pan-x: ;
455- --tw-pan-y: ;
456- --tw-pinch-zoom: ;
457- --tw-scroll-snap-strictness: proximity;
458- --tw-gradient-from-position: ;
459- --tw-gradient-via-position: ;
460- --tw-gradient-to-position: ;
461- --tw-ordinal: ;
462- --tw-slashed-zero: ;
463- --tw-numeric-figure: ;
464- --tw-numeric-spacing: ;
465- --tw-numeric-fraction: ;
466- --tw-ring-inset: ;
467- --tw-ring-offset-width: 0px;
468- --tw-ring-offset-color: #fff;
469- --tw-ring-color: rgb(59 130 246 / 0.5);
470- --tw-ring-offset-shadow: 0 0 #0000;
471- --tw-ring-shadow: 0 0 #0000;
472- --tw-shadow: 0 0 #0000;
473- --tw-shadow-colored: 0 0 #0000;
474- --tw-blur: ;
475- --tw-brightness: ;
476- --tw-contrast: ;
477- --tw-grayscale: ;
478- --tw-hue-rotate: ;
479- --tw-invert: ;
480- --tw-saturate: ;
481- --tw-sepia: ;
482- --tw-drop-shadow: ;
483- --tw-backdrop-blur: ;
484- --tw-backdrop-brightness: ;
485- --tw-backdrop-contrast: ;
486- --tw-backdrop-grayscale: ;
487- --tw-backdrop-hue-rotate: ;
488- --tw-backdrop-invert: ;
489- --tw-backdrop-opacity: ;
490- --tw-backdrop-saturate: ;
491- --tw-backdrop-sepia: ;
492-}
493-
494-::backdrop {
495- --tw-border-spacing-x: 0;
496- --tw-border-spacing-y: 0;
497- --tw-translate-x: 0;
498- --tw-translate-y: 0;
499- --tw-rotate: 0;
500- --tw-skew-x: 0;
501- --tw-skew-y: 0;
502- --tw-scale-x: 1;
503- --tw-scale-y: 1;
504- --tw-pan-x: ;
505- --tw-pan-y: ;
506- --tw-pinch-zoom: ;
507- --tw-scroll-snap-strictness: proximity;
508- --tw-gradient-from-position: ;
509- --tw-gradient-via-position: ;
510- --tw-gradient-to-position: ;
511- --tw-ordinal: ;
512- --tw-slashed-zero: ;
513- --tw-numeric-figure: ;
514- --tw-numeric-spacing: ;
515- --tw-numeric-fraction: ;
516- --tw-ring-inset: ;
517- --tw-ring-offset-width: 0px;
518- --tw-ring-offset-color: #fff;
519- --tw-ring-color: rgb(59 130 246 / 0.5);
520- --tw-ring-offset-shadow: 0 0 #0000;
521- --tw-ring-shadow: 0 0 #0000;
522- --tw-shadow: 0 0 #0000;
523- --tw-shadow-colored: 0 0 #0000;
524- --tw-blur: ;
525- --tw-brightness: ;
526- --tw-contrast: ;
527- --tw-grayscale: ;
528- --tw-hue-rotate: ;
529- --tw-invert: ;
530- --tw-saturate: ;
531- --tw-sepia: ;
532- --tw-drop-shadow: ;
533- --tw-backdrop-blur: ;
534- --tw-backdrop-brightness: ;
535- --tw-backdrop-contrast: ;
536- --tw-backdrop-grayscale: ;
537- --tw-backdrop-hue-rotate: ;
538- --tw-backdrop-invert: ;
539- --tw-backdrop-opacity: ;
540- --tw-backdrop-saturate: ;
541- --tw-backdrop-sepia: ;
542-}
543-
544-.my-2 {
545- margin-top: 0.5rem;
546- margin-bottom: 0.5rem;
547-}
548-
549-.mb-4 {
550- margin-bottom: 1rem;
551-}
552-
553-.ml-2 {
554- margin-left: 0.5rem;
555-}
556-
557-.ml-4 {
558- margin-left: 1rem;
559-}
560-
561-.mr-2 {
562- margin-right: 0.5rem;
563-}
564-
565-.mt-4 {
566- margin-top: 1rem;
567-}
568-
569-.flex {
570- display: flex;
571-}
572-
573-.h-10 {
574- height: 2.5rem;
575-}
576-
577-.h-24 {
578- height: 6rem;
579-}
580-
581-.h-32 {
582- height: 8rem;
583-}
584-
585-.h-44 {
586- height: 11rem;
587-}
588-
589-.h-64 {
590- height: 16rem;
591-}
592-
593-.h-full {
594- height: 100%;
595-}
596-
597-.w-1\/5 {
598- width: 20%;
599-}
600-
601-.w-2\/6 {
602- width: 33.333333%;
603-}
604-
605-.w-4\/6 {
606- width: 66.666667%;
607-}
608-
609-.w-72 {
610- width: 18rem;
611-}
612-
613-.w-full {
614- width: 100%;
615-}
616-
617-.w-2\/3 {
618- width: 66.666667%;
619-}
620-
621-.w-screen {
622- width: 100vw;
623-}
624-
625-.w-96 {
626- width: 24rem;
627-}
628-
629-.flex-1 {
630- flex: 1 1 0%;
631-}
632-
633-.flex-grow {
634- flex-grow: 1;
635-}
636-
637-.flex-row {
638- flex-direction: row;
639-}
640-
641-.flex-col {
642- flex-direction: column;
643-}
644-
645-.items-center {
646- align-items: center;
647-}
648-
649-.justify-end {
650- justify-content: flex-end;
651-}
652-
653-.justify-center {
654- justify-content: center;
655-}
656-
657-.justify-between {
658- justify-content: space-between;
659-}
660-
661-.justify-items-center {
662- justify-items: center;
663-}
664-
665-.space-x-4 > :not([hidden]) ~ :not([hidden]) {
666- --tw-space-x-reverse: 0;
667- margin-right: calc(1rem * var(--tw-space-x-reverse));
668- margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
669-}
670-
671-.space-y-2 > :not([hidden]) ~ :not([hidden]) {
672- --tw-space-y-reverse: 0;
673- margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
674- margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
675-}
676-
677-.space-y-4 > :not([hidden]) ~ :not([hidden]) {
678- --tw-space-y-reverse: 0;
679- margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
680- margin-bottom: calc(1rem * var(--tw-space-y-reverse));
681-}
682-
683-.space-x-3 > :not([hidden]) ~ :not([hidden]) {
684- --tw-space-x-reverse: 0;
685- margin-right: calc(0.75rem * var(--tw-space-x-reverse));
686- margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
687-}
688-
689-.space-y-3 > :not([hidden]) ~ :not([hidden]) {
690- --tw-space-y-reverse: 0;
691- margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
692- margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
693-}
694-
695-.overflow-y-scroll {
696- overflow-y: scroll;
697-}
698-
699-.rounded-lg {
700- border-radius: 0.5rem;
701-}
702-
703-.rounded-md {
704- border-radius: 0.375rem;
705-}
706-
707-.rounded-sm {
708- border-radius: 0.125rem;
709-}
710-
711-.border {
712- border-width: 1px;
713-}
714-
715-.border-gray-300 {
716- --tw-border-opacity: 1;
717- border-color: rgb(209 213 219 / var(--tw-border-opacity));
718-}
719-
720-.bg-gordonmascot-lblue {
721- --tw-bg-opacity: 1;
722- background-color: rgb(0 123 192 / var(--tw-bg-opacity));
723-}
724-
725-.bg-gordonmascot-mblue {
726- --tw-bg-opacity: 1;
727- background-color: rgb(0 82 147 / var(--tw-bg-opacity));
728-}
729-
730-.bg-gray-300 {
731- --tw-bg-opacity: 1;
732- background-color: rgb(209 213 219 / var(--tw-bg-opacity));
733-}
734-
735-.bg-transparent {
736- background-color: transparent;
737-}
738-
739-.bg-white {
740- --tw-bg-opacity: 1;
741- background-color: rgb(255 255 255 / var(--tw-bg-opacity));
742-}
743-
744-.bg-zinc-200 {
745- --tw-bg-opacity: 1;
746- background-color: rgb(228 228 231 / var(--tw-bg-opacity));
747-}
748-
749-.bg-gordonmascot-hblue {
750- --tw-bg-opacity: 1;
751- background-color: rgb(1 30 92 / var(--tw-bg-opacity));
752-}
753-
754-.bg-zinc-400 {
755- --tw-bg-opacity: 1;
756- background-color: rgb(161 161 170 / var(--tw-bg-opacity));
757-}
758-
759-.p-4 {
760- padding: 1rem;
761-}
762-
763-.p-2 {
764- padding: 0.5rem;
765-}
766-
767-.px-2 {
768- padding-left: 0.5rem;
769- padding-right: 0.5rem;
770-}
771-
772-.px-6 {
773- padding-left: 1.5rem;
774- padding-right: 1.5rem;
775-}
776-
777-.py-2 {
778- padding-top: 0.5rem;
779- padding-bottom: 0.5rem;
780-}
781-
782-.px-4 {
783- padding-left: 1rem;
784- padding-right: 1rem;
785-}
786-
787-.text-2xl {
788- font-size: 1.5rem;
789- line-height: 2rem;
790-}
791-
792-.text-5xl {
793- font-size: 3rem;
794- line-height: 1;
795-}
796-
797-.text-lg {
798- font-size: 1.125rem;
799- line-height: 1.75rem;
800-}
801-
802-.text-sm {
803- font-size: 0.875rem;
804- line-height: 1.25rem;
805-}
806-
807-.text-xl {
808- font-size: 1.25rem;
809- line-height: 1.75rem;
810-}
811-
812-.font-semibold {
813- font-weight: 600;
814-}
815-
816-.tracking-tighter {
817- letter-spacing: -0.05em;
818-}
819-
820-.text-gordonmascot-hblue {
821- --tw-text-opacity: 1;
822- color: rgb(1 30 92 / var(--tw-text-opacity));
823-}
824-
825-.text-gordonmascot-mblue {
826- --tw-text-opacity: 1;
827- color: rgb(0 82 147 / var(--tw-text-opacity));
828-}
829-
830-.text-gray-100 {
831- --tw-text-opacity: 1;
832- color: rgb(243 244 246 / var(--tw-text-opacity));
833-}
834-
835-.text-gray-800 {
836- --tw-text-opacity: 1;
837- color: rgb(31 41 55 / var(--tw-text-opacity));
838-}
839-
840-.text-slate-400 {
841- --tw-text-opacity: 1;
842- color: rgb(148 163 184 / var(--tw-text-opacity));
843-}
844-
845-.text-slate-100 {
846- --tw-text-opacity: 1;
847- color: rgb(241 245 249 / var(--tw-text-opacity));
848-}
849-
850-.shadow {
851- --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
852- --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
853- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
854-}
855-
856-.outline-none {
857- outline: 2px solid transparent;
858- outline-offset: 2px;
859-}
860-
861-/* Import IBM Plex Fonts */
862-
863-@font-face {
864- font-family: "IBMPlexSans-Bold";
865-
866- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2");
867-
868- font-weight: bold;
869-
870- font-style: normal;
871-}
872-
873-@font-face {
874- font-family: "IBMPlexSans-Regular";
875-
876- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2");
877-
878- font-weight: normal;
879-
880- font-style: normal;
881-}
882-
883-/* Import Jetbrains Mono Nerd Font */
884-
885-@font-face {
886- font-family: "JetBrainsMono-Nerd-Font-Regular";
887-
888- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2")
889- format("woff2");
890-
891- font-weight: normal;
892-
893- font-style: normal;
894-}
895-
896-@font-face {
897- font-family: "LilitaOne-Regular";
898-
899- src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype");
900-
901- font-weight: normal;
902-
903- font-style: normal;
904-}
905-
906-.fade-me-out.htmx-swapping {
907- opacity: 0;
908- transition: opacity 1s ease-out;
909-}
910-
911-/* View transition */
912-
913-@keyframes fade-in {
914- from {
915- opacity: 0;
916- }
917-}
918-
919-@keyframes fade-out {
920- to {
921- opacity: 0;
922- }
923-}
924-
925-@keyframes slide-from-right {
926- from {
927- transform: translateX(90px);
928- }
929-}
930-
931-@keyframes slide-to-left {
932- to {
933- transform: translateX(-90px);
934- }
935-}
936-
937-.slide-it {
938- view-transition-name: slide-it;
939-}
940-
941-::view-transition-old(slide-it) {
942- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
943- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
944-}
945-
946-::view-transition-new(slide-it) {
947- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
948- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
949-}
950-
951-.gordon-title {
952- font-family: "LilitaOne-Regular", sans-serif;
953-}
954-
955-.IBMPlexSans {
956- font-family: "IBMPlexSans-Regular", sans-serif;
957-}
958-
959-.circle-shadow {
960- width: 100%;
961- position: relative;
962-}
963-
964-.circle-shadow::after {
965- position: absolute;
966- width: 50%;
967- height: 0.4rem;
968- margin: auto;
969- background: rgba(0, 0, 0, 0.1);
970- border-radius: 50%;
971- z-index: -1;
972- bottom: 2%;
973- content: "";
974- box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1);
975-}
976-
977-.iconoir {
978- margin-right: 8px;
979-}
980-
981-@media (min-width: 640px) {
982- .sm\:h-24 {
983- height: 6rem;
984- }
985-}
986-
987-@media (min-width: 768px) {
988- .md\:w-1\/2 {
989- width: 50%;
990- }
991-
992- .md\:flex-row {
993- flex-direction: row;
994- }
995-
996- .md\:space-y-6 > :not([hidden]) ~ :not([hidden]) {
997- --tw-space-y-reverse: 0;
998- margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
999- margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
1000- }
1001-
1002- .md\:p-8 {
1003- padding: 2rem;
1004- }
1005-
1006- .md\:py-8 {
1007- padding-top: 2rem;
1008- padding-bottom: 2rem;
1009- }
1010-
1011- .md\:pl-4 {
1012- padding-left: 1rem;
1013- }
1014-
1015- .md\:pt-10 {
1016- padding-top: 2.5rem;
1017- }
1018-
1019- .md\:text-2xl {
1020- font-size: 1.5rem;
1021- line-height: 2rem;
1022- }
1023-
1024- .md\:text-3xl {
1025- font-size: 1.875rem;
1026- line-height: 2.25rem;
1027- }
1028-}
1029-
1030-@media (min-width: 1024px) {
1031- .lg\:text-4xl {
1032- font-size: 2.25rem;
1033- line-height: 2.5rem;
1034- }
1035-}
1036-
1037-@media (min-width: 1280px) {
1038- .xl\:h-48 {
1039- height: 12rem;
1040- }
1041-
1042- .xl\:text-5xl {
1043- font-size: 3rem;
1044- line-height: 1;
1045- }
1046-}
@@ -1,1046 +0,0 @@
1-/**
2- * Path: internal/ui/public/assets/css/custom.css
3- *
4- * @format
5- */
6-
7-/* Embeded path : assets/css/cutsom.css */
8-
9-/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
10- */
11-
12-/*
13-1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
14-2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
15-*/
16-
17-*,
18-::before,
19-::after {
20- box-sizing: border-box;
21- /* 1 */
22- border-width: 0;
23- /* 2 */
24- border-style: solid;
25- /* 2 */
26- border-color: #e5e7eb;
27- /* 2 */
28-}
29-
30-::before,
31-::after {
32- --tw-content: '';
33-}
34-
35-/*
36-1. Use a consistent sensible line-height in all browsers.
37-2. Prevent adjustments of font size after orientation changes in iOS.
38-3. Use a more readable tab size.
39-4. Use the user's configured `sans` font-family by default.
40-5. Use the user's configured `sans` font-feature-settings by default.
41-6. Use the user's configured `sans` font-variation-settings by default.
42-*/
43-
44-html {
45- line-height: 1.5;
46- /* 1 */
47- -webkit-text-size-adjust: 100%;
48- /* 2 */
49- -moz-tab-size: 4;
50- /* 3 */
51- -o-tab-size: 4;
52- tab-size: 4;
53- /* 3 */
54- font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
55- /* 4 */
56- font-feature-settings: normal;
57- /* 5 */
58- font-variation-settings: normal;
59- /* 6 */
60-}
61-
62-/*
63-1. Remove the margin in all browsers.
64-2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
65-*/
66-
67-body {
68- margin: 0;
69- /* 1 */
70- line-height: inherit;
71- /* 2 */
72-}
73-
74-/*
75-1. Add the correct height in Firefox.
76-2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
77-3. Ensure horizontal rules are visible by default.
78-*/
79-
80-hr {
81- height: 0;
82- /* 1 */
83- color: inherit;
84- /* 2 */
85- border-top-width: 1px;
86- /* 3 */
87-}
88-
89-/*
90-Add the correct text decoration in Chrome, Edge, and Safari.
91-*/
92-
93-abbr:where([title]) {
94- -webkit-text-decoration: underline dotted;
95- text-decoration: underline dotted;
96-}
97-
98-/*
99-Remove the default font size and weight for headings.
100-*/
101-
102-h1,
103-h2,
104-h3,
105-h4,
106-h5,
107-h6 {
108- font-size: inherit;
109- font-weight: inherit;
110-}
111-
112-/*
113-Reset links to optimize for opt-in styling instead of opt-out.
114-*/
115-
116-a {
117- color: inherit;
118- text-decoration: inherit;
119-}
120-
121-/*
122-Add the correct font weight in Edge and Safari.
123-*/
124-
125-b,
126-strong {
127- font-weight: bolder;
128-}
129-
130-/*
131-1. Use the user's configured `mono` font family by default.
132-2. Correct the odd `em` font sizing in all browsers.
133-*/
134-
135-code,
136-kbd,
137-samp,
138-pre {
139- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
140- /* 1 */
141- font-size: 1em;
142- /* 2 */
143-}
144-
145-/*
146-Add the correct font size in all browsers.
147-*/
148-
149-small {
150- font-size: 80%;
151-}
152-
153-/*
154-Prevent `sub` and `sup` elements from affecting the line height in all browsers.
155-*/
156-
157-sub,
158-sup {
159- font-size: 75%;
160- line-height: 0;
161- position: relative;
162- vertical-align: baseline;
163-}
164-
165-sub {
166- bottom: -0.25em;
167-}
168-
169-sup {
170- top: -0.5em;
171-}
172-
173-/*
174-1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
175-2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
176-3. Remove gaps between table borders by default.
177-*/
178-
179-table {
180- text-indent: 0;
181- /* 1 */
182- border-color: inherit;
183- /* 2 */
184- border-collapse: collapse;
185- /* 3 */
186-}
187-
188-/*
189-1. Change the font styles in all browsers.
190-2. Remove the margin in Firefox and Safari.
191-3. Remove default padding in all browsers.
192-*/
193-
194-button,
195-input,
196-optgroup,
197-select,
198-textarea {
199- font-family: inherit;
200- /* 1 */
201- font-feature-settings: inherit;
202- /* 1 */
203- font-variation-settings: inherit;
204- /* 1 */
205- font-size: 100%;
206- /* 1 */
207- font-weight: inherit;
208- /* 1 */
209- line-height: inherit;
210- /* 1 */
211- color: inherit;
212- /* 1 */
213- margin: 0;
214- /* 2 */
215- padding: 0;
216- /* 3 */
217-}
218-
219-/*
220-Remove the inheritance of text transform in Edge and Firefox.
221-*/
222-
223-button,
224-select {
225- text-transform: none;
226-}
227-
228-/*
229-1. Correct the inability to style clickable types in iOS and Safari.
230-2. Remove default button styles.
231-*/
232-
233-button,
234-[type='button'],
235-[type='reset'],
236-[type='submit'] {
237- -webkit-appearance: button;
238- /* 1 */
239- background-color: transparent;
240- /* 2 */
241- background-image: none;
242- /* 2 */
243-}
244-
245-/*
246-Use the modern Firefox focus style for all focusable elements.
247-*/
248-
249-:-moz-focusring {
250- outline: auto;
251-}
252-
253-/*
254-Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
255-*/
256-
257-:-moz-ui-invalid {
258- box-shadow: none;
259-}
260-
261-/*
262-Add the correct vertical alignment in Chrome and Firefox.
263-*/
264-
265-progress {
266- vertical-align: baseline;
267-}
268-
269-/*
270-Correct the cursor style of increment and decrement buttons in Safari.
271-*/
272-
273-::-webkit-inner-spin-button,
274-::-webkit-outer-spin-button {
275- height: auto;
276-}
277-
278-/*
279-1. Correct the odd appearance in Chrome and Safari.
280-2. Correct the outline style in Safari.
281-*/
282-
283-[type='search'] {
284- -webkit-appearance: textfield;
285- /* 1 */
286- outline-offset: -2px;
287- /* 2 */
288-}
289-
290-/*
291-Remove the inner padding in Chrome and Safari on macOS.
292-*/
293-
294-::-webkit-search-decoration {
295- -webkit-appearance: none;
296-}
297-
298-/*
299-1. Correct the inability to style clickable types in iOS and Safari.
300-2. Change font properties to `inherit` in Safari.
301-*/
302-
303-::-webkit-file-upload-button {
304- -webkit-appearance: button;
305- /* 1 */
306- font: inherit;
307- /* 2 */
308-}
309-
310-/*
311-Add the correct display in Chrome and Safari.
312-*/
313-
314-summary {
315- display: list-item;
316-}
317-
318-/*
319-Removes the default spacing and border for appropriate elements.
320-*/
321-
322-blockquote,
323-dl,
324-dd,
325-h1,
326-h2,
327-h3,
328-h4,
329-h5,
330-h6,
331-hr,
332-figure,
333-p,
334-pre {
335- margin: 0;
336-}
337-
338-fieldset {
339- margin: 0;
340- padding: 0;
341-}
342-
343-legend {
344- padding: 0;
345-}
346-
347-ol,
348-ul,
349-menu {
350- list-style: none;
351- margin: 0;
352- padding: 0;
353-}
354-
355-/*
356-Reset default styling for dialogs.
357-*/
358-
359-dialog {
360- padding: 0;
361-}
362-
363-/*
364-Prevent resizing textareas horizontally by default.
365-*/
366-
367-textarea {
368- resize: vertical;
369-}
370-
371-/*
372-1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
373-2. Set the default placeholder color to the user's configured gray 400 color.
374-*/
375-
376-input::-moz-placeholder, textarea::-moz-placeholder {
377- opacity: 1;
378- /* 1 */
379- color: #9ca3af;
380- /* 2 */
381-}
382-
383-input::placeholder,
384-textarea::placeholder {
385- opacity: 1;
386- /* 1 */
387- color: #9ca3af;
388- /* 2 */
389-}
390-
391-/*
392-Set the default cursor for buttons.
393-*/
394-
395-button,
396-[role="button"] {
397- cursor: pointer;
398-}
399-
400-/*
401-Make sure disabled buttons don't get the pointer cursor.
402-*/
403-
404-:disabled {
405- cursor: default;
406-}
407-
408-/*
409-1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
410-2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
411- This can trigger a poorly considered lint error in some tools but is included by design.
412-*/
413-
414-img,
415-svg,
416-video,
417-canvas,
418-audio,
419-iframe,
420-embed,
421-object {
422- display: block;
423- /* 1 */
424- vertical-align: middle;
425- /* 2 */
426-}
427-
428-/*
429-Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
430-*/
431-
432-img,
433-video {
434- max-width: 100%;
435- height: auto;
436-}
437-
438-/* Make elements with the HTML hidden attribute stay hidden by default */
439-
440-[hidden] {
441- display: none;
442-}
443-
444-*, ::before, ::after {
445- --tw-border-spacing-x: 0;
446- --tw-border-spacing-y: 0;
447- --tw-translate-x: 0;
448- --tw-translate-y: 0;
449- --tw-rotate: 0;
450- --tw-skew-x: 0;
451- --tw-skew-y: 0;
452- --tw-scale-x: 1;
453- --tw-scale-y: 1;
454- --tw-pan-x: ;
455- --tw-pan-y: ;
456- --tw-pinch-zoom: ;
457- --tw-scroll-snap-strictness: proximity;
458- --tw-gradient-from-position: ;
459- --tw-gradient-via-position: ;
460- --tw-gradient-to-position: ;
461- --tw-ordinal: ;
462- --tw-slashed-zero: ;
463- --tw-numeric-figure: ;
464- --tw-numeric-spacing: ;
465- --tw-numeric-fraction: ;
466- --tw-ring-inset: ;
467- --tw-ring-offset-width: 0px;
468- --tw-ring-offset-color: #fff;
469- --tw-ring-color: rgb(59 130 246 / 0.5);
470- --tw-ring-offset-shadow: 0 0 #0000;
471- --tw-ring-shadow: 0 0 #0000;
472- --tw-shadow: 0 0 #0000;
473- --tw-shadow-colored: 0 0 #0000;
474- --tw-blur: ;
475- --tw-brightness: ;
476- --tw-contrast: ;
477- --tw-grayscale: ;
478- --tw-hue-rotate: ;
479- --tw-invert: ;
480- --tw-saturate: ;
481- --tw-sepia: ;
482- --tw-drop-shadow: ;
483- --tw-backdrop-blur: ;
484- --tw-backdrop-brightness: ;
485- --tw-backdrop-contrast: ;
486- --tw-backdrop-grayscale: ;
487- --tw-backdrop-hue-rotate: ;
488- --tw-backdrop-invert: ;
489- --tw-backdrop-opacity: ;
490- --tw-backdrop-saturate: ;
491- --tw-backdrop-sepia: ;
492-}
493-
494-::backdrop {
495- --tw-border-spacing-x: 0;
496- --tw-border-spacing-y: 0;
497- --tw-translate-x: 0;
498- --tw-translate-y: 0;
499- --tw-rotate: 0;
500- --tw-skew-x: 0;
501- --tw-skew-y: 0;
502- --tw-scale-x: 1;
503- --tw-scale-y: 1;
504- --tw-pan-x: ;
505- --tw-pan-y: ;
506- --tw-pinch-zoom: ;
507- --tw-scroll-snap-strictness: proximity;
508- --tw-gradient-from-position: ;
509- --tw-gradient-via-position: ;
510- --tw-gradient-to-position: ;
511- --tw-ordinal: ;
512- --tw-slashed-zero: ;
513- --tw-numeric-figure: ;
514- --tw-numeric-spacing: ;
515- --tw-numeric-fraction: ;
516- --tw-ring-inset: ;
517- --tw-ring-offset-width: 0px;
518- --tw-ring-offset-color: #fff;
519- --tw-ring-color: rgb(59 130 246 / 0.5);
520- --tw-ring-offset-shadow: 0 0 #0000;
521- --tw-ring-shadow: 0 0 #0000;
522- --tw-shadow: 0 0 #0000;
523- --tw-shadow-colored: 0 0 #0000;
524- --tw-blur: ;
525- --tw-brightness: ;
526- --tw-contrast: ;
527- --tw-grayscale: ;
528- --tw-hue-rotate: ;
529- --tw-invert: ;
530- --tw-saturate: ;
531- --tw-sepia: ;
532- --tw-drop-shadow: ;
533- --tw-backdrop-blur: ;
534- --tw-backdrop-brightness: ;
535- --tw-backdrop-contrast: ;
536- --tw-backdrop-grayscale: ;
537- --tw-backdrop-hue-rotate: ;
538- --tw-backdrop-invert: ;
539- --tw-backdrop-opacity: ;
540- --tw-backdrop-saturate: ;
541- --tw-backdrop-sepia: ;
542-}
543-
544-.my-2 {
545- margin-top: 0.5rem;
546- margin-bottom: 0.5rem;
547-}
548-
549-.mb-4 {
550- margin-bottom: 1rem;
551-}
552-
553-.ml-2 {
554- margin-left: 0.5rem;
555-}
556-
557-.ml-4 {
558- margin-left: 1rem;
559-}
560-
561-.mr-2 {
562- margin-right: 0.5rem;
563-}
564-
565-.mt-4 {
566- margin-top: 1rem;
567-}
568-
569-.flex {
570- display: flex;
571-}
572-
573-.h-10 {
574- height: 2.5rem;
575-}
576-
577-.h-24 {
578- height: 6rem;
579-}
580-
581-.h-32 {
582- height: 8rem;
583-}
584-
585-.h-44 {
586- height: 11rem;
587-}
588-
589-.h-64 {
590- height: 16rem;
591-}
592-
593-.h-full {
594- height: 100%;
595-}
596-
597-.w-1\/5 {
598- width: 20%;
599-}
600-
601-.w-2\/6 {
602- width: 33.333333%;
603-}
604-
605-.w-4\/6 {
606- width: 66.666667%;
607-}
608-
609-.w-72 {
610- width: 18rem;
611-}
612-
613-.w-full {
614- width: 100%;
615-}
616-
617-.w-2\/3 {
618- width: 66.666667%;
619-}
620-
621-.w-screen {
622- width: 100vw;
623-}
624-
625-.w-96 {
626- width: 24rem;
627-}
628-
629-.flex-1 {
630- flex: 1 1 0%;
631-}
632-
633-.flex-grow {
634- flex-grow: 1;
635-}
636-
637-.flex-row {
638- flex-direction: row;
639-}
640-
641-.flex-col {
642- flex-direction: column;
643-}
644-
645-.items-center {
646- align-items: center;
647-}
648-
649-.justify-end {
650- justify-content: flex-end;
651-}
652-
653-.justify-center {
654- justify-content: center;
655-}
656-
657-.justify-between {
658- justify-content: space-between;
659-}
660-
661-.justify-items-center {
662- justify-items: center;
663-}
664-
665-.space-x-4 > :not([hidden]) ~ :not([hidden]) {
666- --tw-space-x-reverse: 0;
667- margin-right: calc(1rem * var(--tw-space-x-reverse));
668- margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
669-}
670-
671-.space-y-2 > :not([hidden]) ~ :not([hidden]) {
672- --tw-space-y-reverse: 0;
673- margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
674- margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
675-}
676-
677-.space-y-4 > :not([hidden]) ~ :not([hidden]) {
678- --tw-space-y-reverse: 0;
679- margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
680- margin-bottom: calc(1rem * var(--tw-space-y-reverse));
681-}
682-
683-.space-x-3 > :not([hidden]) ~ :not([hidden]) {
684- --tw-space-x-reverse: 0;
685- margin-right: calc(0.75rem * var(--tw-space-x-reverse));
686- margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
687-}
688-
689-.space-y-3 > :not([hidden]) ~ :not([hidden]) {
690- --tw-space-y-reverse: 0;
691- margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
692- margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
693-}
694-
695-.overflow-y-scroll {
696- overflow-y: scroll;
697-}
698-
699-.rounded-lg {
700- border-radius: 0.5rem;
701-}
702-
703-.rounded-md {
704- border-radius: 0.375rem;
705-}
706-
707-.rounded-sm {
708- border-radius: 0.125rem;
709-}
710-
711-.border {
712- border-width: 1px;
713-}
714-
715-.border-gray-300 {
716- --tw-border-opacity: 1;
717- border-color: rgb(209 213 219 / var(--tw-border-opacity));
718-}
719-
720-.bg-gordonmascot-lblue {
721- --tw-bg-opacity: 1;
722- background-color: rgb(0 123 192 / var(--tw-bg-opacity));
723-}
724-
725-.bg-gordonmascot-mblue {
726- --tw-bg-opacity: 1;
727- background-color: rgb(0 82 147 / var(--tw-bg-opacity));
728-}
729-
730-.bg-gray-300 {
731- --tw-bg-opacity: 1;
732- background-color: rgb(209 213 219 / var(--tw-bg-opacity));
733-}
734-
735-.bg-transparent {
736- background-color: transparent;
737-}
738-
739-.bg-white {
740- --tw-bg-opacity: 1;
741- background-color: rgb(255 255 255 / var(--tw-bg-opacity));
742-}
743-
744-.bg-zinc-200 {
745- --tw-bg-opacity: 1;
746- background-color: rgb(228 228 231 / var(--tw-bg-opacity));
747-}
748-
749-.bg-gordonmascot-hblue {
750- --tw-bg-opacity: 1;
751- background-color: rgb(1 30 92 / var(--tw-bg-opacity));
752-}
753-
754-.bg-zinc-400 {
755- --tw-bg-opacity: 1;
756- background-color: rgb(161 161 170 / var(--tw-bg-opacity));
757-}
758-
759-.p-4 {
760- padding: 1rem;
761-}
762-
763-.p-2 {
764- padding: 0.5rem;
765-}
766-
767-.px-2 {
768- padding-left: 0.5rem;
769- padding-right: 0.5rem;
770-}
771-
772-.px-6 {
773- padding-left: 1.5rem;
774- padding-right: 1.5rem;
775-}
776-
777-.py-2 {
778- padding-top: 0.5rem;
779- padding-bottom: 0.5rem;
780-}
781-
782-.px-4 {
783- padding-left: 1rem;
784- padding-right: 1rem;
785-}
786-
787-.text-2xl {
788- font-size: 1.5rem;
789- line-height: 2rem;
790-}
791-
792-.text-5xl {
793- font-size: 3rem;
794- line-height: 1;
795-}
796-
797-.text-lg {
798- font-size: 1.125rem;
799- line-height: 1.75rem;
800-}
801-
802-.text-sm {
803- font-size: 0.875rem;
804- line-height: 1.25rem;
805-}
806-
807-.text-xl {
808- font-size: 1.25rem;
809- line-height: 1.75rem;
810-}
811-
812-.font-semibold {
813- font-weight: 600;
814-}
815-
816-.tracking-tighter {
817- letter-spacing: -0.05em;
818-}
819-
820-.text-gordonmascot-hblue {
821- --tw-text-opacity: 1;
822- color: rgb(1 30 92 / var(--tw-text-opacity));
823-}
824-
825-.text-gordonmascot-mblue {
826- --tw-text-opacity: 1;
827- color: rgb(0 82 147 / var(--tw-text-opacity));
828-}
829-
830-.text-gray-100 {
831- --tw-text-opacity: 1;
832- color: rgb(243 244 246 / var(--tw-text-opacity));
833-}
834-
835-.text-gray-800 {
836- --tw-text-opacity: 1;
837- color: rgb(31 41 55 / var(--tw-text-opacity));
838-}
839-
840-.text-slate-400 {
841- --tw-text-opacity: 1;
842- color: rgb(148 163 184 / var(--tw-text-opacity));
843-}
844-
845-.text-slate-100 {
846- --tw-text-opacity: 1;
847- color: rgb(241 245 249 / var(--tw-text-opacity));
848-}
849-
850-.shadow {
851- --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
852- --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
853- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
854-}
855-
856-.outline-none {
857- outline: 2px solid transparent;
858- outline-offset: 2px;
859-}
860-
861-/* Import IBM Plex Fonts */
862-
863-@font-face {
864- font-family: "IBMPlexSans-Bold";
865-
866- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2") format("woff2");
867-
868- font-weight: bold;
869-
870- font-style: normal;
871-}
872-
873-@font-face {
874- font-family: "IBMPlexSans-Regular";
875-
876- src: url("/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2") format("woff2");
877-
878- font-weight: normal;
879-
880- font-style: normal;
881-}
882-
883-/* Import Jetbrains Mono Nerd Font */
884-
885-@font-face {
886- font-family: "JetBrainsMono-Nerd-Font-Regular";
887-
888- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.woff2")
889- format("woff2");
890-
891- font-weight: normal;
892-
893- font-style: normal;
894-}
895-
896-@font-face {
897- font-family: "LilitaOne-Regular";
898-
899- src: url("/assets/fonts/LilitaOne/LilitaOne-Regular.ttf") format("truetype");
900-
901- font-weight: normal;
902-
903- font-style: normal;
904-}
905-
906-.fade-me-out.htmx-swapping {
907- opacity: 0;
908- transition: opacity 1s ease-out;
909-}
910-
911-/* View transition */
912-
913-@keyframes fade-in {
914- from {
915- opacity: 0;
916- }
917-}
918-
919-@keyframes fade-out {
920- to {
921- opacity: 0;
922- }
923-}
924-
925-@keyframes slide-from-right {
926- from {
927- transform: translateX(90px);
928- }
929-}
930-
931-@keyframes slide-to-left {
932- to {
933- transform: translateX(-90px);
934- }
935-}
936-
937-.slide-it {
938- view-transition-name: slide-it;
939-}
940-
941-::view-transition-old(slide-it) {
942- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
943- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
944-}
945-
946-::view-transition-new(slide-it) {
947- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
948- 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
949-}
950-
951-.gordon-title {
952- font-family: "LilitaOne-Regular", sans-serif;
953-}
954-
955-.IBMPlexSans {
956- font-family: "IBMPlexSans-Regular", sans-serif;
957-}
958-
959-.circle-shadow {
960- width: 100%;
961- position: relative;
962-}
963-
964-.circle-shadow::after {
965- position: absolute;
966- width: 50%;
967- height: 0.4rem;
968- margin: auto;
969- background: rgba(0, 0, 0, 0.1);
970- border-radius: 50%;
971- z-index: -1;
972- bottom: 2%;
973- content: "";
974- box-shadow: 0 0 1px 0.3rem rgba(0, 0, 0, 0.1);
975-}
976-
977-.iconoir {
978- margin-right: 8px;
979-}
980-
981-@media (min-width: 640px) {
982- .sm\:h-24 {
983- height: 6rem;
984- }
985-}
986-
987-@media (min-width: 768px) {
988- .md\:w-1\/2 {
989- width: 50%;
990- }
991-
992- .md\:flex-row {
993- flex-direction: row;
994- }
995-
996- .md\:space-y-6 > :not([hidden]) ~ :not([hidden]) {
997- --tw-space-y-reverse: 0;
998- margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
999- margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
1000- }
1001-
1002- .md\:p-8 {
1003- padding: 2rem;
1004- }
1005-
1006- .md\:py-8 {
1007- padding-top: 2rem;
1008- padding-bottom: 2rem;
1009- }
1010-
1011- .md\:pl-4 {
1012- padding-left: 1rem;
1013- }
1014-
1015- .md\:pt-10 {
1016- padding-top: 2.5rem;
1017- }
1018-
1019- .md\:text-2xl {
1020- font-size: 1.5rem;
1021- line-height: 2rem;
1022- }
1023-
1024- .md\:text-3xl {
1025- font-size: 1.875rem;
1026- line-height: 2.25rem;
1027- }
1028-}
1029-
1030-@media (min-width: 1024px) {
1031- .lg\:text-4xl {
1032- font-size: 2.25rem;
1033- line-height: 2.5rem;
1034- }
1035-}
1036-
1037-@media (min-width: 1280px) {
1038- .xl\:h-48 {
1039- height: 12rem;
1040- }
1041-
1042- .xl\:text-5xl {
1043- font-size: 3rem;
1044- line-height: 1;
1045- }
1046-}
D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Bold.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Light.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Medium.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Regular.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/IBMPlex/IBMPlexSans-Thin.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Regular.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFontMono-Regular.ttf +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/LilitaOne/LilitaOne-Regular.ttf +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/fonts/LilitaOne/LilitaOne-Regular.woff2 +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/imgs/gordon-mascot-hq-trsp.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/imgs/gordon-mascot-mq-trsp.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/assets/imgs/gordon-mascot.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/favicon-16x16.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/favicon-32x32.png +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/favicon.ico +0 -0 binary file: no text diff

binary file: no text diff

D internal/ui/public/index.gohtml +0 -124
@@ -1,124 +0,0 @@
1-<!-- @format -->
2-
3-<!DOCTYPE html>
4-<html lang="en">
5- <head>
6- <meta charset="UTF-8" />
7- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8- <title>{{ .header.meta.title }}</title>
9- <meta name="description" content="{{ .Header.Meta.Description }}" />
10- <meta name="keywords" content="{{ .Header.Meta.Keywords }}" />
11- <!-- Custom CSS + Tailwind -->
12- <link rel="stylesheet" href="/assets/css/tailwind.css" />
13- <!-- Iconoir -->
14- <link rel="stylesheet" href="/libs/iconoir.css" />
15- <!-- Alpine -->
16- <script src="/libs/alpine.min.js"></script>
17- <!-- HTMX min js -->
18- <script src="/libs/htmx.min.js"></script>
19- <!-- HTMX SSE -->
20- <script src="/libs/htmx-sse.js"></script>
21- <!-- Custom JS from assets/js -->
22- <script src="assets/js/custom.js"></script>
23- </head>
24- <body class="IBMPlexSans bg-zinc-200 md:p-8 flex">
25- <!-- Left Menu -->
26- <div class="w-1/5 flex flex-col space-evenly">
27- <!-- Logo -->
28- <div id="logo-mascot-leftnav" class="flex justify-center p-4">
29- <div class="circle-shadow flex justify-center w-100">
30- <img
31- src="/assets/imgs/gordon-mascot-hq-trsp.png"
32- alt="logo"
33- class="sm:h-24 h-32 xl:h-48"
34- />
35- </div>
36- </div>
37- <!-- Site Name + Version -->
38- <div
39- class="p-4 flex flex-col bg-gordonmascot-lblue rounded-lg shadow space-y-4"
40- >
41- <div class="w-full flex justify-center">
42- <h1
43- class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter"
44- >
45- Gordon
46- </h1>
47- <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm"
48- >v0.0.1</span
49- >
50- </div>
51- <!--(Navigation) -->
52- <div id="menu-leftnav" class="w-full">
53- <ul
54- class="flex flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4"
55- >
56- <li class="flex items-center">
57- <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i>
58- <a href="index.html">Dashboard</a>
59- </li>
60- <li class="flex items-center">
61- <i class="iconoir-server mr-2 md:text-3xl"></i>
62- <a href="#manager">Manager</a>
63- </li>
64- <li class="flex items-center">
65- <i class="iconoir-community mr-2 md:text-3xl"></i>
66- <a href="#users">Users</a>
67- </li>
68- <li class="flex items-center">
69- <i class="iconoir-database-settings mr-2 md:text-3xl"></i>
70- <a href="#gordon-settings">Settings</a>
71- </li>
72- </ul>
73- </div>
74-
75- <div class="w-full h-24 bg-gordonmascot-mblue rounded-lg"></div>
76- <!-- Mock menu item -->
77- </div>
78- </div>
79-
80- <!-- Main Content including the navbar -->
81- <div class="flex-1 flex flex-col md:pt-10 space-y-4 ml-4">
82- <!-- Search Nav within the main content -->
83- <div class="mb-4 flex justify-end">
84- <div class="w-72 h-10 bg-gray-300 rounded-lg">
85- <!-- Search input with iconir search -->
86- <div class="flex justify-between items-center h-full px-2">
87- <input
88- type="text"
89- class="w-full h-full bg-transparent outline-none"
90- placeholder="Search"
91- />
92- <i class="iconoir-search"></i>
93- </div>
94- </div>
95- <!-- Mock search or other element -->
96- </div>
97-
98- <!-- Cards section -->
99- <div class="flex space-x-4 flex-grow">
100- <!-- Main Card on the Left -->
101- <div class="w-4/6 bg-white p-4 rounded-md shadow mb-4">
102- <div class="w-full rounded-sm bg-gray-300"></div>
103- <!-- Mock main card -->
104- </div>
105-
106- <!-- Two Cards on the Right -->
107- <div class="w-2/6 flex flex-col flex-justify-start">
108- <div class="bg-white p-4 rounded-md shadow mb-4">
109- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
110- <!-- Mock card -->
111- </div>
112- <div class="bg-white p-4 rounded-md shadow mb-4">
113- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
114- <!-- Mock card -->
115- </div>
116- <div class="bg-white p-4 rounded-md shadow mb-4">
117- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
118- <!-- Mock card -->
119- </div>
120- </div>
121- </div>
122- </div>
123- </body>
124-</html>
@@ -1,124 +0,0 @@
1-<!-- @format -->
2-
3-<!DOCTYPE html>
4-<html lang="en">
5- <head>
6- <meta charset="UTF-8" />
7- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8- <title>{{ .header.meta.title }}</title>
9- <meta name="description" content="{{ .Header.Meta.Description }}" />
10- <meta name="keywords" content="{{ .Header.Meta.Keywords }}" />
11- <!-- Custom CSS + Tailwind -->
12- <link rel="stylesheet" href="/assets/css/tailwind.css" />
13- <!-- Iconoir -->
14- <link rel="stylesheet" href="/libs/iconoir.css" />
15- <!-- Alpine -->
16- <script src="/libs/alpine.min.js"></script>
17- <!-- HTMX min js -->
18- <script src="/libs/htmx.min.js"></script>
19- <!-- HTMX SSE -->
20- <script src="/libs/htmx-sse.js"></script>
21- <!-- Custom JS from assets/js -->
22- <script src="assets/js/custom.js"></script>
23- </head>
24- <body class="IBMPlexSans bg-zinc-200 md:p-8 flex">
25- <!-- Left Menu -->
26- <div class="w-1/5 flex flex-col space-evenly">
27- <!-- Logo -->
28- <div id="logo-mascot-leftnav" class="flex justify-center p-4">
29- <div class="circle-shadow flex justify-center w-100">
30- <img
31- src="/assets/imgs/gordon-mascot-hq-trsp.png"
32- alt="logo"
33- class="sm:h-24 h-32 xl:h-48"
34- />
35- </div>
36- </div>
37- <!-- Site Name + Version -->
38- <div
39- class="p-4 flex flex-col bg-gordonmascot-lblue rounded-lg shadow space-y-4"
40- >
41- <div class="w-full flex justify-center">
42- <h1
43- class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter"
44- >
45- Gordon
46- </h1>
47- <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm"
48- >v0.0.1</span
49- >
50- </div>
51- <!--(Navigation) -->
52- <div id="menu-leftnav" class="w-full">
53- <ul
54- class="flex flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4"
55- >
56- <li class="flex items-center">
57- <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i>
58- <a href="index.html">Dashboard</a>
59- </li>
60- <li class="flex items-center">
61- <i class="iconoir-server mr-2 md:text-3xl"></i>
62- <a href="#manager">Manager</a>
63- </li>
64- <li class="flex items-center">
65- <i class="iconoir-community mr-2 md:text-3xl"></i>
66- <a href="#users">Users</a>
67- </li>
68- <li class="flex items-center">
69- <i class="iconoir-database-settings mr-2 md:text-3xl"></i>
70- <a href="#gordon-settings">Settings</a>
71- </li>
72- </ul>
73- </div>
74-
75- <div class="w-full h-24 bg-gordonmascot-mblue rounded-lg"></div>
76- <!-- Mock menu item -->
77- </div>
78- </div>
79-
80- <!-- Main Content including the navbar -->
81- <div class="flex-1 flex flex-col md:pt-10 space-y-4 ml-4">
82- <!-- Search Nav within the main content -->
83- <div class="mb-4 flex justify-end">
84- <div class="w-72 h-10 bg-gray-300 rounded-lg">
85- <!-- Search input with iconir search -->
86- <div class="flex justify-between items-center h-full px-2">
87- <input
88- type="text"
89- class="w-full h-full bg-transparent outline-none"
90- placeholder="Search"
91- />
92- <i class="iconoir-search"></i>
93- </div>
94- </div>
95- <!-- Mock search or other element -->
96- </div>
97-
98- <!-- Cards section -->
99- <div class="flex space-x-4 flex-grow">
100- <!-- Main Card on the Left -->
101- <div class="w-4/6 bg-white p-4 rounded-md shadow mb-4">
102- <div class="w-full rounded-sm bg-gray-300"></div>
103- <!-- Mock main card -->
104- </div>
105-
106- <!-- Two Cards on the Right -->
107- <div class="w-2/6 flex flex-col flex-justify-start">
108- <div class="bg-white p-4 rounded-md shadow mb-4">
109- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
110- <!-- Mock card -->
111- </div>
112- <div class="bg-white p-4 rounded-md shadow mb-4">
113- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
114- <!-- Mock card -->
115- </div>
116- <div class="bg-white p-4 rounded-md shadow mb-4">
117- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
118- <!-- Mock card -->
119- </div>
120- </div>
121- </div>
122- </div>
123- </body>
124-</html>
D internal/ui/public/libs/alpine.min.js +0 -5 generated file: collapsed
D internal/ui/public/libs/chart.js +0 -20 too large to show in the browser

too large to show in the browser

D internal/ui/public/libs/htmx-sse.js +0 -322
@@ -1,322 +0,0 @@
1-/*
2-Server Sent Events Extension
3-============================
4-This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
5-
6-*/
7-
8-(function(){
9-
10- /** @type {import("../htmx").HtmxInternalApi} */
11- var api;
12-
13- htmx.defineExtension("sse", {
14-
15- /**
16- * Init saves the provided reference to the internal HTMX API.
17- *
18- * @param {import("../htmx").HtmxInternalApi} api
19- * @returns void
20- */
21- init: function(apiRef) {
22- // store a reference to the internal API.
23- api = apiRef;
24-
25- // set a function in the public API for creating new EventSource objects
26- if (htmx.createEventSource == undefined) {
27- htmx.createEventSource = createEventSource;
28- }
29- },
30-
31- /**
32- * onEvent handles all events passed to this extension.
33- *
34- * @param {string} name
35- * @param {Event} evt
36- * @returns void
37- */
38- onEvent: function(name, evt) {
39-
40- switch (name) {
41-
42- // Try to remove remove an EventSource when elements are removed
43- case "htmx:beforeCleanupElement":
44- var internalData = api.getInternalData(evt.target)
45- if (internalData.sseEventSource) {
46- internalData.sseEventSource.close();
47- }
48- return;
49-
50- // Try to create EventSources when elements are processed
51- case "htmx:afterProcessNode":
52- createEventSourceOnElement(evt.target);
53- }
54- }
55- });
56-
57- ///////////////////////////////////////////////
58- // HELPER FUNCTIONS
59- ///////////////////////////////////////////////
60-
61-
62- /**
63- * createEventSource is the default method for creating new EventSource objects.
64- * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
65- *
66- * @param {string} url
67- * @returns EventSource
68- */
69- function createEventSource(url) {
70- return new EventSource(url, {withCredentials:true});
71- }
72-
73- function splitOnWhitespace(trigger) {
74- return trigger.trim().split(/\s+/);
75- }
76-
77- function getLegacySSEURL(elt) {
78- var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
79- if (legacySSEValue) {
80- var values = splitOnWhitespace(legacySSEValue);
81- for (var i = 0; i < values.length; i++) {
82- var value = values[i].split(/:(.+)/);
83- if (value[0] === "connect") {
84- return value[1];
85- }
86- }
87- }
88- }
89-
90- function getLegacySSESwaps(elt) {
91- var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
92- var returnArr = [];
93- if (legacySSEValue) {
94- var values = splitOnWhitespace(legacySSEValue);
95- for (var i = 0; i < values.length; i++) {
96- var value = values[i].split(/:(.+)/);
97- if (value[0] === "swap") {
98- returnArr.push(value[1]);
99- }
100- }
101- }
102- return returnArr;
103- }
104-
105- /**
106- * createEventSourceOnElement creates a new EventSource connection on the provided element.
107- * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
108- * is created and stored in the element's internalData.
109- * @param {HTMLElement} elt
110- * @param {number} retryCount
111- * @returns {EventSource | null}
112- */
113- function createEventSourceOnElement(elt, retryCount) {
114-
115- if (elt == null) {
116- return null;
117- }
118-
119- var internalData = api.getInternalData(elt);
120-
121- // get URL from element's attribute
122- var sseURL = api.getAttributeValue(elt, "sse-connect");
123-
124-
125- if (sseURL == undefined) {
126- var legacyURL = getLegacySSEURL(elt)
127- if (legacyURL) {
128- sseURL = legacyURL;
129- } else {
130- return null;
131- }
132- }
133-
134- // Connect to the EventSource
135- var source = htmx.createEventSource(sseURL);
136- internalData.sseEventSource = source;
137-
138- // Create event handlers
139- source.onerror = function (err) {
140-
141- // Log an error event
142- api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
143-
144- // If parent no longer exists in the document, then clean up this EventSource
145- if (maybeCloseSSESource(elt)) {
146- return;
147- }
148-
149- // Otherwise, try to reconnect the EventSource
150- if (source.readyState === EventSource.CLOSED) {
151- retryCount = retryCount || 0;
152- var timeout = Math.random() * (2 ^ retryCount) * 500;
153- window.setTimeout(function() {
154- createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155- }, timeout);
156- }
157- };
158-
159- source.onopen = function (evt) {
160- api.triggerEvent(elt, "htmx:sseOpen", {source: source});
161- }
162-
163- // Add message handlers for every `sse-swap` attribute
164- queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
165-
166- var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
167- if (sseSwapAttr) {
168- var sseEventNames = sseSwapAttr.split(",");
169- } else {
170- var sseEventNames = getLegacySSESwaps(child);
171- }
172-
173- for (var i = 0 ; i < sseEventNames.length ; i++) {
174- var sseEventName = sseEventNames[i].trim();
175- var listener = function(event) {
176-
177- // If the parent is missing then close SSE and remove listener
178- if (maybeCloseSSESource(elt)) {
179- source.removeEventListener(sseEventName, listener);
180- return;
181- }
182-
183- // swap the response into the DOM and trigger a notification
184- swap(child, event.data);
185- api.triggerEvent(elt, "htmx:sseMessage", event);
186- };
187-
188- // Register the new listener
189- api.getInternalData(elt).sseEventListener = listener;
190- source.addEventListener(sseEventName, listener);
191- }
192- });
193-
194- // Add message handlers for every `hx-trigger="sse:*"` attribute
195- queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
196-
197- var sseEventName = api.getAttributeValue(child, "hx-trigger");
198- if (sseEventName == null) {
199- return;
200- }
201-
202- // Only process hx-triggers for events with the "sse:" prefix
203- if (sseEventName.slice(0, 4) != "sse:") {
204- return;
205- }
206-
207- var listener = function(event) {
208-
209- // If parent is missing, then close SSE and remove listener
210- if (maybeCloseSSESource(elt)) {
211- source.removeEventListener(sseEventName, listener);
212- return;
213- }
214-
215- // Trigger events to be handled by the rest of htmx
216- htmx.trigger(child, sseEventName, event);
217- htmx.trigger(child, "htmx:sseMessage", event);
218- }
219-
220- // Register the new listener
221- api.getInternalData(elt).sseEventListener = listener;
222- source.addEventListener(sseEventName.slice(4), listener);
223- });
224- }
225-
226- /**
227- * maybeCloseSSESource confirms that the parent element still exists.
228- * If not, then any associated SSE source is closed and the function returns true.
229- *
230- * @param {HTMLElement} elt
231- * @returns boolean
232- */
233- function maybeCloseSSESource(elt) {
234- if (!api.bodyContains(elt)) {
235- var source = api.getInternalData(elt).sseEventSource;
236- if (source != undefined) {
237- source.close();
238- // source = null
239- return true;
240- }
241- }
242- return false;
243- }
244-
245- /**
246- * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
247- *
248- * @param {HTMLElement} elt
249- * @param {string} attributeName
250- */
251- function queryAttributeOnThisOrChildren(elt, attributeName) {
252-
253- var result = [];
254-
255- // If the parent element also contains the requested attribute, then add it to the results too.
256- if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
257- result.push(elt);
258- }
259-
260- // Search all child nodes that match the requested attribute
261- elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
262- result.push(node);
263- });
264-
265- return result;
266- }
267-
268- /**
269- * @param {HTMLElement} elt
270- * @param {string} content
271- */
272- function swap(elt, content) {
273-
274- api.withExtensions(elt, function(extension) {
275- content = extension.transformResponse(content, null, elt);
276- });
277-
278- var swapSpec = api.getSwapSpecification(elt);
279- var target = api.getTarget(elt);
280- var settleInfo = api.makeSettleInfo(elt);
281-
282- api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283-
284- settleInfo.elts.forEach(function (elt) {
285- if (elt.classList) {
286- elt.classList.add(htmx.config.settlingClass);
287- }
288- api.triggerEvent(elt, 'htmx:beforeSettle');
289- });
290-
291- // Handle settle tasks (with delay if requested)
292- if (swapSpec.settleDelay > 0) {
293- setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
294- } else {
295- doSettle(settleInfo)();
296- }
297- }
298-
299- /**
300- * doSettle mirrors much of the functionality in htmx that
301- * settles elements after their content has been swapped.
302- * TODO: this should be published by htmx, and not duplicated here
303- * @param {import("../htmx").HtmxSettleInfo} settleInfo
304- * @returns () => void
305- */
306- function doSettle(settleInfo) {
307-
308- return function() {
309- settleInfo.tasks.forEach(function (task) {
310- task.call();
311- });
312-
313- settleInfo.elts.forEach(function (elt) {
314- if (elt.classList) {
315- elt.classList.remove(htmx.config.settlingClass);
316- }
317- api.triggerEvent(elt, 'htmx:afterSettle');
318- });
319- }
320- }
321-
322-})();⏎̸
@@ -1,322 +0,0 @@
1-/*
2-Server Sent Events Extension
3-============================
4-This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
5-
6-*/
7-
8-(function(){
9-
10- /** @type {import("../htmx").HtmxInternalApi} */
11- var api;
12-
13- htmx.defineExtension("sse", {
14-
15- /**
16- * Init saves the provided reference to the internal HTMX API.
17- *
18- * @param {import("../htmx").HtmxInternalApi} api
19- * @returns void
20- */
21- init: function(apiRef) {
22- // store a reference to the internal API.
23- api = apiRef;
24-
25- // set a function in the public API for creating new EventSource objects
26- if (htmx.createEventSource == undefined) {
27- htmx.createEventSource = createEventSource;
28- }
29- },
30-
31- /**
32- * onEvent handles all events passed to this extension.
33- *
34- * @param {string} name
35- * @param {Event} evt
36- * @returns void
37- */
38- onEvent: function(name, evt) {
39-
40- switch (name) {
41-
42- // Try to remove remove an EventSource when elements are removed
43- case "htmx:beforeCleanupElement":
44- var internalData = api.getInternalData(evt.target)
45- if (internalData.sseEventSource) {
46- internalData.sseEventSource.close();
47- }
48- return;
49-
50- // Try to create EventSources when elements are processed
51- case "htmx:afterProcessNode":
52- createEventSourceOnElement(evt.target);
53- }
54- }
55- });
56-
57- ///////////////////////////////////////////////
58- // HELPER FUNCTIONS
59- ///////////////////////////////////////////////
60-
61-
62- /**
63- * createEventSource is the default method for creating new EventSource objects.
64- * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
65- *
66- * @param {string} url
67- * @returns EventSource
68- */
69- function createEventSource(url) {
70- return new EventSource(url, {withCredentials:true});
71- }
72-
73- function splitOnWhitespace(trigger) {
74- return trigger.trim().split(/\s+/);
75- }
76-
77- function getLegacySSEURL(elt) {
78- var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
79- if (legacySSEValue) {
80- var values = splitOnWhitespace(legacySSEValue);
81- for (var i = 0; i < values.length; i++) {
82- var value = values[i].split(/:(.+)/);
83- if (value[0] === "connect") {
84- return value[1];
85- }
86- }
87- }
88- }
89-
90- function getLegacySSESwaps(elt) {
91- var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
92- var returnArr = [];
93- if (legacySSEValue) {
94- var values = splitOnWhitespace(legacySSEValue);
95- for (var i = 0; i < values.length; i++) {
96- var value = values[i].split(/:(.+)/);
97- if (value[0] === "swap") {
98- returnArr.push(value[1]);
99- }
100- }
101- }
102- return returnArr;
103- }
104-
105- /**
106- * createEventSourceOnElement creates a new EventSource connection on the provided element.
107- * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
108- * is created and stored in the element's internalData.
109- * @param {HTMLElement} elt
110- * @param {number} retryCount
111- * @returns {EventSource | null}
112- */
113- function createEventSourceOnElement(elt, retryCount) {
114-
115- if (elt == null) {
116- return null;
117- }
118-
119- var internalData = api.getInternalData(elt);
120-
121- // get URL from element's attribute
122- var sseURL = api.getAttributeValue(elt, "sse-connect");
123-
124-
125- if (sseURL == undefined) {
126- var legacyURL = getLegacySSEURL(elt)
127- if (legacyURL) {
128- sseURL = legacyURL;
129- } else {
130- return null;
131- }
132- }
133-
134- // Connect to the EventSource
135- var source = htmx.createEventSource(sseURL);
136- internalData.sseEventSource = source;
137-
138- // Create event handlers
139- source.onerror = function (err) {
140-
141- // Log an error event
142- api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
143-
144- // If parent no longer exists in the document, then clean up this EventSource
145- if (maybeCloseSSESource(elt)) {
146- return;
147- }
148-
149- // Otherwise, try to reconnect the EventSource
150- if (source.readyState === EventSource.CLOSED) {
151- retryCount = retryCount || 0;
152- var timeout = Math.random() * (2 ^ retryCount) * 500;
153- window.setTimeout(function() {
154- createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155- }, timeout);
156- }
157- };
158-
159- source.onopen = function (evt) {
160- api.triggerEvent(elt, "htmx:sseOpen", {source: source});
161- }
162-
163- // Add message handlers for every `sse-swap` attribute
164- queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
165-
166- var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
167- if (sseSwapAttr) {
168- var sseEventNames = sseSwapAttr.split(",");
169- } else {
170- var sseEventNames = getLegacySSESwaps(child);
171- }
172-
173- for (var i = 0 ; i < sseEventNames.length ; i++) {
174- var sseEventName = sseEventNames[i].trim();
175- var listener = function(event) {
176-
177- // If the parent is missing then close SSE and remove listener
178- if (maybeCloseSSESource(elt)) {
179- source.removeEventListener(sseEventName, listener);
180- return;
181- }
182-
183- // swap the response into the DOM and trigger a notification
184- swap(child, event.data);
185- api.triggerEvent(elt, "htmx:sseMessage", event);
186- };
187-
188- // Register the new listener
189- api.getInternalData(elt).sseEventListener = listener;
190- source.addEventListener(sseEventName, listener);
191- }
192- });
193-
194- // Add message handlers for every `hx-trigger="sse:*"` attribute
195- queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
196-
197- var sseEventName = api.getAttributeValue(child, "hx-trigger");
198- if (sseEventName == null) {
199- return;
200- }
201-
202- // Only process hx-triggers for events with the "sse:" prefix
203- if (sseEventName.slice(0, 4) != "sse:") {
204- return;
205- }
206-
207- var listener = function(event) {
208-
209- // If parent is missing, then close SSE and remove listener
210- if (maybeCloseSSESource(elt)) {
211- source.removeEventListener(sseEventName, listener);
212- return;
213- }
214-
215- // Trigger events to be handled by the rest of htmx
216- htmx.trigger(child, sseEventName, event);
217- htmx.trigger(child, "htmx:sseMessage", event);
218- }
219-
220- // Register the new listener
221- api.getInternalData(elt).sseEventListener = listener;
222- source.addEventListener(sseEventName.slice(4), listener);
223- });
224- }
225-
226- /**
227- * maybeCloseSSESource confirms that the parent element still exists.
228- * If not, then any associated SSE source is closed and the function returns true.
229- *
230- * @param {HTMLElement} elt
231- * @returns boolean
232- */
233- function maybeCloseSSESource(elt) {
234- if (!api.bodyContains(elt)) {
235- var source = api.getInternalData(elt).sseEventSource;
236- if (source != undefined) {
237- source.close();
238- // source = null
239- return true;
240- }
241- }
242- return false;
243- }
244-
245- /**
246- * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
247- *
248- * @param {HTMLElement} elt
249- * @param {string} attributeName
250- */
251- function queryAttributeOnThisOrChildren(elt, attributeName) {
252-
253- var result = [];
254-
255- // If the parent element also contains the requested attribute, then add it to the results too.
256- if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
257- result.push(elt);
258- }
259-
260- // Search all child nodes that match the requested attribute
261- elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
262- result.push(node);
263- });
264-
265- return result;
266- }
267-
268- /**
269- * @param {HTMLElement} elt
270- * @param {string} content
271- */
272- function swap(elt, content) {
273-
274- api.withExtensions(elt, function(extension) {
275- content = extension.transformResponse(content, null, elt);
276- });
277-
278- var swapSpec = api.getSwapSpecification(elt);
279- var target = api.getTarget(elt);
280- var settleInfo = api.makeSettleInfo(elt);
281-
282- api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283-
284- settleInfo.elts.forEach(function (elt) {
285- if (elt.classList) {
286- elt.classList.add(htmx.config.settlingClass);
287- }
288- api.triggerEvent(elt, 'htmx:beforeSettle');
289- });
290-
291- // Handle settle tasks (with delay if requested)
292- if (swapSpec.settleDelay > 0) {
293- setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
294- } else {
295- doSettle(settleInfo)();
296- }
297- }
298-
299- /**
300- * doSettle mirrors much of the functionality in htmx that
301- * settles elements after their content has been swapped.
302- * TODO: this should be published by htmx, and not duplicated here
303- * @param {import("../htmx").HtmxSettleInfo} settleInfo
304- * @returns () => void
305- */
306- function doSettle(settleInfo) {
307-
308- return function() {
309- settleInfo.tasks.forEach(function (task) {
310- task.call();
311- });
312-
313- settleInfo.elts.forEach(function (elt) {
314- if (elt.classList) {
315- elt.classList.remove(htmx.config.settlingClass);
316- }
317- api.triggerEvent(elt, 'htmx:afterSettle');
318- });
319- }
320- }
321-
322-})();⏎̸
D internal/ui/public/libs/htmx.min.js +0 -1 generated file: collapsed
D internal/ui/public/libs/iconoir.css +0 -22 too large to show in the browser

too large to show in the browser

D internal/ui/public/site.webmanifest +0 -1
@@ -1,1 +0,0 @@
1-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}⏎̸
@@ -1,1 +0,0 @@
1-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}⏎̸
D internal/ui/templates/components.gohtml +0 -33
@@ -1,33 +0,0 @@
1-<!-- @format -->
2-
3-<div id="header">
4- <h1
5- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
6- >
7- {{ .body.header.helloworld }}
8- </h1>
9-</div>
10-
11-<div id="hello">
12- <h1
13- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
14- >
15- {{ .body.div.hello }}
16- </h1>
17-</div>
18-<div
19- id="header"
20- hx-get="/htmx"
21- hx-headers='{"HX-Fragment": "header"}'
22- hx-trigger="load"
23- hx-swap="outerHTML"
24-></div>
25-
26-<!-- App Fragment -->
27-<div
28- id="app"
29- hx-get="/htmx"
30- hx-headers='{"HX-Fragment": "hello"}'
31- hx-trigger="load"
32- hx-swap="outerHTML"
33-></div>
@@ -1,33 +0,0 @@
1-<!-- @format -->
2-
3-<div id="header">
4- <h1
5- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
6- >
7- {{ .body.header.helloworld }}
8- </h1>
9-</div>
10-
11-<div id="hello">
12- <h1
13- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
14- >
15- {{ .body.div.hello }}
16- </h1>
17-</div>
18-<div
19- id="header"
20- hx-get="/htmx"
21- hx-headers='{"HX-Fragment": "header"}'
22- hx-trigger="load"
23- hx-swap="outerHTML"
24-></div>
25-
26-<!-- App Fragment -->
27-<div
28- id="app"
29- hx-get="/htmx"
30- hx-headers='{"HX-Fragment": "hello"}'
31- hx-trigger="load"
32- hx-swap="outerHTML"
33-></div>
D internal/ui/templates/dashboard.gohtml +0 -9
@@ -1,9 +0,0 @@
1-<div id="dashboard">
2- <h1
3- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
4- >
5- Hello World from dashboard.html
6- </h1>
7- <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded -->
8- <script src="libs/chart.js"></script>
9-</div>
@@ -1,9 +0,0 @@
1-<div id="dashboard">
2- <h1
3- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
4- >
5- Hello World from dashboard.html
6- </h1>
7- <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded -->
8- <script src="libs/chart.js"></script>
9-</div>
D internal/ui/templates/install.gohtml +0 -137
@@ -1,137 +0,0 @@
1-<!DOCTYPE html>
2-<html lang="en">
3- <head>
4- <meta charset="UTF-8" />
5- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6- <title>{{ .header.meta.title }}</title>
7- <meta name="description" content="{{ .Header.Meta.Description }}" />
8- <meta name="keywords" content="{{ .Header.Meta.Keywords }}" />
9- <!-- Custom CSS + Tailwind -->
10- <link rel="stylesheet" href="/assets/css/tailwind.css" />
11- <!-- Iconoir -->
12- <link rel="stylesheet" href="/libs/iconoir.css" />
13- <!-- Alpine -->
14- <script src="/libs/alpine.min.js"></script>
15- <!-- HTMX min js -->
16- <script src="/libs/htmx.min.js"></script>
17- <!-- HTMX SSE -->
18- <script src="/libs/htmx-sse.js"></script>
19- <!-- Custom JS from assets/js -->
20- <script src="/assets/js/custom.js"></script>
21- </head>
22-
23- <body
24- class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center"
25- hx-boost="true"
26- >
27- <!-- Logo and Title -->
28- <div
29- id="logo-mascot"
30- class="flex flex-col justify-center items-center p-4 mb-4"
31- >
32- <div class="circle-shadow flex justify-center w-100">
33- <img
34- src="/assets/imgs/gordon-mascot-hq-trsp.png"
35- alt="logo"
36- class="sm:h-24 h-32 xl:h-48"
37- />
38- </div>
39- <div class="flex justify-center mt-4">
40- <h1
41- class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter"
42- >
43- Gordon
44- </h1>
45- <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm ml-2"
46- >v0.0.1</span
47- >
48- </div>
49- </div>
50-
51- <!-- Main Content -->
52- <div class="bg-white p-4 rounded-md shadow mb-4 w-2/3">
53- <!-- Install Traefik -->
54- <div id="install-traefik">
55- <div class="flex flex-col items-center">
56- <h2 class="text-2xl font-semibold text-gray-800">
57- 1 - Install Traefik
58- </h2>
59- <!-- Dual view left : install procedure / right : SSE log installation -->
60- <div class="flex flex-col md:flex-row justify-items-center w-full">
61- <!-- Left -->
62- <div class="w-full md:w-1/2 p-4">
63- <form
64- id="install-traefik-form"
65- action="/admin/install/traefik"
66- method="POST"
67- hx-post="/admin/install/traefik"
68- hx-swap="outerHTML"
69- >
70- <h2 class="">1.1 - Set your top domain name</h2>
71- <input
72- type="text"
73- name="topdomain"
74- id="topdomain"
75- class="w-full py-2 px-4 my-2 border border-gray-300 rounded-md"
76- placeholder="example.com"
77- />
78- <h2>1.2 - Set your admin email (a reachable one)</h2>
79- <input
80- type="text"
81- name="adminemail"
82- id="adminemail"
83- class="w-full p-2 border border-gray-300 rounded-md"
84- placeholder="admin@example.com"
85- />
86- <button
87- type="submit"
88- class="w-full p-2 bg-gordonmascot-hblue text-slate-100 rounded-md mt-4"
89- >
90- Save & Install Traefik
91- </button>
92- </form>
93- </div>
94- <!-- Right -->
95- <div class="w-full md:w-1/2 p-4 text-slate-100">
96- <div
97- class="flex flex-col items-center bg-gordonmascot-hblue rounded-md"
98- >
99- <div class="w-full h-64 overflow-y-scroll-auto p-4">
100- <div
101- class="flex flex-col space-y-2"
102- hx-ext="sse"
103- sse-connect="/admin/install/traefik/sse"
104- >
105- <!-- One item -->
106- <div class="flex flex-row items-center">
107- <span class="">[INFO]</span>
108- <span class="">Traefik installed</span>
109- </div>
110- <!-- One item -->
111- <div class="flex flex-row items-center">
112- <span class="">[INFO]</span>
113- <span class="ml-2">Traefik started</span>
114- </div>
115- <!-- One item -->
116- <div class="flex flex-row items-center">
117- <span class="">[INFO]</span>
118- <span class="ml-2"
119- >Traefik configuration file created</span
120- >
121- </div>
122- <!-- One item -->
123- <div class="flex flex-row items-center">
124- <span class="">[INFO]</span>
125- <span class="">Traefik configuration file updated</span>
126- </div>
127- </div>
128- </div>
129- </div>
130- </div>
131- </div>
132- </div>
133- </div>
134- <!-- End Install Traefik -->
135- </div>
136- </body>
137-</html>
@@ -1,137 +0,0 @@
1-<!DOCTYPE html>
2-<html lang="en">
3- <head>
4- <meta charset="UTF-8" />
5- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6- <title>{{ .header.meta.title }}</title>
7- <meta name="description" content="{{ .Header.Meta.Description }}" />
8- <meta name="keywords" content="{{ .Header.Meta.Keywords }}" />
9- <!-- Custom CSS + Tailwind -->
10- <link rel="stylesheet" href="/assets/css/tailwind.css" />
11- <!-- Iconoir -->
12- <link rel="stylesheet" href="/libs/iconoir.css" />
13- <!-- Alpine -->
14- <script src="/libs/alpine.min.js"></script>
15- <!-- HTMX min js -->
16- <script src="/libs/htmx.min.js"></script>
17- <!-- HTMX SSE -->
18- <script src="/libs/htmx-sse.js"></script>
19- <!-- Custom JS from assets/js -->
20- <script src="/assets/js/custom.js"></script>
21- </head>
22-
23- <body
24- class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center"
25- hx-boost="true"
26- >
27- <!-- Logo and Title -->
28- <div
29- id="logo-mascot"
30- class="flex flex-col justify-center items-center p-4 mb-4"
31- >
32- <div class="circle-shadow flex justify-center w-100">
33- <img
34- src="/assets/imgs/gordon-mascot-hq-trsp.png"
35- alt="logo"
36- class="sm:h-24 h-32 xl:h-48"
37- />
38- </div>
39- <div class="flex justify-center mt-4">
40- <h1
41- class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter"
42- >
43- Gordon
44- </h1>
45- <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm ml-2"
46- >v0.0.1</span
47- >
48- </div>
49- </div>
50-
51- <!-- Main Content -->
52- <div class="bg-white p-4 rounded-md shadow mb-4 w-2/3">
53- <!-- Install Traefik -->
54- <div id="install-traefik">
55- <div class="flex flex-col items-center">
56- <h2 class="text-2xl font-semibold text-gray-800">
57- 1 - Install Traefik
58- </h2>
59- <!-- Dual view left : install procedure / right : SSE log installation -->
60- <div class="flex flex-col md:flex-row justify-items-center w-full">
61- <!-- Left -->
62- <div class="w-full md:w-1/2 p-4">
63- <form
64- id="install-traefik-form"
65- action="/admin/install/traefik"
66- method="POST"
67- hx-post="/admin/install/traefik"
68- hx-swap="outerHTML"
69- >
70- <h2 class="">1.1 - Set your top domain name</h2>
71- <input
72- type="text"
73- name="topdomain"
74- id="topdomain"
75- class="w-full py-2 px-4 my-2 border border-gray-300 rounded-md"
76- placeholder="example.com"
77- />
78- <h2>1.2 - Set your admin email (a reachable one)</h2>
79- <input
80- type="text"
81- name="adminemail"
82- id="adminemail"
83- class="w-full p-2 border border-gray-300 rounded-md"
84- placeholder="admin@example.com"
85- />
86- <button
87- type="submit"
88- class="w-full p-2 bg-gordonmascot-hblue text-slate-100 rounded-md mt-4"
89- >
90- Save & Install Traefik
91- </button>
92- </form>
93- </div>
94- <!-- Right -->
95- <div class="w-full md:w-1/2 p-4 text-slate-100">
96- <div
97- class="flex flex-col items-center bg-gordonmascot-hblue rounded-md"
98- >
99- <div class="w-full h-64 overflow-y-scroll-auto p-4">
100- <div
101- class="flex flex-col space-y-2"
102- hx-ext="sse"
103- sse-connect="/admin/install/traefik/sse"
104- >
105- <!-- One item -->
106- <div class="flex flex-row items-center">
107- <span class="">[INFO]</span>
108- <span class="">Traefik installed</span>
109- </div>
110- <!-- One item -->
111- <div class="flex flex-row items-center">
112- <span class="">[INFO]</span>
113- <span class="ml-2">Traefik started</span>
114- </div>
115- <!-- One item -->
116- <div class="flex flex-row items-center">
117- <span class="">[INFO]</span>
118- <span class="ml-2"
119- >Traefik configuration file created</span
120- >
121- </div>
122- <!-- One item -->
123- <div class="flex flex-row items-center">
124- <span class="">[INFO]</span>
125- <span class="">Traefik configuration file updated</span>
126- </div>
127- </div>
128- </div>
129- </div>
130- </div>
131- </div>
132- </div>
133- </div>
134- <!-- End Install Traefik -->
135- </div>
136- </body>
137-</html>
D internal/ui/templates/manager.gohtml +0 -0

no textual change.

D internal/ui/templates/strings.yml +0 -23
@@ -1,23 +0,0 @@
1-en:
2- header:
3- meta:
4- title: Gordon - Admin Dashboard
5- description: Admin dashboard for Gordon
6- keywords: gordon, admin, dashboard
7- body:
8- header:
9- helloworld: Hello World!
10- div:
11- hello: Hello component loaded from a template!
12-
13-fr:
14- header:
15- meta:
16- title: Gordon - Tableau de bord administrateur
17- description: Tableau de bord administrateur pour Gordon
18- keywords: gordon, admin, dashboard
19- body:
20- header:
21- helloworld: Bonjour le monde!
22- div:
23- hello: Composant de salut chargé à partir d'un modèle!
@@ -1,23 +0,0 @@
1-en:
2- header:
3- meta:
4- title: Gordon - Admin Dashboard
5- description: Admin dashboard for Gordon
6- keywords: gordon, admin, dashboard
7- body:
8- header:
9- helloworld: Hello World!
10- div:
11- hello: Hello component loaded from a template!
12-
13-fr:
14- header:
15- meta:
16- title: Gordon - Tableau de bord administrateur
17- description: Tableau de bord administrateur pour Gordon
18- keywords: gordon, admin, dashboard
19- body:
20- header:
21- helloworld: Bonjour le monde!
22- div:
23- hello: Composant de salut chargé à partir d'un modèle!
D internal/ui/templates/user.gohtml +0 -0

no textual change.

D loop +0 -0

no textual change.

D pkg/docker/commands.go +0 -165
@@ -1,165 +0,0 @@
1-package docker
2-
3-import (
4- "bufio"
5- "bytes"
6- "context"
7- "errors"
8- "fmt"
9- "io"
10- "os"
11- "os/exec"
12- "path/filepath"
13-
14- "github.com/docker/docker/api/types"
15- "github.com/docker/docker/client"
16- "github.com/docker/docker/pkg/archive"
17- "gogs.bnema.dev/gordon-echo/internal/app"
18- "gogs.bnema.dev/gordon-echo/pkg/utils"
19-)
20-
21-// Use the App struct to access the logger
22-
23-func CreateImageFromDockerfile(a *app.App) error {
24- ctx := context.Background()
25- cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
26- if err != nil {
27- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to initialize Docker client: %v", err))
28- defer cli.Close()
29- }
30-
31- a.AppLogger.Log(utils.INFO, "Building image...")
32- buildCtx, err := archive.TarWithOptions("tmp/docker-test/", &archive.TarOptions{})
33- if err != nil {
34- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create build context: %v", err))
35- }
36- defer buildCtx.Close()
37-
38- a.AppLogger.Log(utils.INFO, "Building image...")
39- imageBuildResponse, err := cli.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{
40- Tags: []string{"gordon-echo:latest"},
41- })
42- if err != nil {
43- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to build image: %v", err))
44- }
45- defer imageBuildResponse.Body.Close()
46-
47- a.AppLogger.Log(utils.INFO, "Saving image to file...")
48-
49- imageTar, err := cli.ImageSave(ctx, []string{"gordon-echo:latest"})
50- if err != nil {
51- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image: %v", err))
52- }
53-
54- outputFile, err := os.Create("tmp/docker-test/gordon-echo.tar")
55- if err != nil {
56- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create output file: %v", err))
57- }
58- defer outputFile.Close()
59-
60- _, err = io.Copy(outputFile, imageTar)
61- if err != nil {
62- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image to file: %v", err))
63- }
64-
65- return nil
66-}
67-
68-func CreateNetwork(name string, a *app.App) error {
69- a.AppLogger.Log(utils.INFO, "Entering CreateNetwork function with name: "+name)
70-
71- ctx := context.Background()
72- cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
73- if err != nil {
74- errMsg := fmt.Sprintf("Failed to initialize Docker client: %v", err)
75- a.AppLogger.Log(utils.ERROR, errMsg)
76- return errors.New(errMsg)
77- }
78- defer cli.Close()
79-
80- a.AppLogger.Log(utils.INFO, "Attempting to create network...")
81- _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{
82- CheckDuplicate: true,
83- Driver: "bridge",
84- })
85- if err != nil {
86- errMsg := fmt.Sprintf("Failed to create network: %v", err)
87- a.AppLogger.Log(utils.ERROR, errMsg)
88- return errors.New(errMsg)
89- }
90-
91- a.AppLogger.Log(utils.INFO, "Successfully created network: "+name)
92- return nil
93-}
94-
95-// Check if a container exists
96-func ContainerExists(name string, a *app.App) error {
97- ctx := context.Background()
98- cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
99- if err != nil {
100- return err
101- }
102- defer cli.Close()
103-
104- fmt.Println("Checking if container exists...")
105- containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
106- if err != nil {
107- return err
108- }
109-
110- for _, container := range containers {
111- for _, containerName := range container.Names {
112- if containerName == "/"+name {
113- return errors.New("container already exists")
114- }
115- }
116- }
117-
118- return nil
119-}
120-
121-func CheckIfDockerIsRunning(a *app.App) error {
122- cmd := exec.Command("docker", "info")
123- if err := cmd.Run(); err != nil {
124- return errors.New("docker is not running")
125- }
126- return nil
127-}
128-
129-// CreateContainerFromComposeFile creates containers using docker compose based on the provided compose file.
130-func CreateContainerFromComposeFile(composeFilePath string, a *app.App) error {
131- a.AppLogger.Log(utils.INFO, "Executing CreateContainerFromComposeFile()")
132-
133- // Extract directory from the composeFilePath
134- dir := filepath.Dir(composeFilePath)
135-
136- // Change directory to the compose file directory
137- if err := os.Chdir(dir); err != nil {
138- return errors.New("failed to change directory: " + err.Error())
139- }
140-
141- // Create the command
142- cmd := exec.Command("docker", "compose", "up", "-d")
143-
144- // Capture standard output and error
145- var stdout, stderr bytes.Buffer
146- cmd.Stdout = &stdout
147- cmd.Stderr = &stderr
148-
149- // Execute the command
150- if err := cmd.Run(); err != nil {
151- // Log the captured standard output and error for better diagnostics
152- a.AppLogger.Log(utils.ERROR, "Docker Compose Output: "+stdout.String())
153- a.AppLogger.Log(utils.ERROR, "Docker Compose Error: "+stderr.String())
154- return errors.New("failed to execute docker compose command: " + err.Error())
155- }
156-
157- return nil
158-}
159-
160-func streamToLogger(r io.Reader, logger *utils.Logger, level utils.LogLevel) {
161- scanner := bufio.NewScanner(r)
162- for scanner.Scan() {
163- logger.Log(level, scanner.Text())
164- }
165-}
@@ -1,165 +0,0 @@
1-package docker
2-
3-import (
4- "bufio"
5- "bytes"
6- "context"
7- "errors"
8- "fmt"
9- "io"
10- "os"
11- "os/exec"
12- "path/filepath"
13-
14- "github.com/docker/docker/api/types"
15- "github.com/docker/docker/client"
16- "github.com/docker/docker/pkg/archive"
17- "gogs.bnema.dev/gordon-echo/internal/app"
18- "gogs.bnema.dev/gordon-echo/pkg/utils"
19-)
20-
21-// Use the App struct to access the logger
22-
23-func CreateImageFromDockerfile(a *app.App) error {
24- ctx := context.Background()
25- cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
26- if err != nil {
27- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to initialize Docker client: %v", err))
28- defer cli.Close()
29- }
30-
31- a.AppLogger.Log(utils.INFO, "Building image...")
32- buildCtx, err := archive.TarWithOptions("tmp/docker-test/", &archive.TarOptions{})
33- if err != nil {
34- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create build context: %v", err))
35- }
36- defer buildCtx.Close()
37-
38- a.AppLogger.Log(utils.INFO, "Building image...")
39- imageBuildResponse, err := cli.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{
40- Tags: []string{"gordon-echo:latest"},
41- })
42- if err != nil {
43- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to build image: %v", err))
44- }
45- defer imageBuildResponse.Body.Close()
46-
47- a.AppLogger.Log(utils.INFO, "Saving image to file...")
48-
49- imageTar, err := cli.ImageSave(ctx, []string{"gordon-echo:latest"})
50- if err != nil {
51- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image: %v", err))
52- }
53-
54- outputFile, err := os.Create("tmp/docker-test/gordon-echo.tar")
55- if err != nil {
56- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to create output file: %v", err))
57- }
58- defer outputFile.Close()
59-
60- _, err = io.Copy(outputFile, imageTar)
61- if err != nil {
62- a.AppLogger.Log(utils.ERROR, fmt.Sprintf("Failed to save image to file: %v", err))
63- }
64-
65- return nil
66-}
67-
68-func CreateNetwork(name string, a *app.App) error {
69- a.AppLogger.Log(utils.INFO, "Entering CreateNetwork function with name: "+name)
70-
71- ctx := context.Background()
72- cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
73- if err != nil {
74- errMsg := fmt.Sprintf("Failed to initialize Docker client: %v", err)
75- a.AppLogger.Log(utils.ERROR, errMsg)
76- return errors.New(errMsg)
77- }
78- defer cli.Close()
79-
80- a.AppLogger.Log(utils.INFO, "Attempting to create network...")
81- _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{
82- CheckDuplicate: true,
83- Driver: "bridge",
84- })
85- if err != nil {
86- errMsg := fmt.Sprintf("Failed to create network: %v", err)
87- a.AppLogger.Log(utils.ERROR, errMsg)
88- return errors.New(errMsg)
89- }
90-
91- a.AppLogger.Log(utils.INFO, "Successfully created network: "+name)
92- return nil
93-}
94-
95-// Check if a container exists
96-func ContainerExists(name string, a *app.App) error {
97- ctx := context.Background()
98- cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
99- if err != nil {
100- return err
101- }
102- defer cli.Close()
103-
104- fmt.Println("Checking if container exists...")
105- containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
106- if err != nil {
107- return err
108- }
109-
110- for _, container := range containers {
111- for _, containerName := range container.Names {
112- if containerName == "/"+name {
113- return errors.New("container already exists")
114- }
115- }
116- }
117-
118- return nil
119-}
120-
121-func CheckIfDockerIsRunning(a *app.App) error {
122- cmd := exec.Command("docker", "info")
123- if err := cmd.Run(); err != nil {
124- return errors.New("docker is not running")
125- }
126- return nil
127-}
128-
129-// CreateContainerFromComposeFile creates containers using docker compose based on the provided compose file.
130-func CreateContainerFromComposeFile(composeFilePath string, a *app.App) error {
131- a.AppLogger.Log(utils.INFO, "Executing CreateContainerFromComposeFile()")
132-
133- // Extract directory from the composeFilePath
134- dir := filepath.Dir(composeFilePath)
135-
136- // Change directory to the compose file directory
137- if err := os.Chdir(dir); err != nil {
138- return errors.New("failed to change directory: " + err.Error())
139- }
140-
141- // Create the command
142- cmd := exec.Command("docker", "compose", "up", "-d")
143-
144- // Capture standard output and error
145- var stdout, stderr bytes.Buffer
146- cmd.Stdout = &stdout
147- cmd.Stderr = &stderr
148-
149- // Execute the command
150- if err := cmd.Run(); err != nil {
151- // Log the captured standard output and error for better diagnostics
152- a.AppLogger.Log(utils.ERROR, "Docker Compose Output: "+stdout.String())
153- a.AppLogger.Log(utils.ERROR, "Docker Compose Error: "+stderr.String())
154- return errors.New("failed to execute docker compose command: " + err.Error())
155- }
156-
157- return nil
158-}
159-
160-func streamToLogger(r io.Reader, logger *utils.Logger, level utils.LogLevel) {
161- scanner := bufio.NewScanner(r)
162- for scanner.Scan() {
163- logger.Log(level, scanner.Text())
164- }
165-}
D pkg/docker/daemon.json +0 -11
@@ -1,11 +0,0 @@
1-{
2- "log-driver": "json-file",
3- "log-opts": {
4- "max-size": "100m",
5- "max-file": "5"
6- },
7- "features": {
8- "buildkit": true
9- },
10- "live-restore": true
11-}
@@ -1,11 +0,0 @@
1-{
2- "log-driver": "json-file",
3- "log-opts": {
4- "max-size": "100m",
5- "max-file": "5"
6- },
7- "features": {
8- "buildkit": true
9- },
10- "live-restore": true
11-}
D pkg/htmx/html-fragments.go +0 -41
@@ -1,41 +0,0 @@
1-package htmx
2-
3-import (
4- "bytes"
5-
6- "github.com/PuerkitoBio/goquery"
7- "gogs.bnema.dev/gordon-echo/internal/app"
8- "gogs.bnema.dev/gordon-echo/pkg/templating"
9-)
10-
11-// GetHTMLFragmentByID returns the HTML fragment with the specified id
12-func GetHTMLFragmentByID(id string, data interface{}, a *app.App, ac *app.Config) (string, error) {
13- // 1. Render the template
14- renderer, err := templating.GetHTMLRenderer("components.gohtml", ac.GetTemplateFS(), a)
15- if err != nil {
16- return "", err
17- }
18-
19- renderedHTML, err := renderer.Render(data, a)
20- if err != nil {
21- return "", err
22- }
23-
24- // 2. Parse the rendered HTML with goquery
25- doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML)))
26- if err != nil {
27- return "", err
28- }
29-
30- // 3. Find the div with the specified id
31- fragmentContent, err := doc.Find("#" + id).Html()
32- if err != nil {
33- return "", err
34- }
35-
36- if fragmentContent == "" {
37- return "", err
38- }
39-
40- return fragmentContent, nil
41-}
@@ -1,41 +0,0 @@
1-package htmx
2-
3-import (
4- "bytes"
5-
6- "github.com/PuerkitoBio/goquery"
7- "gogs.bnema.dev/gordon-echo/internal/app"
8- "gogs.bnema.dev/gordon-echo/pkg/templating"
9-)
10-
11-// GetHTMLFragmentByID returns the HTML fragment with the specified id
12-func GetHTMLFragmentByID(id string, data interface{}, a *app.App, ac *app.Config) (string, error) {
13- // 1. Render the template
14- renderer, err := templating.GetHTMLRenderer("components.gohtml", ac.GetTemplateFS(), a)
15- if err != nil {
16- return "", err
17- }
18-
19- renderedHTML, err := renderer.Render(data, a)
20- if err != nil {
21- return "", err
22- }
23-
24- // 2. Parse the rendered HTML with goquery
25- doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML)))
26- if err != nil {
27- return "", err
28- }
29-
30- // 3. Find the div with the specified id
31- fragmentContent, err := doc.Find("#" + id).Html()
32- if err != nil {
33- return "", err
34- }
35-
36- if fragmentContent == "" {
37- return "", err
38- }
39-
40- return fragmentContent, nil
41-}
D pkg/htmx/htmx.go +0 -88
@@ -1,88 +0,0 @@
1-package htmx
2-
3-import (
4- "net/http"
5-
6- "github.com/labstack/echo/v4"
7-)
8-
9-// Headers (https://htmx.org/docs/#requests)
10-const (
11- HeaderRequest = "HX-Request"
12- HeaderBoosted = "HX-Boosted"
13- HeaderTrigger = "HX-Trigger"
14- HeaderTriggerName = "HX-Trigger-Name"
15- HeaderTriggerAfterSwap = "HX-Trigger-After-Swap"
16- HeaderTriggerAfterSettle = "HX-Trigger-After-Settle"
17- HeaderTarget = "HX-Target"
18- HeaderPrompt = "HX-Prompt"
19- HeaderPush = "HX-Push"
20- HeaderRedirect = "HX-Redirect"
21- HeaderRefresh = "HX-Refresh"
22- // Custom header
23- HeaderFragment = "HX-Fragment"
24-)
25-
26-type (
27- // Request contains data that HTMX provides during requests
28- Request struct {
29- Enabled bool
30- Boosted bool
31- Trigger string
32- TriggerName string
33- Target string
34- Prompt string
35- }
36-
37- // Response contain data that the server can communicate back to HTMX
38- Response struct {
39- Push string
40- Redirect string
41- Refresh bool
42- Trigger string
43- TriggerAfterSwap string
44- TriggerAfterSettle string
45- NoContent bool
46- }
47-)
48-
49-// GetRequest extracts HTMX data from the request
50-func GetRequest(ctx echo.Context) (Request, error) {
51- request := Request{
52- Enabled: ctx.Request().Header.Get(HeaderRequest) == "true",
53- Boosted: ctx.Request().Header.Get(HeaderBoosted) == "true",
54- Trigger: ctx.Request().Header.Get(HeaderTrigger),
55- TriggerName: ctx.Request().Header.Get(HeaderTriggerName),
56- Target: ctx.Request().Header.Get(HeaderTarget),
57- Prompt: ctx.Request().Header.Get(HeaderPrompt),
58- }
59-
60- return request, nil
61-}
62-
63-// Apply applies data from a Response to a server response
64-func (r Response) Apply(ctx echo.Context) error {
65- if r.Push != "" {
66- ctx.Response().Header().Set(HeaderPush, r.Push)
67- }
68- if r.Redirect != "" {
69- ctx.Response().Header().Set(HeaderRedirect, r.Redirect)
70- }
71- if r.Refresh {
72- ctx.Response().Header().Set(HeaderRefresh, "true")
73- }
74- if r.Trigger != "" {
75- ctx.Response().Header().Set(HeaderTrigger, r.Trigger)
76- }
77- if r.TriggerAfterSwap != "" {
78- ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap)
79- }
80- if r.TriggerAfterSettle != "" {
81- ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle)
82- }
83- if r.NoContent {
84- ctx.Response().Status = http.StatusNoContent
85- }
86-
87- return nil
88-}
@@ -1,88 +0,0 @@
1-package htmx
2-
3-import (
4- "net/http"
5-
6- "github.com/labstack/echo/v4"
7-)
8-
9-// Headers (https://htmx.org/docs/#requests)
10-const (
11- HeaderRequest = "HX-Request"
12- HeaderBoosted = "HX-Boosted"
13- HeaderTrigger = "HX-Trigger"
14- HeaderTriggerName = "HX-Trigger-Name"
15- HeaderTriggerAfterSwap = "HX-Trigger-After-Swap"
16- HeaderTriggerAfterSettle = "HX-Trigger-After-Settle"
17- HeaderTarget = "HX-Target"
18- HeaderPrompt = "HX-Prompt"
19- HeaderPush = "HX-Push"
20- HeaderRedirect = "HX-Redirect"
21- HeaderRefresh = "HX-Refresh"
22- // Custom header
23- HeaderFragment = "HX-Fragment"
24-)
25-
26-type (
27- // Request contains data that HTMX provides during requests
28- Request struct {
29- Enabled bool
30- Boosted bool
31- Trigger string
32- TriggerName string
33- Target string
34- Prompt string
35- }
36-
37- // Response contain data that the server can communicate back to HTMX
38- Response struct {
39- Push string
40- Redirect string
41- Refresh bool
42- Trigger string
43- TriggerAfterSwap string
44- TriggerAfterSettle string
45- NoContent bool
46- }
47-)
48-
49-// GetRequest extracts HTMX data from the request
50-func GetRequest(ctx echo.Context) (Request, error) {
51- request := Request{
52- Enabled: ctx.Request().Header.Get(HeaderRequest) == "true",
53- Boosted: ctx.Request().Header.Get(HeaderBoosted) == "true",
54- Trigger: ctx.Request().Header.Get(HeaderTrigger),
55- TriggerName: ctx.Request().Header.Get(HeaderTriggerName),
56- Target: ctx.Request().Header.Get(HeaderTarget),
57- Prompt: ctx.Request().Header.Get(HeaderPrompt),
58- }
59-
60- return request, nil
61-}
62-
63-// Apply applies data from a Response to a server response
64-func (r Response) Apply(ctx echo.Context) error {
65- if r.Push != "" {
66- ctx.Response().Header().Set(HeaderPush, r.Push)
67- }
68- if r.Redirect != "" {
69- ctx.Response().Header().Set(HeaderRedirect, r.Redirect)
70- }
71- if r.Refresh {
72- ctx.Response().Header().Set(HeaderRefresh, "true")
73- }
74- if r.Trigger != "" {
75- ctx.Response().Header().Set(HeaderTrigger, r.Trigger)
76- }
77- if r.TriggerAfterSwap != "" {
78- ctx.Response().Header().Set(HeaderTriggerAfterSwap, r.TriggerAfterSwap)
79- }
80- if r.TriggerAfterSettle != "" {
81- ctx.Response().Header().Set(HeaderTriggerAfterSettle, r.TriggerAfterSettle)
82- }
83- if r.NoContent {
84- ctx.Response().Status = http.StatusNoContent
85- }
86-
87- return nil
88-}
D pkg/scripts/install-traefik.go +0 -226
@@ -1,226 +0,0 @@
1-package scripts
2-
3-import (
4- "errors"
5- "fmt"
6- "os"
7-
8- root "gogs.bnema.dev/gordon-echo"
9- "gogs.bnema.dev/gordon-echo/internal/app"
10- "gogs.bnema.dev/gordon-echo/pkg/docker"
11- "gogs.bnema.dev/gordon-echo/pkg/templating"
12- "gogs.bnema.dev/gordon-echo/pkg/utils"
13-)
14-
15-var path string
16-
17-const (
18- traefikPath = "traefik"
19- traefikVolumePath = "traefik/volume"
20- traefikLog = "logs/traefik.log"
21- acmeJSON = "traefik/volume/acme.json"
22- networkName = "totototo"
23-)
24-
25-func init() {
26- path = app.GetBuildDir() + "/"
27-}
28-
29-type InstallationResult struct {
30- Success bool
31- Message string
32- Err error
33-}
34-
35-func InstallTraefik(topDomain, adminEmail string, a *app.App) (bool, error) {
36- // 1 - Files/directories creation
37- // 1.1 - Check if directory traefik exists, if not create it
38- if err := createDirectory(path + traefikPath); err != nil {
39- a.AppLogger.Log(utils.ERROR, err)
40- return false, err
41- } else {
42- fmt.Println("traefik directory created")
43- }
44-
45- // 1.2 - Check if directory traefik/volume exists, if not create it
46- if err := createDirectory(path + traefikVolumePath); err != nil {
47- a.AppLogger.Log(utils.ERROR, err)
48- return false, err
49- } else {
50- fmt.Println("traefik/volume directory created")
51- }
52-
53- // 1.3 - touch traefik/traefik.log
54- if err := createTraefikLogFile(path + traefikLog); err != nil {
55- a.AppLogger.Log(utils.ERROR, err)
56- return false, err
57- } else {
58- a.AppLogger.Log(utils.INFO, "Traefik log file created")
59- }
60-
61- // 1.4 - touch traefik/volume/acme.json
62- if err := createACMEfile(path + acmeJSON); err != nil {
63- a.AppLogger.Log(utils.ERROR, err)
64- return false, err
65- }
66-
67- a.AppLogger.Log(utils.INFO, "Traefik acme.json file created")
68-
69- // 2 - Render the yml files
70- // 2.1 - Render traefik-compose.yml
71- traefikComposeContent, err := renderTemplate("traefik-compose.goyml", topDomain, adminEmail, a.AppLogger)
72- if err != nil {
73- a.AppLogger.Log(utils.ERROR, err)
74- return false, err
75- }
76-
77- a.AppLogger.Log(utils.INFO, "Traefik compose file rendered")
78-
79- // 2.2 - Save traefik-compose.yml
80- if err := saveFile(path+"docker-compose.yml", traefikComposeContent); err != nil {
81- a.AppLogger.Log(utils.ERROR, err)
82- return false, err
83- } else {
84- fmt.Println("docker-compose.yml saved")
85- }
86-
87- // 2.3 - Render traefik.yml
88- traefikContent, err := renderTemplate("traefik.goyml", topDomain, adminEmail, a.AppLogger)
89- if err != nil {
90- a.AppLogger.Log(utils.ERROR, err)
91- return false, err
92- }
93-
94- a.AppLogger.Log(utils.INFO, "Traefik file rendered")
95-
96- // 2.4 - Save traefik.yml
97- if err := saveFile(path+"traefik.yml", traefikContent); err != nil {
98- a.AppLogger.Log(utils.ERROR, err)
99- return false, err
100- }
101-
102- a.AppLogger.Log(utils.INFO, "Traefik file saved")
103-
104- // 3 - Check if traefik is running
105- if err := checkIfTraefikIsRunning(a); err != nil {
106- a.AppLogger.Log(utils.ERROR, err)
107- return false, err
108- }
109-
110- // // 4 - Create network
111- // if err := docker.CreateNetwork(networkName, a); err != nil {
112- // a.AppLogger.Log(utils.ERROR, err)
113- // return false, err
114- // }
115-
116- // 5 - Create traefik container
117- if err := createTraefikContainer(path+"docker-compose.yml", a); err != nil {
118- a.AppLogger.Log(utils.ERROR, err)
119- return false, err
120- }
121-
122- // 6 - Ping traefik
123- if err := pingWithCURL(topDomain); err != nil {
124- a.AppLogger.Log(utils.ERROR, err)
125- return false, err
126- }
127-
128- return true, nil
129-}
130-
131-func renderTemplate(filename, topDomain, adminEmail string, logger *utils.Logger) (string, error) {
132- modelFS := root.ModelFS
133- if modelFS == nil {
134- return "", errors.New("modelFS is nil")
135- }
136-
137- renderer, err := templating.GetTXTRenderer(filename, modelFS, logger)
138- if err != nil {
139- return "", err
140- }
141-
142- var contentData map[string]interface{}
143- switch filename {
144- case "traefik-compose.goyml":
145- contentData = map[string]interface{}{"topdomain": topDomain}
146- case "traefik.goyml":
147- contentData = map[string]interface{}{
148- "admin": map[string]string{
149- "email": adminEmail,
150- },
151- }
152- default:
153- return "", errors.New("invalid template filename")
154- }
155-
156- return renderer.TXTRender(contentData)
157-}
158-
159-func createDirectory(dir string) error {
160- if _, err := os.Stat(dir); os.IsNotExist(err) {
161- return os.Mkdir(dir, 0755)
162- }
163- return nil
164-}
165-func saveFile(filePath, content string) error {
166- f, err := os.Create(filePath)
167- if err != nil {
168- return err
169- }
170- defer f.Close()
171- _, err = f.WriteString(content)
172- if err != nil {
173- fmt.Println("Error while writing to file: ", err)
174- return err
175- }
176- return err
177-}
178-
179-func closeFile(f *os.File) error {
180- // defer f.Close()
181- return f.Close()
182-}
183-
184-func createACMEfile(filePath string) error {
185- f, err := os.Create(filePath)
186- if err != nil {
187- return err
188- }
189- defer f.Close()
190- return f.Chmod(0600)
191-}
192-
193-func createTraefikLogFile(filePath string) error {
194- f, err := os.Create(filePath)
195- if err != nil {
196- return err
197- }
198- defer f.Close()
199- return f.Chmod(0600)
200-}
201-
202-func checkIfTraefikIsRunning(a *app.App) error {
203- // Check if docker is running
204- if err := docker.CheckIfDockerIsRunning(a); err != nil {
205- return err
206- }
207- // Check if a container named traefik already exists
208- if err := docker.ContainerExists("traefik", a); err != nil {
209- return err
210- }
211-
212- return nil
213-}
214-
215-func createTraefikContainer(filePath string, a *app.App) error {
216- err := docker.CreateContainerFromComposeFile(filePath, a)
217- if err != nil {
218- return err
219- }
220-
221- return nil
222-}
223-
224-func pingWithCURL(topDomain string) error {
225- return nil
226-}
@@ -1,226 +0,0 @@
1-package scripts
2-
3-import (
4- "errors"
5- "fmt"
6- "os"
7-
8- root "gogs.bnema.dev/gordon-echo"
9- "gogs.bnema.dev/gordon-echo/internal/app"
10- "gogs.bnema.dev/gordon-echo/pkg/docker"
11- "gogs.bnema.dev/gordon-echo/pkg/templating"
12- "gogs.bnema.dev/gordon-echo/pkg/utils"
13-)
14-
15-var path string
16-
17-const (
18- traefikPath = "traefik"
19- traefikVolumePath = "traefik/volume"
20- traefikLog = "logs/traefik.log"
21- acmeJSON = "traefik/volume/acme.json"
22- networkName = "totototo"
23-)
24-
25-func init() {
26- path = app.GetBuildDir() + "/"
27-}
28-
29-type InstallationResult struct {
30- Success bool
31- Message string
32- Err error
33-}
34-
35-func InstallTraefik(topDomain, adminEmail string, a *app.App) (bool, error) {
36- // 1 - Files/directories creation
37- // 1.1 - Check if directory traefik exists, if not create it
38- if err := createDirectory(path + traefikPath); err != nil {
39- a.AppLogger.Log(utils.ERROR, err)
40- return false, err
41- } else {
42- fmt.Println("traefik directory created")
43- }
44-
45- // 1.2 - Check if directory traefik/volume exists, if not create it
46- if err := createDirectory(path + traefikVolumePath); err != nil {
47- a.AppLogger.Log(utils.ERROR, err)
48- return false, err
49- } else {
50- fmt.Println("traefik/volume directory created")
51- }
52-
53- // 1.3 - touch traefik/traefik.log
54- if err := createTraefikLogFile(path + traefikLog); err != nil {
55- a.AppLogger.Log(utils.ERROR, err)
56- return false, err
57- } else {
58- a.AppLogger.Log(utils.INFO, "Traefik log file created")
59- }
60-
61- // 1.4 - touch traefik/volume/acme.json
62- if err := createACMEfile(path + acmeJSON); err != nil {
63- a.AppLogger.Log(utils.ERROR, err)
64- return false, err
65- }
66-
67- a.AppLogger.Log(utils.INFO, "Traefik acme.json file created")
68-
69- // 2 - Render the yml files
70- // 2.1 - Render traefik-compose.yml
71- traefikComposeContent, err := renderTemplate("traefik-compose.goyml", topDomain, adminEmail, a.AppLogger)
72- if err != nil {
73- a.AppLogger.Log(utils.ERROR, err)
74- return false, err
75- }
76-
77- a.AppLogger.Log(utils.INFO, "Traefik compose file rendered")
78-
79- // 2.2 - Save traefik-compose.yml
80- if err := saveFile(path+"docker-compose.yml", traefikComposeContent); err != nil {
81- a.AppLogger.Log(utils.ERROR, err)
82- return false, err
83- } else {
84- fmt.Println("docker-compose.yml saved")
85- }
86-
87- // 2.3 - Render traefik.yml
88- traefikContent, err := renderTemplate("traefik.goyml", topDomain, adminEmail, a.AppLogger)
89- if err != nil {
90- a.AppLogger.Log(utils.ERROR, err)
91- return false, err
92- }
93-
94- a.AppLogger.Log(utils.INFO, "Traefik file rendered")
95-
96- // 2.4 - Save traefik.yml
97- if err := saveFile(path+"traefik.yml", traefikContent); err != nil {
98- a.AppLogger.Log(utils.ERROR, err)
99- return false, err
100- }
101-
102- a.AppLogger.Log(utils.INFO, "Traefik file saved")
103-
104- // 3 - Check if traefik is running
105- if err := checkIfTraefikIsRunning(a); err != nil {
106- a.AppLogger.Log(utils.ERROR, err)
107- return false, err
108- }
109-
110- // // 4 - Create network
111- // if err := docker.CreateNetwork(networkName, a); err != nil {
112- // a.AppLogger.Log(utils.ERROR, err)
113- // return false, err
114- // }
115-
116- // 5 - Create traefik container
117- if err := createTraefikContainer(path+"docker-compose.yml", a); err != nil {
118- a.AppLogger.Log(utils.ERROR, err)
119- return false, err
120- }
121-
122- // 6 - Ping traefik
123- if err := pingWithCURL(topDomain); err != nil {
124- a.AppLogger.Log(utils.ERROR, err)
125- return false, err
126- }
127-
128- return true, nil
129-}
130-
131-func renderTemplate(filename, topDomain, adminEmail string, logger *utils.Logger) (string, error) {
132- modelFS := root.ModelFS
133- if modelFS == nil {
134- return "", errors.New("modelFS is nil")
135- }
136-
137- renderer, err := templating.GetTXTRenderer(filename, modelFS, logger)
138- if err != nil {
139- return "", err
140- }
141-
142- var contentData map[string]interface{}
143- switch filename {
144- case "traefik-compose.goyml":
145- contentData = map[string]interface{}{"topdomain": topDomain}
146- case "traefik.goyml":
147- contentData = map[string]interface{}{
148- "admin": map[string]string{
149- "email": adminEmail,
150- },
151- }
152- default:
153- return "", errors.New("invalid template filename")
154- }
155-
156- return renderer.TXTRender(contentData)
157-}
158-
159-func createDirectory(dir string) error {
160- if _, err := os.Stat(dir); os.IsNotExist(err) {
161- return os.Mkdir(dir, 0755)
162- }
163- return nil
164-}
165-func saveFile(filePath, content string) error {
166- f, err := os.Create(filePath)
167- if err != nil {
168- return err
169- }
170- defer f.Close()
171- _, err = f.WriteString(content)
172- if err != nil {
173- fmt.Println("Error while writing to file: ", err)
174- return err
175- }
176- return err
177-}
178-
179-func closeFile(f *os.File) error {
180- // defer f.Close()
181- return f.Close()
182-}
183-
184-func createACMEfile(filePath string) error {
185- f, err := os.Create(filePath)
186- if err != nil {
187- return err
188- }
189- defer f.Close()
190- return f.Chmod(0600)
191-}
192-
193-func createTraefikLogFile(filePath string) error {
194- f, err := os.Create(filePath)
195- if err != nil {
196- return err
197- }
198- defer f.Close()
199- return f.Chmod(0600)
200-}
201-
202-func checkIfTraefikIsRunning(a *app.App) error {
203- // Check if docker is running
204- if err := docker.CheckIfDockerIsRunning(a); err != nil {
205- return err
206- }
207- // Check if a container named traefik already exists
208- if err := docker.ContainerExists("traefik", a); err != nil {
209- return err
210- }
211-
212- return nil
213-}
214-
215-func createTraefikContainer(filePath string, a *app.App) error {
216- err := docker.CreateContainerFromComposeFile(filePath, a)
217- if err != nil {
218- return err
219- }
220-
221- return nil
222-}
223-
224-func pingWithCURL(topDomain string) error {
225- return nil
226-}
D pkg/services/sqlite.go +0 -1
@@ -1,1 +0,0 @@
1-package services
@@ -1,1 +0,0 @@
1-package services
D pkg/templating/html-render.go +0 -54
@@ -1,54 +0,0 @@
1-package templating
2-
3-import (
4- "bytes"
5- "html/template"
6- "io/fs"
7-
8- "gogs.bnema.dev/gordon-echo/internal/app"
9-)
10-
11-type Renderer struct {
12- Template *template.Template
13- ParseError error
14-}
15-
16-// Render function renders the template with the given data
17-func (r *Renderer) Render(data interface{}, a *app.App) (string, error) {
18- if r.ParseError != nil {
19- a.AppLogger.Error().Err(r.ParseError).Msg("Failed to parse template")
20- return "", r.ParseError
21- }
22- if r.Template == nil {
23- a.AppLogger.Error().Msg("Template is nil")
24- return "", r.ParseError
25- }
26-
27- buf := new(bytes.Buffer)
28- err := r.Template.Execute(buf, data)
29- if err != nil {
30- a.AppLogger.Error().Err(err).Msg("Failed to execute template")
31- return "", err
32- }
33-
34- return buf.String(), nil
35-}
36-
37-// GetRenderer function returns a new Renderer instance
38-func GetHTMLRenderer(filename string, fs fs.FS, a *app.App) (*Renderer, error) {
39- // Check if the file exists in the provided fs.FS using fs.Open
40- file, err := fs.Open(filename)
41- if err != nil {
42- a.AppLogger.Error().Err(err).Msg("Failed to open file")
43- return nil, err
44- }
45- file.Close() // Close the file after checking
46- tmpl, err := template.New(filename).ParseFS(fs, filename)
47- if err != nil {
48- a.AppLogger.Error().Err(err).Msg("Failed to parse template")
49- return nil, err
50- }
51- return &Renderer{
52- Template: tmpl,
53- }, nil
54-}
@@ -1,54 +0,0 @@
1-package templating
2-
3-import (
4- "bytes"
5- "html/template"
6- "io/fs"
7-
8- "gogs.bnema.dev/gordon-echo/internal/app"
9-)
10-
11-type Renderer struct {
12- Template *template.Template
13- ParseError error
14-}
15-
16-// Render function renders the template with the given data
17-func (r *Renderer) Render(data interface{}, a *app.App) (string, error) {
18- if r.ParseError != nil {
19- a.AppLogger.Error().Err(r.ParseError).Msg("Failed to parse template")
20- return "", r.ParseError
21- }
22- if r.Template == nil {
23- a.AppLogger.Error().Msg("Template is nil")
24- return "", r.ParseError
25- }
26-
27- buf := new(bytes.Buffer)
28- err := r.Template.Execute(buf, data)
29- if err != nil {
30- a.AppLogger.Error().Err(err).Msg("Failed to execute template")
31- return "", err
32- }
33-
34- return buf.String(), nil
35-}
36-
37-// GetRenderer function returns a new Renderer instance
38-func GetHTMLRenderer(filename string, fs fs.FS, a *app.App) (*Renderer, error) {
39- // Check if the file exists in the provided fs.FS using fs.Open
40- file, err := fs.Open(filename)
41- if err != nil {
42- a.AppLogger.Error().Err(err).Msg("Failed to open file")
43- return nil, err
44- }
45- file.Close() // Close the file after checking
46- tmpl, err := template.New(filename).ParseFS(fs, filename)
47- if err != nil {
48- a.AppLogger.Error().Err(err).Msg("Failed to parse template")
49- return nil, err
50- }
51- return &Renderer{
52- Template: tmpl,
53- }, nil
54-}
D pkg/templating/models/registry-compose.goyml +0 -15
@@ -1,15 +0,0 @@
1-registry:
2- restart: always
3- image: registry:2
4- ports:
5- - 5000:5000
6- environment:
7- REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
8- REGISTRY_HTTP_TLS_KEY: /certs/domain.key
9- REGISTRY_AUTH: htpasswd
10- REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
11- REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
12- volumes:
13- - /path/data:/var/lib/registry
14- - /path/certs:/certs
15- - /path/auth:/auth⏎̸
@@ -1,15 +0,0 @@
1-registry:
2- restart: always
3- image: registry:2
4- ports:
5- - 5000:5000
6- environment:
7- REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
8- REGISTRY_HTTP_TLS_KEY: /certs/domain.key
9- REGISTRY_AUTH: htpasswd
10- REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
11- REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
12- volumes:
13- - /path/data:/var/lib/registry
14- - /path/certs:/certs
15- - /path/auth:/auth⏎̸
D pkg/templating/models/traefik-compose.goyml +0 -29
@@ -1,29 +0,0 @@
1-version: "3"
2-
3-services:
4- traefik:
5- image: traefik:v2.10
6- container_name: traefik
7- environment:
8- - COMPOSE_PROJECT_NAME=traefik
9- restart: unless-stopped
10- ports:
11- - "80:80"
12- - "443:443"
13- volumes:
14- - /var/run/docker.sock:/var/run/docker.sock:ro
15- - ./traefik.yml:/etc/traefik/traefik.yml
16- - ./volume/acme.json:/acme.json
17- - ./logs/traefik.log:/var/log/traefik/traefik.log
18- labels:
19- - "traefik.enable=true"
20- - "traefik.http.routers.api.rule=Host(`traefik.{{ .topdomain }}`)"
21- - "traefik.http.routers.api.service=api@internal"
22- - "traefik.http.routers.api.entrypoints=websecure"
23- - "traefik.http.routers.api.tls.certresolver=myresolver"
24- - "traefik.http.services.api.loadbalancer.server.port=8080"
25- networks:
26- - exposed
27-
28-networks:
29- exposed:⏎̸
@@ -1,29 +0,0 @@
1-version: "3"
2-
3-services:
4- traefik:
5- image: traefik:v2.10
6- container_name: traefik
7- environment:
8- - COMPOSE_PROJECT_NAME=traefik
9- restart: unless-stopped
10- ports:
11- - "80:80"
12- - "443:443"
13- volumes:
14- - /var/run/docker.sock:/var/run/docker.sock:ro
15- - ./traefik.yml:/etc/traefik/traefik.yml
16- - ./volume/acme.json:/acme.json
17- - ./logs/traefik.log:/var/log/traefik/traefik.log
18- labels:
19- - "traefik.enable=true"
20- - "traefik.http.routers.api.rule=Host(`traefik.{{ .topdomain }}`)"
21- - "traefik.http.routers.api.service=api@internal"
22- - "traefik.http.routers.api.entrypoints=websecure"
23- - "traefik.http.routers.api.tls.certresolver=myresolver"
24- - "traefik.http.services.api.loadbalancer.server.port=8080"
25- networks:
26- - exposed
27-
28-networks:
29- exposed:⏎̸
D pkg/templating/models/traefik.goyml +0 -30
@@ -1,30 +0,0 @@
1-api:
2- dashboard: false
3-
4-entryPoints:
5- web:
6- address: ":80"
7- websecure:
8- address: ":443"
9-
10-providers:
11- docker:
12- endpoint: "unix:///var/run/docker.sock"
13- exposedByDefault: false
14-
15-certificatesResolvers:
16- myresolver:
17- acme:
18- email: {{ .admin.email }}
19- storage: acme.json
20- httpChallenge:
21- entryPoint: web
22-
23-log:
24- level: "DEBUG" # Set the desired log level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC
25- filePath: "/var/log/traefik/traefik.log"
26-
27-accessLog: {}
28-
29-ping:
30- entryPoint: web ⏎̸
@@ -1,30 +0,0 @@
1-api:
2- dashboard: false
3-
4-entryPoints:
5- web:
6- address: ":80"
7- websecure:
8- address: ":443"
9-
10-providers:
11- docker:
12- endpoint: "unix:///var/run/docker.sock"
13- exposedByDefault: false
14-
15-certificatesResolvers:
16- myresolver:
17- acme:
18- email: {{ .admin.email }}
19- storage: acme.json
20- httpChallenge:
21- entryPoint: web
22-
23-log:
24- level: "DEBUG" # Set the desired log level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC
25- filePath: "/var/log/traefik/traefik.log"
26-
27-accessLog: {}
28-
29-ping:
30- entryPoint: web ⏎̸
D pkg/templating/sanitizer.go +0 -22
@@ -1,22 +0,0 @@
1-package templating
2-
3-import (
4- "fmt"
5-
6- "github.com/microcosm-cc/bluemonday"
7-)
8-
9-// SanitizeHTML sanitizes HTML input using bluemonday package and returns an error if sanitization altered the input.
10-func SanitizeHTML(htmlInput string) (string, error) {
11- // Create a new UGC policy
12- p := bluemonday.UGCPolicy()
13-
14- // Sanitize the input HTML
15- sanitized := p.Sanitize(htmlInput)
16-
17- if sanitized != htmlInput {
18- return sanitized, fmt.Errorf("HTML input was altered during sanitization")
19- }
20-
21- return sanitized, nil
22-}
@@ -1,22 +0,0 @@
1-package templating
2-
3-import (
4- "fmt"
5-
6- "github.com/microcosm-cc/bluemonday"
7-)
8-
9-// SanitizeHTML sanitizes HTML input using bluemonday package and returns an error if sanitization altered the input.
10-func SanitizeHTML(htmlInput string) (string, error) {
11- // Create a new UGC policy
12- p := bluemonday.UGCPolicy()
13-
14- // Sanitize the input HTML
15- sanitized := p.Sanitize(htmlInput)
16-
17- if sanitized != htmlInput {
18- return sanitized, fmt.Errorf("HTML input was altered during sanitization")
19- }
20-
21- return sanitized, nil
22-}
D pkg/templating/txt-render.go +0 -63
@@ -1,63 +0,0 @@
1-package templating
2-
3-import (
4- "bytes"
5- "errors"
6- "fmt"
7- "io/fs"
8- "text/template"
9-
10- "gogs.bnema.dev/gordon-echo/pkg/utils"
11-)
12-
13-type TXTRenderer struct {
14- Models *template.Template
15- Format string
16- ParseError error
17- Logger *utils.Logger
18-}
19-
20-// Render function renders the template with the given data
21-func (r *TXTRenderer) TXTRender(data interface{}) (string, error) {
22- if r.ParseError != nil {
23- r.Logger.Error().Err(r.ParseError).Msg("Parse error")
24- return "", r.ParseError
25- }
26-
27- if r.Models == nil {
28- err := errors.New("invalid or nil model")
29- r.Logger.Error().Err(r.ParseError).Msg("Parse error")
30-
31- return "", err
32- }
33-
34- buf := new(bytes.Buffer)
35- err := r.Models.Execute(buf, data)
36- if err != nil {
37- r.Logger.Error().Err(err).Msg("Render error")
38- return "", err
39- }
40-
41- return buf.String(), nil
42-}
43-
44-// GetRenderer function returns a new Renderer instance
45-func GetTXTRenderer(filename string, fs fs.FS, logger *utils.Logger) (*TXTRenderer, error) {
46- fmt.Println(filename)
47- // Check if the file exists in the provided fs.FS using fs.Open
48- file, err := fs.Open(filename)
49- if err != nil {
50- logger.Error().Err(err).Msg("Template or model '%s' not found" + filename)
51- return nil, err
52- }
53- file.Close() // Close the file after checking
54- mdls, err := template.New(filename).ParseFS(fs, filename)
55- if err != nil {
56- logger.Error().Err(err).Msg("Failed to parse template")
57- return nil, err
58- }
59- return &TXTRenderer{
60- Models: mdls,
61- Logger: logger,
62- }, nil
63-}
@@ -1,63 +0,0 @@
1-package templating
2-
3-import (
4- "bytes"
5- "errors"
6- "fmt"
7- "io/fs"
8- "text/template"
9-
10- "gogs.bnema.dev/gordon-echo/pkg/utils"
11-)
12-
13-type TXTRenderer struct {
14- Models *template.Template
15- Format string
16- ParseError error
17- Logger *utils.Logger
18-}
19-
20-// Render function renders the template with the given data
21-func (r *TXTRenderer) TXTRender(data interface{}) (string, error) {
22- if r.ParseError != nil {
23- r.Logger.Error().Err(r.ParseError).Msg("Parse error")
24- return "", r.ParseError
25- }
26-
27- if r.Models == nil {
28- err := errors.New("invalid or nil model")
29- r.Logger.Error().Err(r.ParseError).Msg("Parse error")
30-
31- return "", err
32- }
33-
34- buf := new(bytes.Buffer)
35- err := r.Models.Execute(buf, data)
36- if err != nil {
37- r.Logger.Error().Err(err).Msg("Render error")
38- return "", err
39- }
40-
41- return buf.String(), nil
42-}
43-
44-// GetRenderer function returns a new Renderer instance
45-func GetTXTRenderer(filename string, fs fs.FS, logger *utils.Logger) (*TXTRenderer, error) {
46- fmt.Println(filename)
47- // Check if the file exists in the provided fs.FS using fs.Open
48- file, err := fs.Open(filename)
49- if err != nil {
50- logger.Error().Err(err).Msg("Template or model '%s' not found" + filename)
51- return nil, err
52- }
53- file.Close() // Close the file after checking
54- mdls, err := template.New(filename).ParseFS(fs, filename)
55- if err != nil {
56- logger.Error().Err(err).Msg("Failed to parse template")
57- return nil, err
58- }
59- return &TXTRenderer{
60- Models: mdls,
61- Logger: logger,
62- }, nil
63-}
D pkg/utils/detect-first-launch.go +0 -14
@@ -1,14 +0,0 @@
1-package utils
2-
3-import (
4- "os"
5-)
6-
7-// IsConfigFilePresent checks if it's the first launch of the app based on the existence of gordon.yml
8-func IsConfigFilePresent() bool {
9- // Check if gordon.yml exists at the root of the project
10- if _, err := os.Stat("gordon.yml"); os.IsNotExist(err) {
11- return true
12- }
13- return false
14-}
@@ -1,14 +0,0 @@
1-package utils
2-
3-import (
4- "os"
5-)
6-
7-// IsConfigFilePresent checks if it's the first launch of the app based on the existence of gordon.yml
8-func IsConfigFilePresent() bool {
9- // Check if gordon.yml exists at the root of the project
10- if _, err := os.Stat("gordon.yml"); os.IsNotExist(err) {
11- return true
12- }
13- return false
14-}
D pkg/utils/logger.go +0 -282
@@ -1,282 +0,0 @@
1-package utils
2-
3-import (
4- "bufio"
5- "fmt"
6- "io"
7- "os"
8-
9- "github.com/labstack/gommon/log"
10- "github.com/rs/zerolog"
11-)
12-
13-const (
14- DEBUG LogLevel = "debug"
15- INFO LogLevel = "info"
16- WARN LogLevel = "warn"
17- ERROR LogLevel = "error"
18- FATAL LogLevel = "fatal"
19- PANIC LogLevel = "panic"
20-)
21-
22-type LogLevel string
23-
24-type Logger struct {
25- zerolog.Logger
26-}
27-
28-type stdoutCapture struct {
29- logger *Logger
30-}
31-
32-type EchoLoggerWrapper struct {
33- *Logger
34- output io.Writer
35-}
36-
37-// LogFunc is a function that logs a message with optional context.
38-type LogFunc func() *zerolog.Event
39-
40-// Map log levels to zerolog log functions
41-func (l *Logger) getLogFunc(level LogLevel) LogFunc {
42- switch level {
43- case DEBUG:
44- return func() *zerolog.Event { return l.Logger.Debug() }
45- case INFO:
46- return func() *zerolog.Event { return l.Logger.Info() }
47- case WARN:
48- return func() *zerolog.Event { return l.Logger.Warn() }
49- case ERROR:
50- return func() *zerolog.Event { return l.Logger.Error() }
51- case FATAL:
52- return func() *zerolog.Event { return l.Logger.Fatal() }
53- case PANIC:
54- return func() *zerolog.Event { return l.Logger.Panic() }
55- default:
56- return func() *zerolog.Event { return l.Logger.Info() } // default to INFO if unknown level
57- }
58-}
59-
60-func (l *Logger) Log(level LogLevel, i ...interface{}) {
61- logFunc := l.getLogFunc(level)
62- logFunc().Msg(fmt.Sprint(i...))
63-}
64-
65-func NewLogger() *Logger {
66- writer := zerolog.ConsoleWriter{Out: os.Stdout}
67- zl := zerolog.New(writer).With().Timestamp().Logger()
68- return &Logger{zl}
69-}
70-
71-func (s *stdoutCapture) Write(p []byte) (n int, err error) {
72- s.logger.Info().Str("type", "app").Msg(string(p))
73- return len(p), nil
74-}
75-
76-func CaptureSTDOUT(logger *Logger) {
77- // Create a reading and writing pipe
78- reader, writer, _ := os.Pipe()
79-
80- // Replace os.Stdout with the writer end of the pipe
81- os.Stdout = writer
82-
83- // Start a goroutine to read from the pipe
84- go func() {
85- scanner := bufio.NewScanner(reader)
86- for scanner.Scan() {
87- text := scanner.Text()
88- logger.Debug().Str("type", "app").Msg(text)
89- }
90- }()
91-}
92-
93-func NewEchoLoggerWrapper(l *Logger) *EchoLoggerWrapper {
94- return &EchoLoggerWrapper{Logger: l, output: os.Stdout}
95-}
96-
97-func (l *Logger) SetOutput(w io.Writer) *Logger {
98- l.Logger = l.Logger.Output(w)
99- return l
100-}
101-
102-func (l *Logger) SetLevel(level LogLevel) *Logger {
103- l.Logger = l.Logger.Level(getZerologLevel(level))
104- return l
105-}
106-
107-func getZerologLevel(level LogLevel) zerolog.Level {
108- switch level {
109- case DEBUG:
110- return zerolog.DebugLevel
111- case INFO:
112- return zerolog.InfoLevel
113- case WARN:
114- return zerolog.WarnLevel
115- case ERROR:
116- return zerolog.ErrorLevel
117- case FATAL:
118- return zerolog.FatalLevel
119- case PANIC:
120- return zerolog.PanicLevel
121- default:
122- return zerolog.InfoLevel // default to INFO if unknown level
123- }
124-}
125-
126-func CreateLogsDir(logDir string) error {
127- if _, err := os.Stat(logDir); os.IsNotExist(err) {
128- err := os.MkdirAll(logDir, 0755)
129- if err != nil {
130- return err
131- }
132- }
133- return nil
134-}
135-
136-func (l *EchoLoggerWrapper) Output() io.Writer {
137- return l.output
138-}
139-
140-func (l *EchoLoggerWrapper) SetOutput(w io.Writer) {
141- l.output = w
142-}
143-
144-func (l *EchoLoggerWrapper) Prefix() string {
145- return ""
146-}
147-
148-func (l *EchoLoggerWrapper) SetPrefix(p string) {
149- // do nothing
150-}
151-
152-func (l *EchoLoggerWrapper) Level() log.Lvl {
153- return log.Lvl(l.Logger.GetLevel())
154-}
155-
156-// SetHeader
157-func (l *EchoLoggerWrapper) SetHeader(h string) {
158- // do nothing
159-}
160-
161-func (l *EchoLoggerWrapper) SetLevel(log.Lvl) {
162- // do nothing
163-}
164-
165-// SetEvent creates a new event with the given level and message.
166-func (l *EchoLoggerWrapper) SetEvent(level zerolog.Level, msg string) *zerolog.Event {
167- //cannot use utils.NewEchoLoggerWrapper(app.HTTPLogger) (value of type *utils.EchoLoggerWrapper) as echo.Logger value in assignment: *utils.EchoLoggerWrapper does not implement echo.Logger (wrong type for method Error)
168- return l.Logger.WithLevel(level).Str("message", msg)
169-}
170-
171-func (l *EchoLoggerWrapper) OutputStdout() bool {
172- return true
173-}
174-
175-// Helper function for methods that accept variadic slice of interfaces
176-func (l *EchoLoggerWrapper) LogWithArgs(level LogLevel, i ...interface{}) {
177- l.Logger.Log(level, i...)
178-}
179-
180-// Helper function for methods that accept a format string and arguments
181-func (l *EchoLoggerWrapper) LogWithFormat(level LogLevel, format string, args ...interface{}) {
182- l.Logger.Log(level, fmt.Sprintf(format, args...))
183-}
184-
185-// Helper function for methods that accept a log.JSON object
186-func (l *EchoLoggerWrapper) LogWithJSON(level LogLevel, j log.JSON) {
187- l.Logger.Log(level, j)
188-}
189-
190-// Debug
191-func (l *EchoLoggerWrapper) Debug(i ...interface{}) {
192- l.LogWithArgs(DEBUG, i...)
193-}
194-
195-func (l *EchoLoggerWrapper) Debugf(format string, args ...interface{}) {
196- l.LogWithFormat(DEBUG, format, args...)
197-}
198-
199-func (l *EchoLoggerWrapper) Debugj(j log.JSON) {
200- l.LogWithJSON(DEBUG, j)
201-}
202-
203-// Info
204-func (l *EchoLoggerWrapper) Info(i ...interface{}) {
205- l.LogWithArgs(INFO, i...)
206-}
207-
208-func (l *EchoLoggerWrapper) Infof(format string, args ...interface{}) {
209- l.LogWithFormat(INFO, format, args...)
210-}
211-
212-func (l *EchoLoggerWrapper) Infoj(j log.JSON) {
213- l.LogWithJSON(INFO, j)
214-}
215-
216-// Warn
217-
218-func (l *EchoLoggerWrapper) Warn(i ...interface{}) {
219- l.LogWithArgs(WARN, i...)
220-}
221-
222-func (l *EchoLoggerWrapper) Warnf(format string, args ...interface{}) {
223- l.LogWithFormat(WARN, format, args...)
224-}
225-
226-func (l *EchoLoggerWrapper) Warnj(j log.JSON) {
227- l.LogWithJSON(WARN, j)
228-}
229-
230-// Error
231-func (l *EchoLoggerWrapper) Error(i ...interface{}) {
232- l.LogWithArgs(ERROR, i...)
233-}
234-
235-func (l *EchoLoggerWrapper) Errorf(format string, args ...interface{}) {
236- l.LogWithFormat(ERROR, format, args...)
237-}
238-
239-func (l *EchoLoggerWrapper) Errorj(j log.JSON) {
240- l.LogWithJSON(ERROR, j)
241-}
242-
243-// Fatal
244-func (l *EchoLoggerWrapper) Fatal(i ...interface{}) {
245- l.LogWithArgs(FATAL, i...)
246-}
247-
248-func (l *EchoLoggerWrapper) Fatalf(format string, args ...interface{}) {
249- l.LogWithFormat(FATAL, format, args...)
250-}
251-
252-func (l *EchoLoggerWrapper) Fatalj(j log.JSON) {
253- l.LogWithJSON(FATAL, j)
254-}
255-
256-// Panic
257-
258-func (l *EchoLoggerWrapper) Panic(i ...interface{}) {
259- l.LogWithArgs(PANIC, i...)
260-}
261-
262-func (l *EchoLoggerWrapper) Panicf(format string, args ...interface{}) {
263- l.LogWithFormat(PANIC, format, args...)
264-}
265-
266-func (l *EchoLoggerWrapper) Panicj(j log.JSON) {
267- l.LogWithJSON(PANIC, j)
268-}
269-
270-// Print
271-
272-func (l *EchoLoggerWrapper) Print(i ...interface{}) {
273- l.LogWithArgs(INFO, i...)
274-}
275-
276-func (l *EchoLoggerWrapper) Printf(format string, args ...interface{}) {
277- l.LogWithFormat(INFO, format, args...)
278-}
279-
280-func (l *EchoLoggerWrapper) Printj(j log.JSON) {
281- l.LogWithJSON(INFO, j)
282-}
@@ -1,282 +0,0 @@
1-package utils
2-
3-import (
4- "bufio"
5- "fmt"
6- "io"
7- "os"
8-
9- "github.com/labstack/gommon/log"
10- "github.com/rs/zerolog"
11-)
12-
13-const (
14- DEBUG LogLevel = "debug"
15- INFO LogLevel = "info"
16- WARN LogLevel = "warn"
17- ERROR LogLevel = "error"
18- FATAL LogLevel = "fatal"
19- PANIC LogLevel = "panic"
20-)
21-
22-type LogLevel string
23-
24-type Logger struct {
25- zerolog.Logger
26-}
27-
28-type stdoutCapture struct {
29- logger *Logger
30-}
31-
32-type EchoLoggerWrapper struct {
33- *Logger
34- output io.Writer
35-}
36-
37-// LogFunc is a function that logs a message with optional context.
38-type LogFunc func() *zerolog.Event
39-
40-// Map log levels to zerolog log functions
41-func (l *Logger) getLogFunc(level LogLevel) LogFunc {
42- switch level {
43- case DEBUG:
44- return func() *zerolog.Event { return l.Logger.Debug() }
45- case INFO:
46- return func() *zerolog.Event { return l.Logger.Info() }
47- case WARN:
48- return func() *zerolog.Event { return l.Logger.Warn() }
49- case ERROR:
50- return func() *zerolog.Event { return l.Logger.Error() }
51- case FATAL:
52- return func() *zerolog.Event { return l.Logger.Fatal() }
53- case PANIC:
54- return func() *zerolog.Event { return l.Logger.Panic() }
55- default:
56- return func() *zerolog.Event { return l.Logger.Info() } // default to INFO if unknown level
57- }
58-}
59-
60-func (l *Logger) Log(level LogLevel, i ...interface{}) {
61- logFunc := l.getLogFunc(level)
62- logFunc().Msg(fmt.Sprint(i...))
63-}
64-
65-func NewLogger() *Logger {
66- writer := zerolog.ConsoleWriter{Out: os.Stdout}
67- zl := zerolog.New(writer).With().Timestamp().Logger()
68- return &Logger{zl}
69-}
70-
71-func (s *stdoutCapture) Write(p []byte) (n int, err error) {
72- s.logger.Info().Str("type", "app").Msg(string(p))
73- return len(p), nil
74-}
75-
76-func CaptureSTDOUT(logger *Logger) {
77- // Create a reading and writing pipe
78- reader, writer, _ := os.Pipe()
79-
80- // Replace os.Stdout with the writer end of the pipe
81- os.Stdout = writer
82-
83- // Start a goroutine to read from the pipe
84- go func() {
85- scanner := bufio.NewScanner(reader)
86- for scanner.Scan() {
87- text := scanner.Text()
88- logger.Debug().Str("type", "app").Msg(text)
89- }
90- }()
91-}
92-
93-func NewEchoLoggerWrapper(l *Logger) *EchoLoggerWrapper {
94- return &EchoLoggerWrapper{Logger: l, output: os.Stdout}
95-}
96-
97-func (l *Logger) SetOutput(w io.Writer) *Logger {
98- l.Logger = l.Logger.Output(w)
99- return l
100-}
101-
102-func (l *Logger) SetLevel(level LogLevel) *Logger {
103- l.Logger = l.Logger.Level(getZerologLevel(level))
104- return l
105-}
106-
107-func getZerologLevel(level LogLevel) zerolog.Level {
108- switch level {
109- case DEBUG:
110- return zerolog.DebugLevel
111- case INFO:
112- return zerolog.InfoLevel
113- case WARN:
114- return zerolog.WarnLevel
115- case ERROR:
116- return zerolog.ErrorLevel
117- case FATAL:
118- return zerolog.FatalLevel
119- case PANIC:
120- return zerolog.PanicLevel
121- default:
122- return zerolog.InfoLevel // default to INFO if unknown level
123- }
124-}
125-
126-func CreateLogsDir(logDir string) error {
127- if _, err := os.Stat(logDir); os.IsNotExist(err) {
128- err := os.MkdirAll(logDir, 0755)
129- if err != nil {
130- return err
131- }
132- }
133- return nil
134-}
135-
136-func (l *EchoLoggerWrapper) Output() io.Writer {
137- return l.output
138-}
139-
140-func (l *EchoLoggerWrapper) SetOutput(w io.Writer) {
141- l.output = w
142-}
143-
144-func (l *EchoLoggerWrapper) Prefix() string {
145- return ""
146-}
147-
148-func (l *EchoLoggerWrapper) SetPrefix(p string) {
149- // do nothing
150-}
151-
152-func (l *EchoLoggerWrapper) Level() log.Lvl {
153- return log.Lvl(l.Logger.GetLevel())
154-}
155-
156-// SetHeader
157-func (l *EchoLoggerWrapper) SetHeader(h string) {
158- // do nothing
159-}
160-
161-func (l *EchoLoggerWrapper) SetLevel(log.Lvl) {
162- // do nothing
163-}
164-
165-// SetEvent creates a new event with the given level and message.
166-func (l *EchoLoggerWrapper) SetEvent(level zerolog.Level, msg string) *zerolog.Event {
167- //cannot use utils.NewEchoLoggerWrapper(app.HTTPLogger) (value of type *utils.EchoLoggerWrapper) as echo.Logger value in assignment: *utils.EchoLoggerWrapper does not implement echo.Logger (wrong type for method Error)
168- return l.Logger.WithLevel(level).Str("message", msg)
169-}
170-
171-func (l *EchoLoggerWrapper) OutputStdout() bool {
172- return true
173-}
174-
175-// Helper function for methods that accept variadic slice of interfaces
176-func (l *EchoLoggerWrapper) LogWithArgs(level LogLevel, i ...interface{}) {
177- l.Logger.Log(level, i...)
178-}
179-
180-// Helper function for methods that accept a format string and arguments
181-func (l *EchoLoggerWrapper) LogWithFormat(level LogLevel, format string, args ...interface{}) {
182- l.Logger.Log(level, fmt.Sprintf(format, args...))
183-}
184-
185-// Helper function for methods that accept a log.JSON object
186-func (l *EchoLoggerWrapper) LogWithJSON(level LogLevel, j log.JSON) {
187- l.Logger.Log(level, j)
188-}
189-
190-// Debug
191-func (l *EchoLoggerWrapper) Debug(i ...interface{}) {
192- l.LogWithArgs(DEBUG, i...)
193-}
194-
195-func (l *EchoLoggerWrapper) Debugf(format string, args ...interface{}) {
196- l.LogWithFormat(DEBUG, format, args...)
197-}
198-
199-func (l *EchoLoggerWrapper) Debugj(j log.JSON) {
200- l.LogWithJSON(DEBUG, j)
201-}
202-
203-// Info
204-func (l *EchoLoggerWrapper) Info(i ...interface{}) {
205- l.LogWithArgs(INFO, i...)
206-}
207-
208-func (l *EchoLoggerWrapper) Infof(format string, args ...interface{}) {
209- l.LogWithFormat(INFO, format, args...)
210-}
211-
212-func (l *EchoLoggerWrapper) Infoj(j log.JSON) {
213- l.LogWithJSON(INFO, j)
214-}
215-
216-// Warn
217-
218-func (l *EchoLoggerWrapper) Warn(i ...interface{}) {
219- l.LogWithArgs(WARN, i...)
220-}
221-
222-func (l *EchoLoggerWrapper) Warnf(format string, args ...interface{}) {
223- l.LogWithFormat(WARN, format, args...)
224-}
225-
226-func (l *EchoLoggerWrapper) Warnj(j log.JSON) {
227- l.LogWithJSON(WARN, j)
228-}
229-
230-// Error
231-func (l *EchoLoggerWrapper) Error(i ...interface{}) {
232- l.LogWithArgs(ERROR, i...)
233-}
234-
235-func (l *EchoLoggerWrapper) Errorf(format string, args ...interface{}) {
236- l.LogWithFormat(ERROR, format, args...)
237-}
238-
239-func (l *EchoLoggerWrapper) Errorj(j log.JSON) {
240- l.LogWithJSON(ERROR, j)
241-}
242-
243-// Fatal
244-func (l *EchoLoggerWrapper) Fatal(i ...interface{}) {
245- l.LogWithArgs(FATAL, i...)
246-}
247-
248-func (l *EchoLoggerWrapper) Fatalf(format string, args ...interface{}) {
249- l.LogWithFormat(FATAL, format, args...)
250-}
251-
252-func (l *EchoLoggerWrapper) Fatalj(j log.JSON) {
253- l.LogWithJSON(FATAL, j)
254-}
255-
256-// Panic
257-
258-func (l *EchoLoggerWrapper) Panic(i ...interface{}) {
259- l.LogWithArgs(PANIC, i...)
260-}
261-
262-func (l *EchoLoggerWrapper) Panicf(format string, args ...interface{}) {
263- l.LogWithFormat(PANIC, format, args...)
264-}
265-
266-func (l *EchoLoggerWrapper) Panicj(j log.JSON) {
267- l.LogWithJSON(PANIC, j)
268-}
269-
270-// Print
271-
272-func (l *EchoLoggerWrapper) Print(i ...interface{}) {
273- l.LogWithArgs(INFO, i...)
274-}
275-
276-func (l *EchoLoggerWrapper) Printf(format string, args ...interface{}) {
277- l.LogWithFormat(INFO, format, args...)
278-}
279-
280-func (l *EchoLoggerWrapper) Printj(j log.JSON) {
281- l.LogWithJSON(INFO, j)
282-}
D pkg/utils/parser.go +0 -101
@@ -1,101 +0,0 @@
1-package utils
2-
3-import (
4- "fmt"
5- "io"
6- "io/fs"
7- "os"
8-
9- "github.com/tidwall/gjson"
10- "gopkg.in/yaml.v3"
11-)
12-
13-// Yaml Parser
14-type LangData map[string]interface{}
15-
16-type StringsYamlData struct {
17- StringsModel StringsModel `yaml:"strings_model"`
18- EN LangData `yaml:"en"`
19- FR LangData `yaml:"fr"`
20- CurrentLang LangData
21-}
22-
23-type ComposeModelData struct {
24- ComposeModel ComposeModel `yaml:"compose_model"`
25-}
26-
27-type StringsModel struct {
28- Header string `yaml:"header"`
29- Subheader string `yaml:"subheader"`
30-}
31-
32-type ComposeModel struct {
33- Header string `yaml:"header"`
34- Subheader string `yaml:"subheader"`
35-}
36-
37-// ReadDataFromYAML loads the data from the given YAML file from any filesystem
38-func ReadDataFromYAML(fs fs.FS, filename string, out interface{}) error {
39- // Open the YAML file
40- file, err := fs.Open(filename)
41- if err != nil {
42- return fmt.Errorf("failed to open %s: %w", filename, err)
43- }
44- defer file.Close()
45-
46- // Read the content of the file
47- content, err := io.ReadAll(file)
48- if err != nil {
49- return fmt.Errorf("failed to read %s: %w", filename, err)
50- }
51-
52- // Unmarshal the YAML content into the out interface
53- if err := yaml.Unmarshal(content, out); err != nil {
54- return fmt.Errorf("failed to unmarshal %s: %w", filename, err)
55- }
56-
57- return nil
58-}
59-
60-// PopulateDataFromYAML retrieves the data from the YAML file and sets the appropriate language data
61-func PopulateDataFromYAML(lang string, fs fs.FS, filename string, out interface{}) error {
62- err := ReadDataFromYAML(fs, filename, out)
63- if err != nil {
64- return fmt.Errorf("failed to read data from YAML: %w", err)
65- }
66-
67- // If the type assertion succeeds, we set the appropriate language data
68- if data, ok := out.(*StringsYamlData); ok {
69- switch lang {
70- case "en":
71- data.CurrentLang = data.EN
72- case "fr":
73- data.CurrentLang = data.FR
74- default: // default to English if no match
75- data.CurrentLang = data.EN
76- }
77- }
78-
79- return nil
80-}
81-
82-// Json Parser
83-
84-// ReadDataFromJSONFile loads the data from the given JSON file
85-func ReadDataFromJSONFile(filePath string) (string, error) {
86- bytes, err := os.ReadFile(filePath)
87- if err != nil {
88- return "", err
89- }
90- return string(bytes), nil
91-}
92-
93-// QueryJSON queries the provided JSON content based on the given gjson query and returns the results as a slice of strings
94-func QueryJSON(jsonContent, query string) []string {
95- results := gjson.GetMany(jsonContent, query)
96- var values []string
97- for _, result := range results {
98- values = append(values, result.String())
99- }
100- return values
101-}
@@ -1,101 +0,0 @@
1-package utils
2-
3-import (
4- "fmt"
5- "io"
6- "io/fs"
7- "os"
8-
9- "github.com/tidwall/gjson"
10- "gopkg.in/yaml.v3"
11-)
12-
13-// Yaml Parser
14-type LangData map[string]interface{}
15-
16-type StringsYamlData struct {
17- StringsModel StringsModel `yaml:"strings_model"`
18- EN LangData `yaml:"en"`
19- FR LangData `yaml:"fr"`
20- CurrentLang LangData
21-}
22-
23-type ComposeModelData struct {
24- ComposeModel ComposeModel `yaml:"compose_model"`
25-}
26-
27-type StringsModel struct {
28- Header string `yaml:"header"`
29- Subheader string `yaml:"subheader"`
30-}
31-
32-type ComposeModel struct {
33- Header string `yaml:"header"`
34- Subheader string `yaml:"subheader"`
35-}
36-
37-// ReadDataFromYAML loads the data from the given YAML file from any filesystem
38-func ReadDataFromYAML(fs fs.FS, filename string, out interface{}) error {
39- // Open the YAML file
40- file, err := fs.Open(filename)
41- if err != nil {
42- return fmt.Errorf("failed to open %s: %w", filename, err)
43- }
44- defer file.Close()
45-
46- // Read the content of the file
47- content, err := io.ReadAll(file)
48- if err != nil {
49- return fmt.Errorf("failed to read %s: %w", filename, err)
50- }
51-
52- // Unmarshal the YAML content into the out interface
53- if err := yaml.Unmarshal(content, out); err != nil {
54- return fmt.Errorf("failed to unmarshal %s: %w", filename, err)
55- }
56-
57- return nil
58-}
59-
60-// PopulateDataFromYAML retrieves the data from the YAML file and sets the appropriate language data
61-func PopulateDataFromYAML(lang string, fs fs.FS, filename string, out interface{}) error {
62- err := ReadDataFromYAML(fs, filename, out)
63- if err != nil {
64- return fmt.Errorf("failed to read data from YAML: %w", err)
65- }
66-
67- // If the type assertion succeeds, we set the appropriate language data
68- if data, ok := out.(*StringsYamlData); ok {
69- switch lang {
70- case "en":
71- data.CurrentLang = data.EN
72- case "fr":
73- data.CurrentLang = data.FR
74- default: // default to English if no match
75- data.CurrentLang = data.EN
76- }
77- }
78-
79- return nil
80-}
81-
82-// Json Parser
83-
84-// ReadDataFromJSONFile loads the data from the given JSON file
85-func ReadDataFromJSONFile(filePath string) (string, error) {
86- bytes, err := os.ReadFile(filePath)
87- if err != nil {
88- return "", err
89- }
90- return string(bytes), nil
91-}
92-
93-// QueryJSON queries the provided JSON content based on the given gjson query and returns the results as a slice of strings
94-func QueryJSON(jsonContent, query string) []string {
95- results := gjson.GetMany(jsonContent, query)
96- var values []string
97- for _, result := range results {
98- values = append(values, result.String())
99- }
100- return values
101-}
D pkg/utils/tty.go +0 -65
@@ -1,65 +0,0 @@
1-package utils
2-
3-import (
4- "net/http"
5- "os"
6- "os/exec"
7- "os/signal"
8- "syscall"
9- "time"
10-
11- "github.com/rs/zerolog"
12-)
13-
14-// SetRawMode sets the terminal to raw mode.
15-func SetRawMode() {
16- cmd := exec.Command("stty", "raw", "-echo")
17- cmd.Stdin = os.Stdin
18- _ = cmd.Run()
19-}
20-
21-// RestoreTerminal restores the terminal settings.
22-func RestoreTerminal() {
23- cmd := exec.Command("stty", "-raw", "echo")
24- cmd.Stdin = os.Stdin
25- _ = cmd.Run()
26-}
27-
28-func RunAppCatchSIGINT(startServerFunc func() error, logger *zerolog.Logger) {
29- // Start the server in a separate goroutine
30- go func() {
31- if err := startServerFunc(); err != nil && err != http.ErrServerClosed {
32- logger.Fatal().Err(err).Msg("Failed to start the server")
33- }
34- }()
35-
36- // Catch SIGINT and SIGTERM signals without raw mode initially
37- c := make(chan os.Signal, 1)
38- signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
39- <-c
40-
41- logger.Warn().Msg("Graceful shutdown initiated")
42- logger.Warn().Msg("Server will shutdown in 5 seconds press ENTER to cancel")
43-
44- // Now, set raw mode
45- SetRawMode()
46- defer RestoreTerminal()
47-
48- // Channel to signal a key press
49- keypress := make(chan struct{})
50-
51- // Goroutine to listen for a key press
52- go func() {
53- var b []byte = make([]byte, 1)
54- os.Stdin.Read(b)
55- keypress <- struct{}{}
56- }()
57-
58- // Wait for either a key press or the 5-second timer
59- select {
60- case <-time.After(5 * time.Second):
61- logger.Info().Msg("Server shutdown")
62- case <-keypress:
63- logger.Info().Msg("Shutdown cancelled by user")
64- }
65-}
@@ -1,65 +0,0 @@
1-package utils
2-
3-import (
4- "net/http"
5- "os"
6- "os/exec"
7- "os/signal"
8- "syscall"
9- "time"
10-
11- "github.com/rs/zerolog"
12-)
13-
14-// SetRawMode sets the terminal to raw mode.
15-func SetRawMode() {
16- cmd := exec.Command("stty", "raw", "-echo")
17- cmd.Stdin = os.Stdin
18- _ = cmd.Run()
19-}
20-
21-// RestoreTerminal restores the terminal settings.
22-func RestoreTerminal() {
23- cmd := exec.Command("stty", "-raw", "echo")
24- cmd.Stdin = os.Stdin
25- _ = cmd.Run()
26-}
27-
28-func RunAppCatchSIGINT(startServerFunc func() error, logger *zerolog.Logger) {
29- // Start the server in a separate goroutine
30- go func() {
31- if err := startServerFunc(); err != nil && err != http.ErrServerClosed {
32- logger.Fatal().Err(err).Msg("Failed to start the server")
33- }
34- }()
35-
36- // Catch SIGINT and SIGTERM signals without raw mode initially
37- c := make(chan os.Signal, 1)
38- signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
39- <-c
40-
41- logger.Warn().Msg("Graceful shutdown initiated")
42- logger.Warn().Msg("Server will shutdown in 5 seconds press ENTER to cancel")
43-
44- // Now, set raw mode
45- SetRawMode()
46- defer RestoreTerminal()
47-
48- // Channel to signal a key press
49- keypress := make(chan struct{})
50-
51- // Goroutine to listen for a key press
52- go func() {
53- var b []byte = make([]byte, 1)
54- os.Stdin.Read(b)
55- keypress <- struct{}{}
56- }()
57-
58- // Wait for either a key press or the 5-second timer
59- select {
60- case <-time.After(5 * time.Second):
61- logger.Info().Msg("Server shutdown")
62- case <-keypress:
63- logger.Info().Msg("Shutdown cancelled by user")
64- }
65-}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close