Added register page, email field, finished login + register UI, reduced the number of templates generated.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
WebApp/node_modules
|
WebApp/node_modules
|
||||||
|
Misc
|
||||||
@@ -5,10 +5,6 @@ DB_PASSWORD=1234
|
|||||||
DB_HOST=pagerino_db
|
DB_HOST=pagerino_db
|
||||||
DB_NAME=pager_data
|
DB_NAME=pager_data
|
||||||
|
|
||||||
# App server specific settings
|
|
||||||
# === Chirpstack API
|
|
||||||
CHIRP_API_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJjaGlycHN0YWNrIiwiaXNzIjoiY2hpcnBzdGFjayIsInN1YiI6IjI3YzI5Y2M2LTdjMTUtNDI5Yy1iMjBmLTczNzliZWYzYTI1ZCIsInR5cCI6ImtleSJ9.RsMPIGgPaGluBllRz0Ma_EthxUj3xM9pTPy_uUEAbvk
|
|
||||||
|
|
||||||
# === MQTT broker settings
|
# === MQTT broker settings
|
||||||
MQTT_ADDRESS=ssl://mosquitto:8883
|
MQTT_ADDRESS=ssl://mosquitto:8883
|
||||||
MQTT_CLIENT_ID=app-server
|
MQTT_CLIENT_ID=app-server
|
||||||
@@ -16,7 +12,12 @@ MQTT_QOS=0
|
|||||||
# This App's ID in Chirpstack
|
# This App's ID in Chirpstack
|
||||||
# + matches all IDs
|
# + matches all IDs
|
||||||
# # matches all IDs and all(!) subtopics
|
# # 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
|
# Enviroment variables that will be available for every application
|
||||||
|
|
||||||
@@ -28,7 +29,6 @@ LOG_PREFIX=[Pagerino]
|
|||||||
|
|
||||||
# Main app container name
|
# Main app container name
|
||||||
SERVER_NAME=pagerino-app
|
SERVER_NAME=pagerino-app
|
||||||
SERVER_API_PORT=50222
|
|
||||||
|
|
||||||
# General preferred connection timeout
|
# General preferred connection timeout
|
||||||
TIMEOUT=3s
|
TIMEOUT=3s
|
||||||
@@ -9,6 +9,10 @@ services:
|
|||||||
- app_net
|
- app_net
|
||||||
- pagerino_net
|
- pagerino_net
|
||||||
restart: on-failure:3
|
restart: on-failure:3
|
||||||
|
environment:
|
||||||
|
CHIRP_ADDRESS: ${CHIRP_ADDRESS}
|
||||||
|
SERVER_API_PORT: ${SERVER_API_PORT}
|
||||||
|
APP_ID: ${APP_ID}
|
||||||
|
|
||||||
pagerino_db:
|
pagerino_db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
@@ -27,6 +31,19 @@ services:
|
|||||||
# ports:
|
# ports:
|
||||||
# - "5432:5432"
|
# - "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:
|
volumes:
|
||||||
dbdata:
|
dbdata:
|
||||||
|
|||||||
@@ -73,25 +73,10 @@ type AppData struct {
|
|||||||
APIServer *grpc.Server // Provide API for apps
|
APIServer *grpc.Server // Provide API for apps
|
||||||
MQTTClient mqtt.Client // Get Uplinks and downlinks
|
MQTTClient mqtt.Client // Get Uplinks and downlinks
|
||||||
ChirpGateClient chirp_api.GatewayServiceClient // Check gateway stats and manage them
|
ChirpGateClient chirp_api.GatewayServiceClient // Check gateway stats and manage them
|
||||||
|
ChirpDeviceCLient chirp_api.DeviceServiceClient // Check device stats
|
||||||
Timeout time.Duration // General connection/request timeout
|
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downlink reflects Chirpstack's downlink format
|
// Downlink reflects Chirpstack's downlink format
|
||||||
type Downlink struct {
|
type Downlink struct {
|
||||||
Confirmed bool `json:"confirmed"` // false for no Ack
|
Confirmed bool `json:"confirmed"` // false for no Ack
|
||||||
@@ -155,6 +140,49 @@ func MakeTLSConfig() (*tls.Config, error) {
|
|||||||
}, nil
|
}, 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
|
// VerifyCredentials returns no error if password matches in DB
|
||||||
func VerifyCredentials(username string, password string, data *AppData) error {
|
func VerifyCredentials(username string, password string, data *AppData) error {
|
||||||
if len(username) > MAX_CHAR_LEN || len(password) > MAX_CHAR_LEN {
|
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")
|
log.Println("Uplink is responding in an unrecognized channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//data.printChirpStats()
|
||||||
|
|
||||||
// Query timeout
|
// Query timeout
|
||||||
ctx, close := context.WithTimeout(context.Background(), data.Timeout)
|
ctx, close := context.WithTimeout(context.Background(), data.Timeout)
|
||||||
defer close()
|
defer close()
|
||||||
|
|
||||||
// Get device IDs
|
// Get device IDs
|
||||||
row := data.DBPool.QueryRow(ctx,
|
row := data.DBPool.QueryRow(ctx,
|
||||||
"SELECT id FROM Devices WHERE euid = '$1'",
|
"SELECT id FROM \"Devices\" WHERE euid = '$1';",
|
||||||
uplink.DevEUI,
|
uplink.DevEUI,
|
||||||
)
|
)
|
||||||
|
|
||||||
var dev_id int32
|
var dev_id int32
|
||||||
if err = row.Scan(&dev_id); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Message to DB
|
// Add Message to DB
|
||||||
_, err = data.DBPool.Exec(ctx,
|
_, 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,
|
dev_id, nil, decoded,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(ErrModify.Error() + ": Message")
|
log.Println(ErrModify.Error() + ": " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get decoded custom payload
|
// Get decoded custom payload
|
||||||
pager_msg, err := serder.Deserialize(decoded)
|
pager_msg, err := serder.Deserialize(decoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error() + ": deserialization")
|
log.Println("Deserialization error: " + err.Error())
|
||||||
return
|
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 {
|
for _, field := range pager_msg.Fields {
|
||||||
log.Println(field)
|
log.Println(field)
|
||||||
}
|
}
|
||||||
@@ -354,7 +400,7 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Created MQTT broker connection.")
|
log.Println("Created MQTT broker connection at: " + FindEnv("MQTT_ADDRESS"))
|
||||||
defer client.Disconnect(150)
|
defer client.Disconnect(150)
|
||||||
|
|
||||||
Data.MQTTClient = client
|
Data.MQTTClient = client
|
||||||
@@ -378,7 +424,7 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
|
|||||||
// Serve gRPC
|
// Serve gRPC
|
||||||
go func() {
|
go func() {
|
||||||
if err := grpc_server.Serve(net_listen); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -391,42 +437,23 @@ Expected: [number]{h|m|s|ms|us|ns}...`)
|
|||||||
grpc.WithTransportCredentials(insecure.NewCredentials()), // remove this when using TLS
|
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 {
|
if err != nil {
|
||||||
log.Println("Failed to create a gRPC connection to Chirpstack's API: " + err.Error())
|
log.Println("Failed to create a gRPC connection to Chirpstack's API: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Data.ChirpGateClient = chirp_api.NewGatewayServiceClient(grpc_conn)
|
Data.ChirpGateClient = chirp_api.NewGatewayServiceClient(grpc_conn)
|
||||||
|
Data.ChirpDeviceCLient = chirp_api.NewDeviceServiceClient(grpc_conn)
|
||||||
defer grpc_conn.Close()
|
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
|
Data.printChirpStats()
|
||||||
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]")
|
|
||||||
|
|
||||||
// === Listen for Uplinks
|
// === Listen for Uplinks
|
||||||
// Subscribe to topics
|
// Subscribe to topics
|
||||||
topic := "application/" + FindEnv("APP_ID") + "/device/+/event/up"
|
topic := "application/+/device/+/event/+"
|
||||||
qos, err := strconv.Atoi(FindEnv("MQTT_QOS"))
|
qos, err := strconv.Atoi(FindEnv("MQTT_QOS"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Format misconfiguration for MQTT_QOS: " + err.Error())
|
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))
|
client.Subscribe(topic, byte(qos), makeMQTTHandler(&Data))
|
||||||
log.Println("Subscribed to uplink data on: " + topic + ".")
|
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
|
// === Shutdown
|
||||||
<-Data.ExitSig // Continue here on force quit
|
<-Data.ExitSig // Continue here on force quit
|
||||||
log.Println("The server is shutting down.")
|
log.Println("The application is shutting down.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const (
|
|||||||
NFC_LENGTH = 7
|
NFC_LENGTH = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var MAX_BUFFER_LENGTH = 221
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTooShort error = errors.New("insufficient bytes in message")
|
ErrTooShort error = errors.New("insufficient bytes in message")
|
||||||
ErrNoType error = errors.New("encountered unknown data type")
|
ErrNoType error = errors.New("encountered unknown data type")
|
||||||
@@ -76,6 +78,15 @@ type PagerMessage struct {
|
|||||||
Fields []Field
|
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.
|
// readArray reads a subset of bytes marked with DT_Array.
|
||||||
// This data is copied, it is not parsed.
|
// This data is copied, it is not parsed.
|
||||||
func readArray(data []byte, offset int, dest *[]byte) (int, error) {
|
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
|
// writeField encodes a field into buffer
|
||||||
func writeField(field *Field, buf []byte) ([]byte, error) {
|
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 {
|
switch field.DType {
|
||||||
case DT_Array:
|
case DT_Array:
|
||||||
@@ -179,7 +193,11 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return buf, ErrInvalidData
|
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]
|
size, ok := DataSizeMap[sub_pair.DType]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -192,7 +210,7 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
length := len(val) / size
|
length := len(val) / size
|
||||||
if length > 255 {
|
if length+len(buf)+1 > MAX_BUFFER_LENGTH {
|
||||||
return buf, ErrTooLong
|
return buf, ErrTooLong
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +222,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
return buf, ErrInvalidData
|
return buf, ErrInvalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = append(buf, val)
|
ok, buf = tryAppend(buf, val)
|
||||||
|
if !ok {
|
||||||
|
return buf, ErrTooLong
|
||||||
|
}
|
||||||
|
|
||||||
case DT_Authorization, DT_NFCPair:
|
case DT_Authorization, DT_NFCPair:
|
||||||
val, ok := field.Value.([7]byte)
|
val, ok := field.Value.([7]byte)
|
||||||
@@ -212,7 +233,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
return buf, ErrInvalidData
|
return buf, ErrInvalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
buf = append(buf, val[:]...)
|
ok, buf = tryAppend(buf, val[:]...)
|
||||||
|
if !ok {
|
||||||
|
return buf, ErrTooLong
|
||||||
|
}
|
||||||
|
|
||||||
case DT_Charge, DT_Temperature:
|
case DT_Charge, DT_Temperature:
|
||||||
val, ok := field.Value.(float32)
|
val, ok := field.Value.(float32)
|
||||||
@@ -220,6 +244,9 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
return buf, ErrInvalidData
|
return buf, ErrInvalidData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(buf)+4 > MAX_BUFFER_LENGTH {
|
||||||
|
return buf, ErrTooLong
|
||||||
|
}
|
||||||
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val))
|
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val))
|
||||||
|
|
||||||
case DT_Location:
|
case DT_Location:
|
||||||
@@ -228,6 +255,10 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
return buf, ErrInvalidData
|
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.Latitude))
|
||||||
buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Longitude))
|
buf = binary.LittleEndian.AppendUint64(buf, math.Float64bits(val.Longitude))
|
||||||
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val.Altitude))
|
buf = binary.LittleEndian.AppendUint32(buf, math.Float32bits(val.Altitude))
|
||||||
@@ -241,20 +272,27 @@ func writeField(field *Field, buf []byte) ([]byte, error) {
|
|||||||
return buf, nil
|
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
|
// SplitByLimit splits encoded message into parts
|
||||||
// with maximum length derived from data rate.
|
// with maximum length derived from data rate.
|
||||||
//
|
//
|
||||||
// These messages are not decodable on the server, as
|
// These messages are not decodable on the server, as
|
||||||
// data is fractured. (TODO)
|
// data is fractured. (TODO)
|
||||||
func SplitByLimit(buf []byte, data_rate int) ([][]byte, error) {
|
func SplitByLimit(buf []byte, data_rate int) ([][]byte, error) {
|
||||||
var limit int
|
limit := GetLengthFromDataRate(data_rate)
|
||||||
if data_rate >= 0 && data_rate <= 3 {
|
|
||||||
limit = 51
|
if limit == 0 {
|
||||||
} else if data_rate == 4 {
|
|
||||||
limit = 115
|
|
||||||
} else if data_rate >= 5 && data_rate <= 7 {
|
|
||||||
limit = 222
|
|
||||||
} else {
|
|
||||||
return nil, ErrUndefined
|
return nil, ErrUndefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
136
AppServer/src/uplink.go
Normal file
136
AppServer/src/uplink.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -26,7 +26,11 @@ RUN apk add --no-cache ca-certificates
|
|||||||
## Final build
|
## Final build
|
||||||
# Get
|
# Get
|
||||||
COPY --from=builder /web-server/build ./build/
|
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
|
COPY ./.env ./config/.env
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
# Variables
|
# Variables
|
||||||
CSS_INPUT:=./src/layouts/static/styles/global.css
|
STYLES:=./src/layouts/static/styles
|
||||||
CSS_OUTPUT:=./build/stylesheet.css
|
TW_INPUT:=$(STYLES)/global.css
|
||||||
|
TW_OUTPUT:=$(STYLES)/stylesheet.css
|
||||||
|
TW_CONFIG := tailwind.config.js
|
||||||
|
|
||||||
NAME:=web-app
|
NAME:=web-app
|
||||||
|
|
||||||
all: css
|
all: run
|
||||||
|
|
||||||
# Build Tailwind
|
# Build Tailwind
|
||||||
css: $(CSS_INPUT) ./src/layouts/**/*.tmpl
|
css: $(TW_INPUT) $(TW_CONFIG)
|
||||||
npx tailwindcss -i $(CSS_INPUT) -o $(CSS_OUTPUT) --minify
|
npx tailwindcss -c $(TW_CONFIG) -i $(TW_INPUT) -o $(TW_OUTPUT) --minify
|
||||||
|
|
||||||
# Build App
|
# Build App
|
||||||
go: ./src/main/*.go
|
go: ./src/main/*.go
|
||||||
go build -o ./build/$(NAME) ./src/main.go
|
go build -o ./build/$(NAME) ./src/main.go
|
||||||
|
|
||||||
run: css go
|
run: css down
|
||||||
./build/$(NAME)
|
docker-compose up --build
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
# Clear build
|
# Clear build
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,8 @@ services:
|
|||||||
restart: on-failure:3
|
restart: on-failure:3
|
||||||
ports:
|
ports:
|
||||||
- "8222:8222"
|
- "8222:8222"
|
||||||
|
environment:
|
||||||
|
SERVER_API_PORT: 50222
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
pagerino_net:
|
pagerino_net:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{{ define "footer" -}}
|
|
||||||
<footer class="bg-gray-900 text-gray-300 mt-12">
|
<footer class="bg-gray-900 text-gray-300 mt-12">
|
||||||
<div class="max-w-7xl mx-auto px-6 py-10">
|
<div class="max-w-7xl mx-auto px-6 py-10">
|
||||||
<div class="flex flex-col md:flex-row md:justify-between gap-8">
|
<div class="flex flex-col md:flex-row md:justify-between gap-8">
|
||||||
@@ -41,4 +40,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{{- end }}
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{{ define "meta" -}}
|
|
||||||
<title>{{ .meta.Title }}</title>
|
<title>{{ .meta.Title }}</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/static/{{ if .meta.Icon }}{{ .meta.Icon }}{{ else }}favicon.ico{{ end }}">
|
<link rel="icon" type="image/x-icon" href="/static/{{ if .meta.Icon }}{{ .meta.Icon }}{{ else }}favicon.ico{{ end }}">
|
||||||
|
<link rel="manifest" href="/static/site.webmanifest" />
|
||||||
|
|
||||||
<meta name="author" content="Olek" />
|
<meta name="author" content="Olek" />
|
||||||
<meta name="description"
|
<meta name="description"
|
||||||
content={{ .meta.Description }} />
|
content={{ .meta.Description }} />
|
||||||
{{- end }}
|
|
||||||
@@ -1,84 +1,149 @@
|
|||||||
{{ define "navbar" }}
|
<nav class="fixed top-0 left-0 right-0 z-50 w-full bg-[var(--primary-bg)] shadow-md transition-all duration-300" id="navbar">
|
||||||
<nav class="sticky top-0 w-full bg-[var(--primary-bg)] shadow-md z-50 transition-all duration-300">
|
<div class="w-full mx-auto flex items-center px-4 py-4 md:py-6 transition-all duration-300" id="navbar-inner">
|
||||||
<div id="navbar-inner" class="max-w-7xl mx-auto flex items-center justify-between px-4 py-3 md:py-4 transition-all duration-300">
|
|
||||||
<!-- Logo -->
|
<div class="flex flex-1 justify-start">
|
||||||
<a href="/" class="flex items-start transition-all duration-300" id="logo-container">
|
<a href="/" class="flex items-center space-x-2 shrink-0 transition-opacity hover:opacity-80">
|
||||||
<img src="/static/GORAKLOGO.png" alt="Logo" class="h-12 md:h-16 transition-all duration-300" id="logo-img"/>
|
<img src="/static/Gorak_logo.svg" alt="Gorak logo" class="h-10 md:h-12" />
|
||||||
<span class="ml-2 text-[var(--accent)] font-bold text-xl md:text-2xl">Pagerino</span>
|
</a>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
|
<div id="nav-links" class="hidden nav:flex shrink-0">
|
||||||
|
<ul class="flex items-center space-x-8 font-semibold text-lg">
|
||||||
|
|
||||||
<!-- Nav Links -->
|
|
||||||
<ul class="hidden md:flex space-x-10 text-[var(--text-primary)] font-semibold">
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/network"
|
<a href="/network" class="group flex items-center space-x-2 hover:text-accent transition-all">
|
||||||
class="px-4 py-2 rounded-lg hover:bg-[var(--accent)] hover:text-white transition-colors duration-200">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 hidden lg:block group-hover:scale-110 transition-transform" fill="none" viewBox="0 0 32 32" stroke="currentColor" stroke-width="2">
|
||||||
Network
|
<path xmlns="http://www.w3.org/2000/svg" d="M27 22.25c-0.831 0.002-1.598 0.277-2.215 0.739l0.010-0.007-3.299-2.998c0.82-1.097 1.313-2.479 1.313-3.977 0-1.614-0.572-3.094-1.525-4.249l0.009 0.011 3.644-3.643c0.584 0.391 1.302 0.624 2.074 0.624 2.077 0 3.76-1.683 3.76-3.76s-1.683-3.76-3.76-3.76c-2.077 0-3.76 1.683-3.76 3.76 0 0.773 0.233 1.491 0.633 2.088l-0.009-0.014-3.643 3.643c-1.145-0.944-2.627-1.517-4.244-1.517-0.937 0-1.828 0.192-2.638 0.54l0.044-0.017-1.032-1.874c0.791-0.688 1.288-1.695 1.288-2.819 0-2.060-1.67-3.729-3.729-3.729s-3.729 1.67-3.729 3.729c0 2.060 1.67 3.729 3.729 3.729 0.007 0 0.015-0 0.022-0h-0.001c0.398-0.006 0.778-0.073 1.133-0.194l-0.026 0.008 1.037 1.883c-1.757 1.243-2.89 3.265-2.894 5.553v0.001c0.010 0.697 0.125 1.364 0.33 1.99l-0.013-0.047-1.423 0.603c-0.681-0.971-1.795-1.597-3.056-1.597-2.056 0-3.722 1.666-3.722 3.722s1.666 3.722 3.722 3.722c2.056 0 3.722-1.666 3.722-3.722 0-0.264-0.027-0.521-0.079-0.769l0.004 0.024 1.419-0.602c1.167 2.093 3.367 3.485 5.892 3.485 1.73 0 3.308-0.654 4.5-1.728l-0.006 0.005 3.309 3.007c-0.335 0.544-0.535 1.201-0.539 1.906v0.001c0 2.071 1.679 3.75 3.75 3.75s3.75-1.679 3.75-3.75c0-2.071-1.679-3.75-3.75-3.75v0zM7.69 5c0-1.243 1.007-2.25 2.25-2.25s2.25 1.007 2.25 2.25c0 1.243-1.007 2.25-2.25 2.25v0c-1.242-0.002-2.248-1.008-2.25-2.25v-0zM5 22.92c-1.242-0.001-2.248-1.007-2.248-2.249s1.007-2.249 2.249-2.249c1.242 0 2.248 1.006 2.249 2.248v0c-0.002 1.242-1.008 2.248-2.25 2.25h-0zM27 2.75c1.243 0 2.25 1.007 2.25 2.25s-1.007 2.25-2.25 2.25c-1.243 0-2.25-1.007-2.25-2.25v0c0.002-1.242 1.008-2.248 2.25-2.25h0zM10.69 16c0-0 0-0 0-0.001 0-2.932 2.377-5.309 5.309-5.309s5.309 2.377 5.309 5.309c0 2.932-2.377 5.309-5.309 5.309h-0c-2.931-0.003-5.306-2.378-5.31-5.308v-0zM27 28.25c-1.243 0-2.25-1.007-2.25-2.25s1.007-2.25 2.25-2.25c1.243 0 2.25 1.007 2.25 2.25v0c-0.002 1.242-1.008 2.248-2.25 2.25h-0z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Network</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/map"
|
<a href="/map" class="group flex items-center space-x-2 hover:text-accent transition-all">
|
||||||
class="px-4 py-2 rounded-lg hover:bg-[var(--accent)] hover:text-white transition-colors duration-200">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 hidden lg:block group-hover:scale-110 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
|
||||||
Map
|
<path xmlns="http://www.w3.org/2000/svg" d="M2.28998 7.77998V17.51C2.28998 19.41 3.63998 20.19 5.27998 19.25L7.62998 17.91C8.13998 17.62 8.98998 17.59 9.51998 17.86L14.77 20.49C15.3 20.75 16.15 20.73 16.66 20.44L20.99 17.96C21.54 17.64 22 16.86 22 16.22V6.48998C22 4.58998 20.65 3.80998 19.01 4.74998L16.66 6.08998C16.15 6.37998 15.3 6.40998 14.77 6.13998L9.51998 3.51998C8.98998 3.25998 8.13998 3.27998 7.62998 3.56998L3.29998 6.04998C2.73998 6.36998 2.28998 7.14998 2.28998 7.77998Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path xmlns="http://www.w3.org/2000/svg" d="M8.56 4V17" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path xmlns="http://www.w3.org/2000/svg" d="M15.73 6.62012V20.0001" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Map</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/analytics"
|
<div class="hover:scale-110 transition-transform duration-200">
|
||||||
class="px-4 py-2 rounded-lg hover:bg-[var(--accent)] hover:text-white transition-colors duration-200">
|
<a href="/" class="
|
||||||
Analytics
|
font-extrabold
|
||||||
|
text-3xl
|
||||||
|
tracking-wide
|
||||||
|
|
||||||
|
bg-clip-text
|
||||||
|
text-transparent
|
||||||
|
bg-gradient-to-r
|
||||||
|
from-red-600 via-amber-500 to-orange-600
|
||||||
|
|
||||||
|
animate-[anim-wave_12s_ease-in-out_infinite]
|
||||||
|
bg-[size:var(--custom-bg-size)]"
|
||||||
|
>
|
||||||
|
Pagerino
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="/analytics" class="group flex items-center space-x-2 hover:text-accent transition-all">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 hidden lg:block group-hover:scale-110 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path xmlns="http://www.w3.org/2000/svg" d="M9 7H4.6C4.03995 7 3.75992 7 3.54601 7.10899C3.35785 7.20487 3.20487 7.35785 3.10899 7.54601C3 7.75992 3 8.03995 3 8.6V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H9M9 21H15M9 21L9 4.6C9 4.03995 9 3.75992 9.10899 3.54601C9.20487 3.35785 9.35785 3.20487 9.54601 3.10899C9.75992 3 10.0399 3 10.6 3L13.4 3C13.9601 3 14.2401 3 14.454 3.10899C14.6422 3.20487 14.7951 3.35785 14.891 3.54601C15 3.75992 15 4.03995 15 4.6V21M15 11H19.4C19.9601 11 20.2401 11 20.454 11.109C20.6422 11.2049 20.7951 11.3578 20.891 11.546C21 11.7599 21 12.0399 21 12.6V19.4C21 19.9601 21 20.2401 20.891 20.454C20.7951 20.6422 20.6422 20.7951 20.454 20.891C20.2401 21 19.9601 21 19.4 21H15" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Analytics</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="/devices" class="group flex items-center space-x-2 hover:text-accent transition-all">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 hidden lg:block group-hover:scale-110 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path xmlns="http://www.w3.org/2000/svg" d="M16.2426 7.75738C18.5858 10.1005 18.5858 13.8995 16.2426 16.2427M7.75736 16.2426C5.41421 13.8995 5.41421 10.1005 7.75736 7.75735M4.92893 19.0711C1.02369 15.1658 1.02369 8.8342 4.92893 4.92896M19.0711 4.929C22.9763 8.83424 22.9763 15.1659 19.0711 19.0711M14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8955 10.8954 10 12 10C13.1046 10 14 10.8955 14 12Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>Devices</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User / Login -->
|
<div class="hidden flex-1 justify-end items-center space-x-4 text-lg font-semibold nav:flex">
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
{{- if .user.Authorized -}}
|
{{- if .user.Authorized -}}
|
||||||
<!-- Logged in -->
|
<a href="/account" class="px-3 py-1 rounded-lg text-text-primary
|
||||||
<button id="user-btn" class="hidden md:flex items-center space-x-2 text-[var(--text-primary)] hover:text-[var(--accent)] transition-colors">
|
hover:text-accent hover:bg-accent-muted
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
transition-all duration-200 transform hover:scale-105">
|
||||||
<path d="M12 12c2.7 0 4.9-2.2 4.9-4.9S14.7 2.2 12 2.2 7.1 4.4 7.1 7.1 9.3 12 12 12zm0 2.2c-3.1 0-9.3 1.6-9.3 4.9v2.4h18.6v-2.4c0-3.3-6.2-4.9-9.3-4.9z"/>
|
Account
|
||||||
</svg>
|
</a>
|
||||||
<span id="username-display">Account</span>
|
|
||||||
</button>
|
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
<!-- Logged out -->
|
<a href="/login" class="px-3 py-1 rounded-lg text-accent
|
||||||
<a id="login-btn" href="/login" class="text-[var(--accent)] font-semibold hover:text-[var(--accent-light)] transition-colors">Login</a>
|
hover:text-text-inverse hover:bg-accent
|
||||||
|
transition-all duration-200 transform hover:scale-105">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
<span class="text-text-muted select-none">|</span>
|
||||||
|
<a href="/register" class="px-3 py-1 rounded-lg text-accent
|
||||||
|
hover:text-text-inverse hover:bg-accent
|
||||||
|
transition-all duration-200 transform hover:scale-105">
|
||||||
|
Register
|
||||||
|
</a>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
<!-- Mobile menu button -->
|
||||||
<button class="md:hidden flex items-center text-[var(--text-primary)]">
|
<button id="menu-toggle" class="nav:hidden hover:text-accent transition-colors focus:outline-none">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m0 6H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Dropdown Menu -->
|
||||||
|
<div id="mobile-menu" class="flex nav:hidden flex-col space-y-3 bg-[var(--primary-bg)] px-6 py-4 border-t border-[var(--divider)]">
|
||||||
|
<a href="/network" class="hover:text-accent transition-colors">Network</a>
|
||||||
|
<a href="/map" class="hover:text-accent transition-colors">Map</a>
|
||||||
|
<a href="/analytics" class="hover:text-accent transition-colors">Analytics</a>
|
||||||
|
<a href="/devices" class="hover:text-accent transition-colors">Devices</a>
|
||||||
|
{{- if .user.Authorized -}}
|
||||||
|
<a href="/account" class="hover:text-accent transition-colors">Account</a>
|
||||||
|
{{- else -}}
|
||||||
|
<a href="/login" class="hover:text-accent transition-colors">Login</a>
|
||||||
|
<a href="/register" class="hover:text-accent transition-colors">Register</a>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Navbar collapsing behaviour
|
// Mobile dropdown toggle
|
||||||
const logoImg = document.getElementById('logo-img')
|
const menuToggle = document.getElementById('menu-toggle');
|
||||||
const navbarInner = document.getElementById('navbar-inner')
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
menuToggle?.addEventListener('click', () => {
|
||||||
|
mobileMenu.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navbar compression on scroll
|
||||||
|
const navbarInner = document.getElementById('navbar-inner');
|
||||||
|
let lastScrollY = 0;
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (!ticking) {
|
function updateNavbar() {
|
||||||
window.requestAnimationFrame(() => {
|
if (lastScrollY > 20) {
|
||||||
if (window.scrollY > navbarInner.offsetHeight) {
|
navbarInner.classList.remove('py-4', 'md:py-6');
|
||||||
logoImg.classList.add('h-8', 'md:h-10')
|
navbarInner.classList.add('py-2', 'md:py-3');
|
||||||
logoImg.classList.remove('h-12', 'md:h-16')
|
|
||||||
navbarInner.classList.add('py-2', 'md:py-3')
|
|
||||||
navbarInner.classList.remove('py-3', 'md:py-4')
|
|
||||||
} else {
|
} else {
|
||||||
logoImg.classList.remove('h-8', 'md:h-10')
|
navbarInner.classList.remove('py-2', 'md:py-3');
|
||||||
logoImg.classList.add('h-12', 'md:h-16')
|
navbarInner.classList.add('py-4', 'md:py-6');
|
||||||
navbarInner.classList.remove('py-2', 'md:py-3')
|
}
|
||||||
navbarInner.classList.add('py-3', 'md:py-4')
|
ticking = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ticking = false;
|
window.addEventListener('scroll', () => {
|
||||||
});
|
lastScrollY = window.scrollY;
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(updateNavbar);
|
||||||
ticking = true;
|
ticking = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{{ define "websocket" -}}
|
|
||||||
<script>
|
<script>
|
||||||
let socket; // Global socket variable
|
let socket; // Global socket variable
|
||||||
|
|
||||||
@@ -26,4 +25,3 @@ function initWebSocket() { //TODO Make connection persistent across all pages
|
|||||||
|
|
||||||
window.addEventListener("load", initWebSocket);
|
window.addEventListener("load", initWebSocket);
|
||||||
</script>
|
</script>
|
||||||
{{- end }}
|
|
||||||
@@ -1,5 +1,45 @@
|
|||||||
{{ define "home" -}}
|
<div class="min-h-screen bg-gray-100 p-6">
|
||||||
<h1>
|
<!-- System Status -->
|
||||||
Hello, Pagerino User!
|
<div class="bg-white shadow-lg rounded-2xl p-6 mb-8">
|
||||||
</h1>
|
<h2 class="text-2xl font-bold text-gray-800 mb-4">System Overview</h2>
|
||||||
{{- end }}
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div class="p-4 rounded-lg bg-green-50 border border-green-200">
|
||||||
|
<p class="text-sm text-gray-500">Status</p>
|
||||||
|
<p class="text-lg font-semibold text-green-700">Operational</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg bg-blue-50 border border-blue-200">
|
||||||
|
<p class="text-sm text-gray-500">Active Devices</p>
|
||||||
|
<p class="text-lg font-semibold text-blue-700">42</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 rounded-lg bg-yellow-50 border border-yellow-200">
|
||||||
|
<p class="text-sm text-gray-500">Pending Alerts</p>
|
||||||
|
<p class="text-lg font-semibold text-yellow-700">3</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Devices List -->
|
||||||
|
<div class="bg-white shadow-lg rounded-2xl p-6">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Devices</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Example Device Card -->
|
||||||
|
<div class="p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700">Device A</h3>
|
||||||
|
<p class="text-sm text-gray-500">ID: 12345</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">Status: <span class="text-green-600 font-semibold">Online</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700">Device B</h3>
|
||||||
|
<p class="text-sm text-gray-500">ID: 67890</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">Status: <span class="text-red-600 font-semibold">Offline</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-700">Device C</h3>
|
||||||
|
<p class="text-sm text-gray-500">ID: 54321</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">Status: <span class="text-yellow-600 font-semibold">Pending</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,25 +3,25 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<link href="/static/styles/stylesheet.css" rel="stylesheet">
|
<link href="/static/styles/stylesheet.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
|
||||||
<link rel="manifest" href="/static/site.webmanifest" />
|
|
||||||
|
|
||||||
{{ template "meta" . }}
|
{{ template "meta.tmpl" . }}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100">
|
<body>
|
||||||
{{ template "navbar" . }}
|
<header>
|
||||||
|
{{ template "navbar.tmpl" . }}
|
||||||
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{{ if eq .Page "login" }}
|
{{ if eq .Page "login" }}
|
||||||
{{ template "login" . }}
|
{{ template "login.tmpl" . }}
|
||||||
{{ else if eq .Page "home" }}
|
{{ else if eq .Page "home" }}
|
||||||
{{ template "home" . }}
|
{{ template "home.tmpl" . }}
|
||||||
|
{{ else if eq .Page "register" }}
|
||||||
|
{{ template "register.tmpl" . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
{{ template "footer" . }}
|
{{ template "footer.tmpl" . }}
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,35 +1,28 @@
|
|||||||
{{ define "login" -}}
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
<div class="flex items-center justify-center min-h-screen bg-gray-100">
|
<div class="w-full max-w-sm card">
|
||||||
<form method="POST" action="/login"
|
<h2 class="card-title">Login</h2>
|
||||||
class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md space-y-6">
|
|
||||||
|
|
||||||
<h2 class="text-2xl font-bold text-center text-gray-800">Login</h2>
|
|
||||||
|
|
||||||
|
<form method="POST" action="/login" class="space-y-4">
|
||||||
<!-- Username -->
|
<!-- Username -->
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="block text-sm font-medium text-gray-600">Username</label>
|
<label for="username" class="input-label">Username</label>
|
||||||
<input type="text" id="username" name="username" required
|
<input type="text" id="username" name="username" required class="input-field" />
|
||||||
class="mt-1 w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<!-- Password -->
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="block text-sm font-medium text-gray-600">Password</label>
|
<label for="password" class="input-label">Password</label>
|
||||||
<input type="password" id="password" name="password" required
|
<input type="password" id="password" name="password" required class="input-field" />
|
||||||
class="mt-1 w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit -->
|
<!-- Submit -->
|
||||||
<button type="submit"
|
<button type="submit" class="btn btn-accent">Sign in</button>
|
||||||
class="w-full bg-blue-600 text-white py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors">
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Optional link -->
|
|
||||||
<p class="text-sm text-center text-gray-500">
|
|
||||||
Don’t have an account?
|
|
||||||
<a href="/register" class="text-blue-600 hover:underline">Sign up</a>
|
|
||||||
</p>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Register link -->
|
||||||
|
<p class="mt-6 text-center text-sm text-text-muted">
|
||||||
|
Don’t have an account?
|
||||||
|
<a href="/register" class="text-accent-muted">Register here</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
|
||||||
40
WebApp/src/layouts/pages/register.tmpl
Normal file
40
WebApp/src/layouts/pages/register.tmpl
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<div class="flex items-center justify-center min-h-screen">
|
||||||
|
<div class="w-full max-w-md card">
|
||||||
|
<h2 class="card-title">Create an Account</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="/register" class="space-y-4">
|
||||||
|
<!-- Username -->
|
||||||
|
<div>
|
||||||
|
<label for="username" class="input-label">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required class="input-field" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<div>
|
||||||
|
<label for="email" class="input-label">Email</label>
|
||||||
|
<input type="email" id="email" name="email" required class="input-field" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<div>
|
||||||
|
<label for="password" class="input-label">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required class="input-field" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password -->
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password" class="input-label">Confirm Password</label>
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" required class="input-field" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<button type="submit" class="btn btn-accent">Register</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Link to login -->
|
||||||
|
<p class="mt-6 text-center text-sm">
|
||||||
|
Already have an account?
|
||||||
|
<a href="/login" class="text-accent-muted">Login here</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
133
WebApp/src/layouts/static/Gorak_logo.svg
Normal file
133
WebApp/src/layouts/static/Gorak_logo.svg
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="5505.000000pt" height="1877.000000pt" viewBox="0 0 5505.000000 1877.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,1877.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M4500 18750 c-1433 -102 -2653 -749 -3460 -1835 -507 -683 -833
|
||||||
|
-1501 -964 -2415 -134 -933 -87 -1933 129 -2765 257 -991 771 -1850 1482
|
||||||
|
-2475 766 -674 1694 -1049 2798 -1130 180 -13 669 -13 850 0 694 51 1302 212
|
||||||
|
1860 494 719 364 1280 882 1686 1557 307 511 491 1065 564 1696 26 227 35 524
|
||||||
|
35 1216 l0 667 -2170 0 -2170 0 2 -807 3 -808 1124 -3 1124 -2 -7 -121 c-43
|
||||||
|
-768 -475 -1414 -1171 -1754 -397 -194 -819 -276 -1350 -262 -326 9 -569 48
|
||||||
|
-833 136 -394 130 -688 315 -982 617 -521 536 -823 1305 -889 2262 -14 194
|
||||||
|
-14 642 -1 832 43 612 178 1150 400 1585 365 716 925 1176 1655 1359 360 91
|
||||||
|
827 108 1200 46 924 -154 1563 -713 1800 -1575 l33 -120 1080 -3 1081 -2 -6
|
||||||
|
37 c-99 675 -285 1164 -640 1688 -381 561 -951 1054 -1590 1374 -526 263
|
||||||
|
-1110 428 -1753 497 -172 18 -737 27 -920 14z"/>
|
||||||
|
<path d="M14920 18750 c-1044 -74 -1960 -435 -2730 -1074 -211 -176 -500 -480
|
||||||
|
-677 -714 -606 -800 -942 -1701 -1048 -2804 -37 -390 -43 -890 -15 -1269 93
|
||||||
|
-1267 478 -2280 1194 -3144 129 -156 409 -435 561 -561 673 -556 1455 -896
|
||||||
|
2340 -1018 259 -36 354 -41 750 -41 439 0 570 10 905 66 1383 232 2532 1034
|
||||||
|
3234 2258 356 620 566 1273 671 2081 57 450 66 1114 20 1610 -111 1184 -500
|
||||||
|
2168 -1186 2995 -103 124 -414 436 -534 536 -730 609 -1592 965 -2575 1064
|
||||||
|
-184 19 -729 28 -910 15z m586 -1880 c366 -29 639 -103 949 -255 843 -415
|
||||||
|
1382 -1305 1519 -2510 41 -358 46 -901 11 -1245 -71 -707 -259 -1278 -578
|
||||||
|
-1755 -356 -533 -891 -904 -1497 -1038 -355 -79 -750 -88 -1105 -26 -496 86
|
||||||
|
-941 315 -1290 664 -492 491 -787 1152 -894 2005 -59 467 -53 1107 15 1565
|
||||||
|
145 983 583 1755 1249 2200 123 82 384 211 525 260 338 117 725 164 1096 135z"/>
|
||||||
|
<path d="M50210 18370 l0 -230 345 0 345 0 0 -915 0 -915 280 0 280 0 0 915 0
|
||||||
|
915 345 0 345 0 0 230 0 230 -970 0 -970 0 0 -230z"/>
|
||||||
|
<path d="M52560 17455 l0 -1145 275 0 275 0 2 662 3 663 259 -640 c142 -352
|
||||||
|
263 -650 269 -662 10 -22 14 -23 162 -23 l153 0 232 578 c128 317 247 613 266
|
||||||
|
657 l33 80 1 -657 0 -658 280 0 280 0 0 1145 0 1145 -336 -2 -336 -3 -286
|
||||||
|
-752 c-157 -414 -289 -753 -292 -753 -3 0 -134 340 -291 755 l-284 755 -332 0
|
||||||
|
-333 0 0 -1145z"/>
|
||||||
|
<path d="M21360 13435 l0 -5155 1065 0 1065 0 0 1805 0 1805 947 -2 947 -3
|
||||||
|
912 -1800 912 -1800 1141 -3 c628 -1 1141 0 1141 3 0 4 -1874 3633 -2015 3902
|
||||||
|
l-54 102 142 74 c342 177 589 358 849 625 429 440 700 977 802 1592 66 392 64
|
||||||
|
913 -4 1315 -155 918 -668 1678 -1441 2136 -421 249 -910 421 -1422 500 -364
|
||||||
|
56 -204 52 -2689 56 l-2298 4 0 -5156z m4276 3374 c677 -66 1203 -414 1424
|
||||||
|
-942 148 -353 161 -833 35 -1214 -72 -217 -177 -385 -345 -553 -277 -278 -656
|
||||||
|
-445 -1114 -489 -69 -7 -484 -11 -1128 -11 l-1018 0 0 1610 0 1610 1018 0
|
||||||
|
c644 0 1059 -4 1128 -11z"/>
|
||||||
|
<path d="M33484 18568 c-4 -13 -816 -2329 -1805 -5147 -990 -2819 -1799 -5128
|
||||||
|
-1799 -5133 0 -4 516 -7 1147 -6 l1146 3 375 1123 374 1122 1924 0 1923 0 100
|
||||||
|
-307 c55 -170 217 -672 361 -1118 144 -445 264 -813 266 -817 7 -12 2354 -10
|
||||||
|
2354 1 0 7 -3327 9495 -3587 10229 l-26 72 -1373 0 -1373 0 -7 -22z m1821
|
||||||
|
-3378 c410 -1363 593 -1957 742 -2420 91 -283 168 -521 170 -527 4 -10 -271
|
||||||
|
-13 -1356 -13 -1089 0 -1361 3 -1361 13 0 7 74 235 165 507 245 731 697 2202
|
||||||
|
1100 3575 54 182 100 334 103 339 3 5 72 -225 154 -510 82 -285 209 -719 283
|
||||||
|
-964z"/>
|
||||||
|
<path d="M40690 13435 l0 -5155 1068 2 1067 3 5 1425 5 1425 675 782 c371 430
|
||||||
|
677 780 680 778 3 -2 660 -996 1460 -2209 l1454 -2206 1228 0 1229 0 -53 78
|
||||||
|
c-28 42 -918 1357 -1976 2922 -1059 1565 -1925 2850 -1926 2857 -1 6 836 978
|
||||||
|
1859 2159 1023 1181 1888 2180 1923 2220 l63 74 -1278 -1 -1278 0 -1297 -1517
|
||||||
|
c-713 -834 -1377 -1614 -1475 -1732 -98 -118 -300 -359 -448 -535 -149 -177
|
||||||
|
-398 -474 -555 -662 l-285 -341 -3 2394 -2 2394 -1070 0 -1070 0 0 -5155z"/>
|
||||||
|
<path d="M39676 6494 c-139 -34 -288 -148 -353 -270 -47 -90 -63 -155 -63
|
||||||
|
-263 0 -168 43 -276 154 -387 116 -116 235 -164 408 -165 175 0 295 48 414
|
||||||
|
166 83 83 127 163 151 280 28 136 -2 288 -79 403 -76 112 -211 206 -339 237
|
||||||
|
-79 18 -215 18 -293 -1z"/>
|
||||||
|
<path d="M12230 3295 l0 -3185 475 0 475 0 0 3185 0 3185 -475 0 -475 0 0
|
||||||
|
-3185z"/>
|
||||||
|
<path d="M22300 5223 c-1 -1117 -3 -1256 -16 -1238 -8 11 -43 60 -77 109 -295
|
||||||
|
423 -855 661 -1451 616 -442 -33 -806 -192 -1118 -488 -391 -373 -624 -906
|
||||||
|
-679 -1560 -15 -178 -6 -593 15 -747 79 -560 280 -1009 599 -1342 354 -369
|
||||||
|
799 -553 1334 -553 580 0 1060 251 1355 710 l48 75 0 -347 0 -348 445 0 445 0
|
||||||
|
0 3185 0 3185 -450 0 -450 0 0 -1257z m-1024 -1294 c540 -75 928 -500 1044
|
||||||
|
-1143 45 -247 45 -585 0 -832 -91 -507 -363 -897 -739 -1060 -320 -138 -743
|
||||||
|
-124 -1051 36 -227 119 -425 354 -539 642 -174 440 -170 1185 9 1618 171 414
|
||||||
|
480 673 878 734 108 17 295 20 398 5z"/>
|
||||||
|
<path d="M33850 5245 l0 -615 -397 -2 -398 -3 0 -370 0 -370 397 -3 397 -2 4
|
||||||
|
-1453 c3 -1315 5 -1459 20 -1527 72 -322 234 -537 491 -655 98 -45 220 -82
|
||||||
|
351 -107 96 -17 164 -21 533 -25 l422 -5 0 370 0 370 -292 5 c-318 5 -377 14
|
||||||
|
-471 64 -60 33 -103 94 -131 183 -20 64 -21 89 -24 1423 l-3 1357 461 0 460 0
|
||||||
|
0 375 0 375 -460 0 -460 0 0 615 0 615 -450 0 -450 0 0 -615z"/>
|
||||||
|
<path d="M43000 4724 c-495 -45 -908 -236 -1252 -577 -323 -321 -548 -773
|
||||||
|
-632 -1267 -41 -239 -53 -615 -27 -850 67 -617 285 -1099 662 -1466 386 -375
|
||||||
|
860 -554 1465 -554 339 0 591 45 869 157 581 233 996 696 1118 1246 l15 67
|
||||||
|
-435 -2 -435 -3 -22 -60 c-91 -253 -322 -477 -591 -574 -160 -58 -268 -75
|
||||||
|
-485 -75 -215 -1 -289 8 -440 56 -424 135 -711 489 -811 1000 -11 57 -23 147
|
||||||
|
-26 200 l-6 98 1659 2 1659 3 3 120 c4 149 -14 456 -34 590 -68 459 -231 848
|
||||||
|
-491 1170 -308 383 -734 623 -1243 700 -102 15 -429 27 -520 19z m303 -754
|
||||||
|
c286 -23 520 -123 712 -301 182 -170 319 -439 370 -726 8 -49 15 -98 15 -110
|
||||||
|
l0 -23 -1213 0 -1213 0 9 73 c14 110 61 284 105 386 92 211 237 390 416 509
|
||||||
|
166 111 350 175 551 191 55 5 105 9 111 10 6 0 68 -4 137 -9z"/>
|
||||||
|
<path d="M16352 4709 c-57 -5 -156 -20 -219 -35 -404 -90 -695 -300 -965 -694
|
||||||
|
-16 -24 -17 -8 -17 313 l-1 337 -440 0 -440 0 0 -2260 0 -2260 450 0 449 0 4
|
||||||
|
1418 c4 1552 1 1467 63 1672 83 272 264 486 506 600 290 136 717 143 985 16
|
||||||
|
225 -107 379 -286 456 -532 64 -204 61 -114 64 -1716 l4 -1458 450 0 450 0 -4
|
||||||
|
1547 c-3 1706 0 1615 -67 1878 -172 684 -701 1120 -1423 1175 -146 11 -157 11
|
||||||
|
-305 -1z"/>
|
||||||
|
<path d="M30425 4708 c-238 -26 -466 -92 -670 -192 -486 -239 -745 -637 -745
|
||||||
|
-1146 0 -604 369 -1023 1087 -1234 65 -19 323 -81 573 -136 250 -56 494 -115
|
||||||
|
542 -131 226 -74 382 -191 451 -337 30 -64 32 -73 32 -187 0 -101 -4 -129 -23
|
||||||
|
-180 -71 -185 -265 -336 -513 -399 -280 -71 -643 -55 -875 39 -161 65 -293
|
||||||
|
176 -377 316 -43 73 -94 222 -103 304 l-7 55 -445 0 -445 0 7 -77 c37 -440
|
||||||
|
267 -825 640 -1073 446 -297 1128 -401 1790 -273 612 118 1081 489 1219 964
|
||||||
|
48 165 61 421 31 600 -38 223 -123 387 -284 550 -145 147 -283 235 -520 329
|
||||||
|
-163 65 -308 103 -827 219 -505 113 -597 138 -718 198 -242 121 -347 282 -332
|
||||||
|
508 10 139 63 251 167 353 148 145 340 218 610 229 231 10 423 -32 588 -128
|
||||||
|
75 -44 230 -199 270 -269 44 -79 81 -197 89 -285 l6 -75 433 0 434 0 -6 63
|
||||||
|
c-18 209 -60 380 -133 533 -226 475 -699 782 -1322 859 -136 17 -488 19 -624
|
||||||
|
3z"/>
|
||||||
|
<path d="M47420 4713 c-698 -71 -1215 -409 -1395 -913 -98 -275 -93 -609 14
|
||||||
|
-885 73 -189 245 -395 440 -525 250 -167 461 -239 1116 -385 527 -118 588
|
||||||
|
-135 720 -200 214 -107 318 -242 332 -431 22 -281 -199 -524 -558 -613 -191
|
||||||
|
-47 -469 -52 -671 -11 -189 39 -330 107 -440 215 -124 122 -198 272 -233 478
|
||||||
|
l-6 37 -445 0 -445 0 6 -53 c45 -385 173 -665 415 -907 266 -267 633 -431
|
||||||
|
1110 -496 172 -24 548 -24 720 0 313 42 579 128 794 254 258 152 471 383 569
|
||||||
|
614 114 273 128 617 37 888 -56 169 -121 273 -249 400 -151 151 -348 265 -613
|
||||||
|
355 -167 58 -230 73 -733 185 -478 105 -585 135 -709 195 -158 78 -262 177
|
||||||
|
-307 292 -31 81 -38 237 -14 319 51 178 197 331 393 410 260 105 644 96 886
|
||||||
|
-20 239 -115 394 -324 428 -576 l12 -90 429 0 430 0 -7 98 c-21 322 -149 616
|
||||||
|
-369 849 -271 287 -641 457 -1117 513 -78 9 -459 12 -540 3z"/>
|
||||||
|
<path d="M38135 4666 c-44 -7 -107 -21 -140 -29 -313 -83 -564 -322 -696 -660
|
||||||
|
-15 -39 -32 -73 -38 -75 -8 -3 -11 104 -11 362 l0 366 -430 0 -430 0 0 -2260
|
||||||
|
0 -2260 450 0 450 0 0 1385 c0 944 4 1417 11 1487 48 441 280 742 650 843 128
|
||||||
|
36 319 43 526 21 83 -9 165 -19 182 -22 l31 -6 0 420 0 420 -27 6 c-64 14
|
||||||
|
-448 15 -528 2z"/>
|
||||||
|
<path d="M24220 3127 c0 -1610 0 -1620 51 -1854 105 -489 388 -872 795 -1076
|
||||||
|
461 -231 1077 -237 1520 -14 196 99 414 289 544 476 36 51 70 100 77 109 10
|
||||||
|
14 12 -53 13 -320 l0 -338 440 0 440 0 0 2260 0 2260 -450 0 -449 0 -4 -1412
|
||||||
|
c-4 -1508 -2 -1448 -52 -1638 -82 -315 -281 -546 -570 -665 -158 -65 -246 -80
|
||||||
|
-470 -79 -184 0 -208 3 -299 27 -309 83 -512 271 -613 567 -70 207 -66 105
|
||||||
|
-70 1733 l-4 1467 -449 0 -450 0 0 -1503z"/>
|
||||||
|
<path d="M39380 2370 l0 -2260 450 0 450 0 -2 2258 -3 2257 -447 3 -448 2 0
|
||||||
|
-2260z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.6 KiB |
BIN
WebApp/src/layouts/static/fonts/Inter/InterVariable-Italic.woff2
Normal file
BIN
WebApp/src/layouts/static/fonts/Inter/InterVariable-Italic.woff2
Normal file
Binary file not shown.
BIN
WebApp/src/layouts/static/fonts/Inter/InterVariable.woff2
Normal file
BIN
WebApp/src/layouts/static/fonts/Inter/InterVariable.woff2
Normal file
Binary file not shown.
@@ -1,79 +1,137 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* Global theme variables */
|
/* Inter variable font */
|
||||||
:root {
|
@font-face {
|
||||||
/* Core palette */
|
font-family: 'Inter';
|
||||||
--color-bg: #f8f9fa; /* main background (soft white) */
|
src: url('/static/fonts/Inter/InterVariable.woff2') format('woff2');
|
||||||
--color-bg-alt: #e9ecef; /* alternate background (slightly darker gray) */
|
font-weight: 400;
|
||||||
--color-surface: #111213; /* text surfaces or headers */
|
font-style: normal;
|
||||||
--color-surface-alt: #1c1d1f;
|
font-display: swap;
|
||||||
|
|
||||||
/* Accent tones */
|
|
||||||
--color-text: #111213; /* main text (almost black) */
|
|
||||||
--color-text-muted: #555; /* muted text (medium gray) */
|
|
||||||
--color-text-inverse: #f8f9fa; /* for buttons on dark backgrounds */
|
|
||||||
|
|
||||||
/* Feedback tones */
|
|
||||||
--color-accent: #4f46e5; /* indigo-ish */
|
|
||||||
--color-accent-hover: #6366f1;
|
|
||||||
--color-accent-muted: #818cf8;
|
|
||||||
|
|
||||||
--color-success: #22c55e; /* green-500 */
|
|
||||||
--color-warning: #eab308; /* yellow-500 */
|
|
||||||
--color-danger: #ef4444; /* red-500 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base reset / typography */
|
/* Italic */
|
||||||
body {
|
@font-face {
|
||||||
margin: 0;
|
font-family: 'Inter';
|
||||||
font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
src: url('/static/fonts/Inter/InterVariable-Italic.woff2') format('woff2');
|
||||||
background-color: var(--color-bg);
|
font-weight: 400;
|
||||||
color: var(--color-text);
|
font-style: italic;
|
||||||
line-height: 1.6;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Links */
|
|
||||||
a {
|
@theme {
|
||||||
color: var(--color-accent);
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
|
||||||
text-decoration: none;
|
BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
--color-bg: #f7f6f4;
|
||||||
a:hover {
|
--color-bg-alt: #ebe8e5;
|
||||||
color: var(--color-accent-hover);
|
|
||||||
text-decoration: underline;
|
--color-surface: #1b1c1d;
|
||||||
|
--color-surface-alt: #2b2c2e;
|
||||||
|
|
||||||
|
--color-text: #222;
|
||||||
|
--color-text-muted: #6b6b6b;
|
||||||
|
--color-text-inverse: #f9f9f8;
|
||||||
|
|
||||||
|
--color-accent: #df6b19;
|
||||||
|
--color-accent-hover: #e78625;
|
||||||
|
--color-accent-muted: #f3be81;
|
||||||
|
|
||||||
|
--color-success: #2bb55b;
|
||||||
|
--color-warning: #e2a418;
|
||||||
|
--color-danger: #e0473c;
|
||||||
|
|
||||||
|
--breakpoint-nav: 920px;
|
||||||
|
|
||||||
|
--custom-bg-size: 300% 300%;
|
||||||
|
@keyframes anim-wave {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%; /* Start top-left */
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%; /* Shift to bottom-right */
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%; /* Loop back */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Muted text */
|
|
||||||
/* .text-muted {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Card-like containers */
|
/* ---- Global Defaults ---- */
|
||||||
/* .card {
|
|
||||||
background-color: var(--color-bg-alt);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
/* .btn {
|
@layer base {
|
||||||
display: inline-block;
|
html, body {
|
||||||
padding: 0.5rem 1rem;
|
@apply m-0 p-0 bg-bg text-text font-sans leading-relaxed;
|
||||||
border-radius: 0.5rem;
|
}
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
/* Links */
|
||||||
cursor: pointer;
|
a {
|
||||||
background-color: var(--color-accent);
|
@apply transition-colors duration-200 no-underline;
|
||||||
color: var(--color-text);
|
}
|
||||||
transition: background-color 0.2s ease;
|
a:hover {
|
||||||
|
@apply text-accent-hover;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
@layer components {
|
||||||
background-color: var(--color-accent-hover);
|
/* Card container */
|
||||||
}
|
.card {
|
||||||
|
@apply bg-bg-alt rounded-2xl p-8 shadow-md transition-all duration-200 ease-linear hover:shadow-lg hover:-translate-y-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-muted {
|
/* Card title */
|
||||||
background-color: var(--color-bg-alt);
|
.card-title {
|
||||||
color: var(--color-text-muted);
|
@apply text-surface font-bold text-3xl text-center tracking-tight mb-7;
|
||||||
} */
|
}
|
||||||
|
|
||||||
|
/* Input label */
|
||||||
|
.input-label {
|
||||||
|
@apply block text-sm font-medium text-text-muted mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input field */
|
||||||
|
.input-field {
|
||||||
|
@apply w-full border border-surface-alt rounded-md bg-bg text-text px-3 py-2 text-[0.95rem]
|
||||||
|
shadow-sm transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus {
|
||||||
|
@apply outline-none border-accent ring-2 ring-accent-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accent button */
|
||||||
|
.btn-accent {
|
||||||
|
@apply bg-accent text-text-inverse font-semibold px-4 py-2 rounded-md
|
||||||
|
transition-colors shadow-sm
|
||||||
|
hover:bg-accent-hover hover:shadow-md
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-accent-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic button wrapper */
|
||||||
|
.btn {
|
||||||
|
@apply inline-block w-full font-semibold text-center px-4 py-2 rounded-md transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Reusable button-like link */
|
||||||
|
.nav-link {
|
||||||
|
@apply px-3 py-1 rounded-lg font-semibold transition-all duration-200 transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-primary {
|
||||||
|
@apply text-[var(--accent)] hover:text-[var(--accent-light)]
|
||||||
|
hover:bg-[var(--accent-muted)] hover:shadow-md hover:scale-105;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-secondary {
|
||||||
|
@apply text-[var(--text-primary)] hover:text-[var(--accent)]
|
||||||
|
hover:bg-[var(--accent-muted)] hover:shadow-md hover:scale-105;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Muted separator */
|
||||||
|
.nav-separator {
|
||||||
|
@apply text-[var(--text-muted)] select-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -17,7 +17,6 @@ import (
|
|||||||
api_user "pagerino-web/pager_api/user"
|
api_user "pagerino-web/pager_api/user"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -93,6 +92,7 @@ func CheckAuth(ctx *gin.Context) {
|
|||||||
auth_header := ctx.GetHeader("Authorization")
|
auth_header := ctx.GetHeader("Authorization")
|
||||||
if auth_header == "" { // No token -> login
|
if auth_header == "" { // No token -> login
|
||||||
ctx.Redirect(http.StatusTemporaryRedirect, "/login")
|
ctx.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
ctx.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,9 @@ func CheckAuth(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !token.Valid { // Token expired -> login
|
if !token.Valid { // Token expired -> login
|
||||||
ctx.Redirect(http.StatusTemporaryRedirect, "/login")
|
//ctx.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
ctx.Status(200)
|
||||||
|
ctx.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +200,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// = Load timeout env var
|
||||||
tmout, err := time.ParseDuration(Data.FindEnv("TIMEOUT"))
|
tmout, err := time.ParseDuration(Data.FindEnv("TIMEOUT"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Format misconfiguration: TIMEOUT")
|
log.Println("Format misconfiguration: TIMEOUT")
|
||||||
@@ -206,19 +209,20 @@ func main() {
|
|||||||
|
|
||||||
Data.Timeout = tmout
|
Data.Timeout = tmout
|
||||||
|
|
||||||
dial_opts := []grpc.DialOption{
|
// == User API connection
|
||||||
//grpc.WithBlock(),
|
// dial_opts := []grpc.DialOption{
|
||||||
grpc.WithInsecure(),
|
// //grpc.WithBlock(),
|
||||||
}
|
// grpc.WithInsecure(),
|
||||||
grpc_client, err := grpc.Dial(
|
// }
|
||||||
"app_server:"+Data.FindEnv("SERVER_API_PORT"),
|
// grpc_client, err := grpc.Dial(
|
||||||
dial_opts...,
|
// "app_server:"+Data.FindEnv("SERVER_API_PORT"),
|
||||||
)
|
// dial_opts...,
|
||||||
if err != nil {
|
// )
|
||||||
log.Println("Failed to connect to gRPC API: " + err.Error())
|
// if err != nil {
|
||||||
return
|
// log.Println("Failed to connect to gRPC API: " + err.Error())
|
||||||
}
|
// return
|
||||||
Data.ApiUserClient = api_user.NewUserServiceClient(grpc_client)
|
// }
|
||||||
|
// Data.ApiUserClient = api_user.NewUserServiceClient(grpc_client)
|
||||||
|
|
||||||
// Create auth group
|
// Create auth group
|
||||||
auth_group := router.Group("/", CheckAuth)
|
auth_group := router.Group("/", CheckAuth)
|
||||||
@@ -254,6 +258,18 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Register page (escape auth handler)
|
||||||
|
router.GET("/register", func(ctx *gin.Context) {
|
||||||
|
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||||
|
"meta": PageMeta{
|
||||||
|
Title: "Register",
|
||||||
|
Description: "Pagerino system register page",
|
||||||
|
//Icon: "",
|
||||||
|
},
|
||||||
|
"Page": "register",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Login attempt handler
|
// Login attempt handler
|
||||||
router.POST("/login", func(ctx *gin.Context) {
|
router.POST("/login", func(ctx *gin.Context) {
|
||||||
loginPost(ctx, &Data)
|
loginPost(ctx, &Data)
|
||||||
|
|||||||
8
WebApp/tailwind.config.js
Normal file
8
WebApp/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./src/layouts/static/styles/*.css",
|
||||||
|
"./src/layouts/components/*.tmpl",
|
||||||
|
"./src/layouts/pages/*.tmpl"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user