diff --git a/.gitignore b/.gitignore index 049f0f5..fab81eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -WebApp/node_modules \ No newline at end of file +WebApp/node_modules +Misc \ No newline at end of file diff --git a/AppServer/.env b/AppServer/.env index 4215f1d..e410c20 100755 --- a/AppServer/.env +++ b/AppServer/.env @@ -5,10 +5,6 @@ DB_PASSWORD=1234 DB_HOST=pagerino_db DB_NAME=pager_data -# App server specific settings -# === Chirpstack API -CHIRP_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjaGlycHN0YWNrIiwiaXNzIjoiY2hpcnBzdGFjayIsInN1YiI6IjI3YzI5Y2M2LTdjMTUtNDI5Yy1iMjBmLTczNzliZWYzYTI1ZCIsInR5cCI6ImtleSJ9.RsMPIGgPaGluBllRz0Ma_EthxUj3xM9pTPy_uUEAbvk - # === MQTT broker settings MQTT_ADDRESS=ssl://mosquitto:8883 MQTT_CLIENT_ID=app-server @@ -16,7 +12,12 @@ MQTT_QOS=0 # This App's ID in Chirpstack # + matches all IDs # # matches all IDs and all(!) subtopics -APP_ID=d6ccd2ad-0cf7-46ab-8618-7a5a14b8676d +#APP_ID=d6ccd2ad-0cf7-46ab-8618-7a5a14b8676d +APP_ID=188c5d25-2d6b-43e3-aaf3-07dd0a8c386e + +CHIRP_ADDRESS=chirpstack:8080 +CHIRP_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjaGlycHN0YWNrIiwiaXNzIjoiY2hpcnBzdGFjayIsInN1YiI6IjZlYTA1YTQ1LWUxODgtNDI3Yy1hMWMwLWZiNTQ1NWQ1N2QxZSIsInR5cCI6ImtleSJ9.i6as57Fod4m4oDvHw8LoV2wVz2wjnQFOi_l60zTnGy8 +SERVER_API_PORT=50222 # Enviroment variables that will be available for every application @@ -28,7 +29,6 @@ LOG_PREFIX=[Pagerino] # Main app container name SERVER_NAME=pagerino-app -SERVER_API_PORT=50222 # General preferred connection timeout TIMEOUT=3s \ No newline at end of file diff --git a/AppServer/docker-compose.yml b/AppServer/docker-compose.yml index 7d177d1..327c138 100644 --- a/AppServer/docker-compose.yml +++ b/AppServer/docker-compose.yml @@ -9,6 +9,10 @@ services: - app_net - pagerino_net restart: on-failure:3 + environment: + CHIRP_ADDRESS: ${CHIRP_ADDRESS} + SERVER_API_PORT: ${SERVER_API_PORT} + APP_ID: ${APP_ID} pagerino_db: image: postgres:15 @@ -27,6 +31,19 @@ services: # ports: # - "5432:5432" + migrate: + image: migrate/migrate + networks: + - app_net + volumes: + - ./migrations:/migrations + command: [ + "-path", "/migrations", + "-database", "postgres://${DB_USER}:${DB_PASSWORD}@${DB_NAME}:5432/pagerino_db?sslmode=disable", + "up" + ] + depends_on: + - pagerino_db volumes: dbdata: diff --git a/AppServer/src/main.go b/AppServer/src/main.go index 37c402e..0976ce6 100755 --- a/AppServer/src/main.go +++ b/AppServer/src/main.go @@ -68,28 +68,13 @@ const ( // AppData holds data and connections needed througout the program type AppData struct { - ExitSig chan os.Signal // Use on fatal error to graciously exit, otherwise potential leaks - DBPool *pgxpool.Pool // Execute queries on internal DB on this pool - APIServer *grpc.Server // Provide API for apps - MQTTClient mqtt.Client // Get Uplinks and downlinks - ChirpGateClient chirp_api.GatewayServiceClient // Check gateway stats and manage them - Timeout time.Duration // General connection/request timeout -} - -// Uplink reflects Chirpstack's uplink format -type Uplink struct { - ApplicationID string `json:"applicationID"` - DevEUI string `json:"devEUI"` - FCnt uint32 `json:"fCnt"` - FPort uint8 `json:"fPort"` - Data string `json:"data"` // Base64 payload - RxInfo []struct { - GatewayID string `json:"gatewayID"` - Time string `json:"time"` - RSSI int32 `json:"rssi"` - SNR float64 `json:"snr"` - } `json:"rxInfo"` - DecodedPayload map[string]any `json:"decodedPayload,omitempty"` + ExitSig chan os.Signal // Use on fatal error to graciously exit, otherwise potential leaks + DBPool *pgxpool.Pool // Execute queries on internal DB on this pool + APIServer *grpc.Server // Provide API for apps + MQTTClient mqtt.Client // Get Uplinks and downlinks + ChirpGateClient chirp_api.GatewayServiceClient // Check gateway stats and manage them + ChirpDeviceCLient chirp_api.DeviceServiceClient // Check device stats + Timeout time.Duration // General connection/request timeout } // Downlink reflects Chirpstack's downlink format @@ -155,6 +140,49 @@ func MakeTLSConfig() (*tls.Config, error) { }, nil } +func (data *AppData) printChirpStats() { + ctx_tmout, cancel_ctx := context.WithTimeout(context.Background(), data.Timeout) + defer cancel_ctx() + + resp_gate, err := data.ChirpGateClient.List(ctx_tmout, &chirp_api.ListGatewaysRequest{ + Limit: 10, + Offset: 0, + OrderByDesc: true, + }) + if err != nil { + log.Println("Failed to get Chirpstack's API response: " + err.Error()) + return + } + + log.Println("Found the following gateways (first 10):") + for idx, gate := range resp_gate.Result { + log.Println(strconv.FormatInt(int64(idx), 10) + ": App[" + + gate.TenantId + "] GateID[" + gate.GatewayId + "] Name[" + + gate.Name + "]") + } + log.Println("[END]") + + resp_device, err := data.ChirpDeviceCLient.List(ctx_tmout, &chirp_api.ListDevicesRequest{ + Limit: 10, + Offset: 0, + OrderByDesc: true, + ApplicationId: FindEnv("APP_ID"), + }) + if err != nil { + log.Println("Failed to get Chirpstack's API response: " + err.Error()) + return + } + + log.Println("Found the following devices (first 10):") + for idx, dev := range resp_device.Result { + log.Println(strconv.FormatInt(int64(idx), 10) + ": Name[" + dev.Name + + "] Description:[" + dev.Description + + "] DevEUI[" + dev.DevEui + "]", + ) + } + log.Println("[END]") +} + // VerifyCredentials returns no error if password matches in DB func VerifyCredentials(username string, password string, data *AppData) error { if len(username) > MAX_CHAR_LEN || len(password) > MAX_CHAR_LEN { @@ -233,41 +261,59 @@ func makeMQTTHandler(data *AppData) mqtt.MessageHandler { log.Println("Uplink is responding in an unrecognized channel") } + //data.printChirpStats() + // Query timeout ctx, close := context.WithTimeout(context.Background(), data.Timeout) defer close() // Get device IDs row := data.DBPool.QueryRow(ctx, - "SELECT id FROM Devices WHERE euid = '$1'", + "SELECT id FROM \"Devices\" WHERE euid = '$1';", uplink.DevEUI, ) var dev_id int32 if err = row.Scan(&dev_id); err != nil { - log.Println(ErrNotFound.Error() + ": Device.euid = " + uplink.DevEUI) + if err == pgx.ErrNoRows { + log.Println( + "Adding new device to DB with DeviceEUI[" + uplink.DevEUI + + "] and Name[" + uplink.DeviceName + "]", + ) + data.DBPool.Exec(ctx, + "INSERT INTO \"Devices\" (euid, name) VALUES ($1, $2)", + uplink.DevEUI, uplink.DeviceName, + ) + + } else { + log.Println(err.Error() + ": Device.euid = " + uplink.DevEUI) + return + } return } // Add Message to DB _, err = data.DBPool.Exec(ctx, - "INSERT INTO Messages (sender_id, receiver_id, payload) VALUES ($1, $2, $3)", + "INSERT INTO \"Messages\" (sender_id, receiver_id, payload) VALUES ($1, $2, $3);", dev_id, nil, decoded, ) if err != nil { - log.Println(ErrModify.Error() + ": Message") + log.Println(ErrModify.Error() + ": " + err.Error()) return } // Get decoded custom payload pager_msg, err := serder.Deserialize(decoded) if err != nil { - log.Println(err.Error() + ": deserialization") + log.Println("Deserialization error: " + err.Error()) return } - log.Println("Received Pagerino payload for AppID[", pager_msg.AppID, "]:") + log.Println( + "Received Pagerino payload for AppID[" + + strconv.FormatUint(uint64(pager_msg.AppID), 10) + "]:", + ) for _, field := range pager_msg.Fields { log.Println(field) } @@ -354,7 +400,7 @@ Expected: [number]{h|m|s|ms|us|ns}...`) return } - log.Println("Created MQTT broker connection.") + log.Println("Created MQTT broker connection at: " + FindEnv("MQTT_ADDRESS")) defer client.Disconnect(150) Data.MQTTClient = client @@ -378,7 +424,7 @@ Expected: [number]{h|m|s|ms|us|ns}...`) // Serve gRPC go func() { if err := grpc_server.Serve(net_listen); err != nil { - log.Println("Failed to serve gRPC API") + log.Println("Failed to serve gRPC API: " + err.Error()) return } }() @@ -391,42 +437,23 @@ Expected: [number]{h|m|s|ms|us|ns}...`) grpc.WithTransportCredentials(insecure.NewCredentials()), // remove this when using TLS } - grpc_conn, err := grpc.Dial("chirpstack:8080", dialOpts...) + grpc_conn, err := grpc.Dial(FindEnv("CHIRP_ADDRESS"), dialOpts...) if err != nil { log.Println("Failed to create a gRPC connection to Chirpstack's API: " + err.Error()) return } Data.ChirpGateClient = chirp_api.NewGatewayServiceClient(grpc_conn) + Data.ChirpDeviceCLient = chirp_api.NewDeviceServiceClient(grpc_conn) defer grpc_conn.Close() - log.Println("Created Chirpstack's API connection.") + log.Println("Created Chirpstack's API connection at: " + FindEnv("CHIRP_ADDRESS")) - // Test gRPC connection - ctx_tmout, cancel_ctx = context.WithTimeout(context.Background(), Data.Timeout) - defer cancel_ctx() - - resp, err := Data.ChirpGateClient.List(ctx_tmout, &chirp_api.ListGatewaysRequest{ - Limit: 10, - Offset: 0, - OrderByDesc: true, - }) - if err != nil { - log.Println("Failed to test Chirpstack's API connection: "+err.Error(), resp) - return - } - - log.Println("Found the following gateways (first 10):") - for idx, gate := range resp.Result { - log.Println(strconv.FormatInt(int64(idx), 10) + ": App[" + - gate.TenantId + "] GateID[" + gate.GatewayId + "] Name[" + - gate.Name + "]") - } - log.Println("[END]") + Data.printChirpStats() // === Listen for Uplinks // Subscribe to topics - topic := "application/" + FindEnv("APP_ID") + "/device/+/event/up" + topic := "application/+/device/+/event/+" qos, err := strconv.Atoi(FindEnv("MQTT_QOS")) if err != nil { log.Println("Format misconfiguration for MQTT_QOS: " + err.Error()) @@ -436,7 +463,35 @@ Expected: [number]{h|m|s|ms|us|ns}...`) client.Subscribe(topic, byte(qos), makeMQTTHandler(&Data)) log.Println("Subscribed to uplink data on: " + topic + ".") + // === Simulate uplinks + go func() { + for { + time.Sleep(time.Second * 3) + + uplink, err := MakePayload() + if err != nil { + log.Println("Error while creating uplink: " + err.Error()) + break + } + + payload, err := json.Marshal(*uplink) + if err != nil { + log.Println("Failed to marshal uplink json: " + err.Error()) + break + } + + client.Publish( + "application/"+FindEnv("APP_ID")+"/device/"+DeviceEUI+"/event/up", + 0, + false, + payload, + ) + } + + log.Println("Stopping uplink loop...") + }() + // === Shutdown <-Data.ExitSig // Continue here on force quit - log.Println("The server is shutting down.") + log.Println("The application is shutting down.") } diff --git a/AppServer/src/serder/serder.go b/AppServer/src/serder/serder.go index 90d6f8f..31af387 100644 --- a/AppServer/src/serder/serder.go +++ b/AppServer/src/serder/serder.go @@ -17,6 +17,8 @@ const ( NFC_LENGTH = 7 ) +var MAX_BUFFER_LENGTH = 221 + var ( ErrTooShort error = errors.New("insufficient bytes in message") ErrNoType error = errors.New("encountered unknown data type") @@ -76,6 +78,15 @@ type PagerMessage struct { Fields []Field } +// tryAppend is append function with custom contraints (like length) +func tryAppend[T byte](slice []T, elems ...T) (bool, []T) { + if len(slice)+len(elems) > MAX_BUFFER_LENGTH { + return false, slice + } + + return true, append(slice, elems...) +} + // readArray reads a subset of bytes marked with DT_Array. // This data is copied, it is not parsed. func readArray(data []byte, offset int, dest *[]byte) (int, error) { @@ -171,7 +182,10 @@ func readField(data []byte, offset int) (Field, int, error) { // writeField encodes a field into buffer func writeField(field *Field, buf []byte) ([]byte, error) { - buf = append(buf, field.DType) + ok, buf := tryAppend(buf, field.DType) + if !ok { + return buf, ErrTooLong + } switch field.DType { case DT_Array: @@ -179,7 +193,11 @@ func writeField(field *Field, buf []byte) ([]byte, error) { if !ok { return buf, ErrInvalidData } - buf = append(buf, sub_pair.DType) + + ok, buf = tryAppend(buf, sub_pair.DType) + if !ok { + return buf, ErrTooLong + } size, ok := DataSizeMap[sub_pair.DType] if !ok { @@ -192,7 +210,7 @@ func writeField(field *Field, buf []byte) ([]byte, error) { } length := len(val) / size - if length > 255 { + if length+len(buf)+1 > MAX_BUFFER_LENGTH { return buf, ErrTooLong } @@ -204,7 +222,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) { return buf, ErrInvalidData } - buf = append(buf, val) + ok, buf = tryAppend(buf, val) + if !ok { + return buf, ErrTooLong + } case DT_Authorization, DT_NFCPair: val, ok := field.Value.([7]byte) @@ -212,7 +233,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) { return buf, ErrInvalidData } - buf = append(buf, val[:]...) + ok, buf = tryAppend(buf, val[:]...) + if !ok { + return buf, ErrTooLong + } case DT_Charge, DT_Temperature: val, ok := field.Value.(float32) @@ -220,6 +244,9 @@ func writeField(field *Field, buf []byte) ([]byte, error) { return buf, ErrInvalidData } + if len(buf)+4 > MAX_BUFFER_LENGTH { + return buf, ErrTooLong + } buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val)) case DT_Location: @@ -228,6 +255,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) { return buf, ErrInvalidData } + if len(buf)+20 > MAX_BUFFER_LENGTH { + return buf, ErrTooLong + } + buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Latitude)) buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Longitude)) buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val.Altitude)) @@ -241,20 +272,27 @@ func writeField(field *Field, buf []byte) ([]byte, error) { return buf, nil } +func GetLengthFromDataRate(data_rate int) int { + if data_rate >= 0 && data_rate <= 3 { + return 51 + } else if data_rate == 4 { + return 115 + } else if data_rate >= 5 && data_rate <= 7 { + return 222 + } else { + return 0 + } +} + // SplitByLimit splits encoded message into parts // with maximum length derived from data rate. // // These messages are not decodable on the server, as // data is fractured. (TODO) func SplitByLimit(buf []byte, data_rate int) ([][]byte, error) { - var limit int - if data_rate >= 0 && data_rate <= 3 { - limit = 51 - } else if data_rate == 4 { - limit = 115 - } else if data_rate >= 5 && data_rate <= 7 { - limit = 222 - } else { + limit := GetLengthFromDataRate(data_rate) + + if limit == 0 { return nil, ErrUndefined } diff --git a/AppServer/src/uplink.go b/AppServer/src/uplink.go new file mode 100644 index 0000000..3e50a74 --- /dev/null +++ b/AppServer/src/uplink.go @@ -0,0 +1,136 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "math/rand" + "time" + + "server/serder" +) + +// ---------- CONFIG ---------- +const ( + Broker = "tls://mosquitto:8883" + GatewayID = "54d4d6102c916aed" + DeviceEUI = "1122334455667788" + AppId = "188c5d25-2d6b-43e3-aaf3-07dd0a8c386e" +) + +var FCount uint32 = 0 + +// ---------- DATA STRUCTURES ---------- +type Location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Altitude float64 `json:"altitude"` +} + +type RXInfo struct { + GatewayID string `json:"gatewayID"` + Time time.Time `json:"time"` + RSSI int `json:"rssi"` + LoRaSNR float64 `json:"loRaSNR"` + Channel int `json:"channel"` + RFChain int `json:"rfChain"` + Board int `json:"board"` + Antenna int `json:"antenna"` + Location Location `json:"location"` +} + +type TXInfo struct { + Frequency int `json:"frequency"` + DR int `json:"dr"` + ADR bool `json:"adr"` + CodeRate string `json:"codeRate"` +} + +type Uplink struct { + ApplicationID string `json:"applicationID"` + ApplicationName string `json:"applicationName"` + DeviceName string `json:"deviceName"` + DevEUI string `json:"devEUI"` + RXInfo []RXInfo `json:"rxInfo"` + TXInfo TXInfo `json:"txInfo"` + FCnt uint32 `json:"fCnt"` + FPort uint8 `json:"fPort"` + Data string `json:"data"` // Base64-encoded payload + Object any `json:"object"` // decoded JSON payload if device uses JSON codec +} + +// ---------- UTILITY FUNCTIONS ---------- +// func generateRandomPHY(n int) []byte { +// payload := make([]byte, n) +// for i := 0; i < n; i++ { +// payload[i] = byte(rand.Intn(256)) +// } +// return payload +// } + +// Create a random uplink +func MakePayload() (*Uplink, error) { + + // Generate random sensor data + custom_pay := serder.PagerMessage{ + AppID: 1, + Fields: []serder.Field{ + { + DType: serder.DT_Array, + Value: serder.Field{ + DType: serder.DT_Char, + Value: []byte{ + byte(5), + 'H', 'e', 'l', 'l', 'o', + }, + }, + }, + }, + } + object, err := serder.Serialize(&custom_pay) + + if err != nil { + return nil, err + } + + // Encode object as JSON and then as base64 for Data field + objBytes, _ := json.Marshal(object) + dataB64 := base64.StdEncoding.EncodeToString(objBytes) + + uplink := Uplink{ + ApplicationID: AppId, + ApplicationName: "pagerino-app", + DeviceName: "pager-1", + DevEUI: DeviceEUI, + RXInfo: []RXInfo{ + { + GatewayID: GatewayID, + Time: time.Now().UTC(), + RSSI: -30 + rand.Intn(10), + LoRaSNR: -5 + rand.Float64()*10, + Channel: 0, + RFChain: 0, + Board: 1, + Antenna: 0, + Location: Location{ + Latitude: 52.0 + rand.Float64(), + Longitude: 13.0 + rand.Float64(), + Altitude: 100 + rand.Float64()*50, + }, + }, + }, + TXInfo: TXInfo{ + Frequency: 868100000, + DR: 5, + ADR: true, + CodeRate: "4/5", + }, + FCnt: FCount, + FPort: 1, + Data: dataB64, + Object: object, + } + + FCount++ + + return &uplink, err +} diff --git a/AppServer/tarball.tar.gz b/AppServer/tarball.tar.gz index 29db8f5..7a889cb 100644 Binary files a/AppServer/tarball.tar.gz and b/AppServer/tarball.tar.gz differ diff --git a/WebApp/Dockerfile b/WebApp/Dockerfile index b36e0ca..00e40c4 100755 --- a/WebApp/Dockerfile +++ b/WebApp/Dockerfile @@ -26,7 +26,11 @@ RUN apk add --no-cache ca-certificates ## Final build # Get COPY --from=builder /web-server/build ./build/ -COPY ./src/layouts ./layouts +COPY ./src/layouts/components ./layouts/components +COPY ./src/layouts/pages ./layouts/pages +COPY ./src/layouts/static/styles/global.css ./layouts/static/styles/global.css +COPY ./src/layouts/static/styles/stylesheet.css ./layouts/static/styles/stylesheet.css +COPY ./src/layouts/static ./layouts/static COPY ./.env ./config/.env # Run diff --git a/WebApp/Makefile b/WebApp/Makefile index 5d0c038..06bd8b3 100644 --- a/WebApp/Makefile +++ b/WebApp/Makefile @@ -1,20 +1,26 @@ # Variables -CSS_INPUT:=./src/layouts/static/styles/global.css -CSS_OUTPUT:=./build/stylesheet.css +STYLES:=./src/layouts/static/styles +TW_INPUT:=$(STYLES)/global.css +TW_OUTPUT:=$(STYLES)/stylesheet.css +TW_CONFIG := tailwind.config.js + NAME:=web-app -all: css +all: run # Build Tailwind -css: $(CSS_INPUT) ./src/layouts/**/*.tmpl - npx tailwindcss -i $(CSS_INPUT) -o $(CSS_OUTPUT) --minify +css: $(TW_INPUT) $(TW_CONFIG) + npx tailwindcss -c $(TW_CONFIG) -i $(TW_INPUT) -o $(TW_OUTPUT) --minify # Build App go: ./src/main/*.go go build -o ./build/$(NAME) ./src/main.go -run: css go - ./build/$(NAME) +run: css down + docker-compose up --build + +down: + docker-compose down # Clear build clean: diff --git a/WebApp/build/stylesheet.css b/WebApp/build/stylesheet.css deleted file mode 100644 index 18def4c..0000000 --- a/WebApp/build/stylesheet.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-7xl:80rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:.5rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.sticky{position:sticky}.top-0{top:calc(var(--spacing)*0)}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-12{margin-top:calc(var(--spacing)*12)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.ml-2{margin-left:calc(var(--spacing)*2)}.block{display:block}.flex{display:flex}.hidden{display:none}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-12{height:calc(var(--spacing)*12)}.min-h-screen{min-height:100vh}.w-6{width:calc(var(--spacing)*6)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-md{max-width:var(--container-md)}.flex-1{flex:1}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-10>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*10)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*10)*calc(1 - var(--tw-space-x-reverse)))}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-gray-700{border-color:var(--color-gray-700)}.bg-\[var\(--primary-bg\)\]{background-color:var(--primary-bg)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-white{background-color:var(--color-white)}.p-8{padding:calc(var(--spacing)*8)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-10{padding-block:calc(var(--spacing)*10)}.pt-6{padding-top:calc(var(--spacing)*6)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-\[var\(--accent\)\]{color:var(--accent)}.text-\[var\(--text-primary\)\]{color:var(--text-primary)}.text-blue-600{color:var(--color-blue-600)}.text-gray-300{color:var(--color-gray-300)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-800{color:var(--color-gray-800)}.text-white{color:var(--color-white)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}@media (hover:hover){.hover\:bg-\[var\(--accent\)\]:hover{background-color:var(--accent)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:text-\[var\(--accent\)\]:hover{color:var(--accent)}.hover\:text-\[var\(--accent-light\)\]:hover{color:var(--accent-light)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:48rem){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-10{height:calc(var(--spacing)*10)}.md\:h-16{height:calc(var(--spacing)*16)}.md\:flex-row{flex-direction:row}.md\:justify-between{justify-content:space-between}.md\:py-3{padding-block:calc(var(--spacing)*3)}.md\:py-4{padding-block:calc(var(--spacing)*4)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}}:root{--color-bg:#f8f9fa;--color-bg-alt:#e9ecef;--color-surface:#111213;--color-surface-alt:#1c1d1f;--color-text:#111213;--color-text-muted:#555;--color-text-inverse:#f8f9fa;--color-accent:#4f46e5;--color-accent-hover:#6366f1;--color-accent-muted:#818cf8;--color-success:#22c55e;--color-warning:#eab308;--color-danger:#ef4444}body{background-color:var(--color-bg);color:var(--color-text);margin:0;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.6}a{color:var(--color-accent);text-decoration:none;transition:color .2s}a:hover{color:var(--color-accent-hover);text-decoration:underline}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false} \ No newline at end of file diff --git a/WebApp/docker-compose.yml b/WebApp/docker-compose.yml index ac894e3..38d2a77 100644 --- a/WebApp/docker-compose.yml +++ b/WebApp/docker-compose.yml @@ -9,6 +9,8 @@ services: restart: on-failure:3 ports: - "8222:8222" + environment: + SERVER_API_PORT: 50222 networks: pagerino_net: diff --git a/WebApp/src/layouts/components/footer.tmpl b/WebApp/src/layouts/components/footer.tmpl index 8e2b08d..7da79ea 100644 --- a/WebApp/src/layouts/components/footer.tmpl +++ b/WebApp/src/layouts/components/footer.tmpl @@ -1,4 +1,3 @@ -{{ define "footer" -}}
@@ -41,4 +40,3 @@
-{{- end }} diff --git a/WebApp/src/layouts/components/meta.tmpl b/WebApp/src/layouts/components/meta.tmpl index 5e787f7..1b2d725 100644 --- a/WebApp/src/layouts/components/meta.tmpl +++ b/WebApp/src/layouts/components/meta.tmpl @@ -1,8 +1,7 @@ -{{ define "meta" -}} {{ .meta.Title }} + -{{- end }} \ No newline at end of file + content={{ .meta.Description }} /> \ No newline at end of file diff --git a/WebApp/src/layouts/components/navbar.tmpl b/WebApp/src/layouts/components/navbar.tmpl index 7831f5f..ff2de3c 100644 --- a/WebApp/src/layouts/components/navbar.tmpl +++ b/WebApp/src/layouts/components/navbar.tmpl @@ -1,84 +1,149 @@ -{{ define "navbar" }} -