feat: allow homepage customization
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
Some checks reported warnings
arcad/arcast/pipeline/head This commit is unstable
This commit is contained in:
64
pkg/server/embed/_partials/base.gohtml
Normal file
64
pkg/server/embed/_partials/base.gohtml
Normal file
@ -0,0 +1,64 @@
|
||||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/logo.png" />
|
||||
<title>Ready to cast !</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
{{ block "head" .
|
||||
}}{{
|
||||
end
|
||||
}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="panel">
|
||||
<div id="icon"></div>
|
||||
{{ block "message" . }}
|
||||
<h2 id="title" class="text-centered">Ready to cast !</h2>
|
||||
{{ end }}
|
||||
{{ block "info" . }}
|
||||
<p><b>Instance ID</b></p>
|
||||
<p class="text-small text-centered">
|
||||
<code>{{ .ID }}</code>
|
||||
</p>
|
||||
<p><b>Addresses</b></p>
|
||||
<ul class="text-italic text-small text-centered">
|
||||
{{ $port := .Port }}
|
||||
{{
|
||||
range.IPs
|
||||
}}
|
||||
<li>
|
||||
<code>{{ . }}:{{ $port }}</code>
|
||||
</li>
|
||||
{{
|
||||
end
|
||||
}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{if .Apps }}
|
||||
{{ block "apps" . }}
|
||||
<p><b>Apps</b></p>
|
||||
<ul class="text-italic text-small text-centered">
|
||||
{{ $tlsPort := .TLSPort }}
|
||||
{{
|
||||
range.IPs
|
||||
}}
|
||||
<li>
|
||||
<a href="https://{{ . }}:{{ $tlsPort }}/apps">
|
||||
https://{{ . }}:{{ $tlsPort }}/apps
|
||||
</a>
|
||||
</li>
|
||||
{{
|
||||
end
|
||||
}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
3
pkg/server/embed/_templates/index.gohtml
Normal file
3
pkg/server/embed/_templates/index.gohtml
Normal file
@ -0,0 +1,3 @@
|
||||
{{ define "index" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
BIN
pkg/server/embed/logo.png
Normal file
BIN
pkg/server/embed/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
121
pkg/server/embed/style.css
Normal file
121
pkg/server/embed/style.css
Normal file
@ -0,0 +1,121 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgb(76, 96, 188);
|
||||
background: linear-gradient(
|
||||
415deg,
|
||||
rgba(4, 168, 243, 1),
|
||||
rgb(76, 136, 188, 1),
|
||||
rgba(76, 96, 188, 1),
|
||||
rgb(115, 76, 188, 1),
|
||||
rgb(87, 76, 188, 1)
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
ol,
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 10px 10px 10px #33333361;
|
||||
position: relative;
|
||||
padding: 50px 30px 30px 30px;
|
||||
min-width: 50%;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#title {
|
||||
margin: 10px 0px 20px 0px;
|
||||
}
|
||||
|
||||
#icon {
|
||||
width: 100px;
|
||||
aspect-ratio: 1/1;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
background-image: url("logo.png");
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -50px;
|
||||
margin-top: -100px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.panel p,
|
||||
.panel ul {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.text-centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.mt {
|
||||
margin-top: 1em;
|
||||
display: block;
|
||||
}
|
90
pkg/server/fs.go
Normal file
90
pkg/server/fs.go
Normal file
@ -0,0 +1,90 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/arcad/arcast/pkg/network"
|
||||
"github.com/dschmidt/go-layerfs"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed embed/**
|
||||
embedFS embed.FS
|
||||
)
|
||||
|
||||
func (s *Server) initLayeredFS() error {
|
||||
layers := make([]fs.FS, 0)
|
||||
|
||||
if s.upperLayerDir != "" {
|
||||
upperLayer := os.DirFS(s.upperLayerDir)
|
||||
layers = append(layers, upperLayer)
|
||||
}
|
||||
|
||||
baseLayer, err := fs.Sub(embedFS, "embed")
|
||||
if err != nil {
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
layers = append(layers, baseLayer)
|
||||
|
||||
s.layeredFS = layerfs.New(layers...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/_templates") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFileFS(w, r, s.layeredFS, path)
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
type templateData struct {
|
||||
IPs []string
|
||||
Port int
|
||||
TLSPort int
|
||||
ID string
|
||||
Apps bool
|
||||
}
|
||||
|
||||
ips, err := network.GetLANIPv4Addrs()
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
d := templateData{
|
||||
ID: s.instanceID,
|
||||
IPs: ips,
|
||||
Port: s.port,
|
||||
TLSPort: s.tlsPort,
|
||||
Apps: s.appsEnabled,
|
||||
}
|
||||
|
||||
templates, err := template.New("").ParseFS(s.layeredFS, "_partials/*.gohtml", "_templates/*.gohtml")
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not parse template", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "index", d); err != nil {
|
||||
logger.Error(r.Context(), "could not render index page", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -14,25 +13,8 @@ import (
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates/idle.html.gotmpl
|
||||
rawIdleTemplate []byte
|
||||
idleTemplate *template.Template
|
||||
)
|
||||
|
||||
func init() {
|
||||
tmpl, err := template.New("").Parse(string(rawIdleTemplate))
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not parse idle template"))
|
||||
}
|
||||
|
||||
idleTemplate = tmpl
|
||||
}
|
||||
|
||||
func (s *Server) startWebServers(ctx context.Context) error {
|
||||
router := chi.NewRouter()
|
||||
|
||||
@ -50,7 +32,6 @@ func (s *Server) startWebServers(ctx context.Context) error {
|
||||
}))
|
||||
}
|
||||
|
||||
router.Get("/", s.handleHome)
|
||||
router.Get("/api/v1/info", s.handleInfo)
|
||||
router.Post("/api/v1/cast", s.handleCast)
|
||||
router.Delete("/api/v1/cast", s.handleReset)
|
||||
@ -63,6 +44,9 @@ func (s *Server) startWebServers(ctx context.Context) error {
|
||||
router.Handle("/api/v1/broadcast/{channelID}", http.HandlerFunc(s.handleBroadcast))
|
||||
}
|
||||
|
||||
router.Get("/", s.handleIndex)
|
||||
router.Get("/*", s.handleStatic)
|
||||
|
||||
if err := s.startHTTPServer(ctx, router); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -193,32 +177,3 @@ func (s *Server) getAllowedOrigins() ([]string, error) {
|
||||
|
||||
return allowedOrigins, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
type templateData struct {
|
||||
IPs []string
|
||||
Port int
|
||||
TLSPort int
|
||||
ID string
|
||||
Apps bool
|
||||
}
|
||||
|
||||
ips, err := network.GetLANIPv4Addrs()
|
||||
if err != nil {
|
||||
logger.Error(r.Context(), "could not retrieve lan ip addresses", logger.CapturedE(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
d := templateData{
|
||||
ID: s.instanceID,
|
||||
IPs: ips,
|
||||
Port: s.port,
|
||||
TLSPort: s.tlsPort,
|
||||
Apps: s.appsEnabled,
|
||||
}
|
||||
|
||||
if err := idleTemplate.Execute(w, d); err != nil {
|
||||
logger.Error(r.Context(), "could not render idle page", logger.CapturedE(errors.WithStack(err)))
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ type Options struct {
|
||||
DefaultApp string
|
||||
AllowedOrigins []string
|
||||
Apps []App
|
||||
UpperLayerDir string
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
@ -42,6 +43,7 @@ func NewOptions(funcs ...OptionFunc) *Options {
|
||||
DefaultApp: "",
|
||||
AllowedOrigins: make([]string, 0),
|
||||
Apps: make([]App, 0),
|
||||
UpperLayerDir: "",
|
||||
}
|
||||
|
||||
for _, fn := range funcs {
|
||||
@ -105,6 +107,12 @@ func WithServiceDiscoveryEnabled(enabled bool) OptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func WithUpperLayerDir(dir string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.UpperLayerDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func NewRandomInstanceID() string {
|
||||
return newRandomInstanceID()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io/fs"
|
||||
|
||||
"forge.cadoles.com/arcad/arcast/pkg/browser"
|
||||
"github.com/gorilla/websocket"
|
||||
@ -32,10 +33,19 @@ type Server struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
layeredFS fs.FS
|
||||
upperLayerDir string
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
serverCtx, cancelServer := context.WithCancel(context.Background())
|
||||
ctx := context.Background()
|
||||
|
||||
if err := s.initLayeredFS(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
serverCtx, cancelServer := context.WithCancel(ctx)
|
||||
|
||||
s.cancel = cancelServer
|
||||
s.ctx = serverCtx
|
||||
@ -92,6 +102,7 @@ func New(browser browser.Browser, funcs ...OptionFunc) *Server {
|
||||
defaultApp: opts.DefaultApp,
|
||||
apps: opts.Apps,
|
||||
serviceDiscoveryEnabled: opts.EnableServiceDiscovery,
|
||||
upperLayerDir: opts.UpperLayerDir,
|
||||
}
|
||||
|
||||
server.upgrader = websocket.Upgrader{
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user