Compare commits
45 Commits
da1108d441
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d8ab968e | ||
|
|
98ccc8338c | ||
|
|
f97c5af26f | ||
|
|
e8dbf9611e | ||
|
|
770a033429 | ||
|
|
1d657f351c | ||
|
|
bf8b1af964 | ||
|
|
a4b341f421 | ||
|
|
18d7d73005 | ||
|
|
0f2a11af43 | ||
|
|
866ff65eac | ||
|
|
745b44eb10 | ||
|
|
efd5c2007e | ||
|
|
93908f7da5 | ||
|
|
7338d34765 | ||
|
|
acf756765b | ||
|
|
130fe2ad57 | ||
|
|
0b2d10e7b7 | ||
|
|
a0a469bc91 | ||
|
|
6cfe8e3136 | ||
|
|
165ddf8b76 | ||
|
|
4cc4ce4736 | ||
|
|
708620489d | ||
|
|
7e337cbaa7 | ||
|
|
2759a0a525 | ||
|
|
fa5728fba2 | ||
|
|
2139b83174 | ||
|
|
1a7d6cbe11 | ||
|
|
973ca5bb78 | ||
|
|
37f9396428 | ||
|
|
ef3117ea2a | ||
|
|
ca697da0da | ||
|
|
da1ff4e4e5 | ||
|
|
7cdc18bd78 | ||
|
|
6441aa9f81 | ||
|
|
938e4ef348 | ||
|
|
14b4856d47 | ||
|
|
f8a472f10c | ||
|
|
a912c68450 | ||
|
|
f2573f2273 | ||
|
|
4aae0896aa | ||
|
|
8edbbb4347 | ||
|
|
4ff139b217 | ||
|
|
4f9de31cce | ||
|
|
16d782fbe8 |
@@ -1,6 +1,8 @@
|
||||
.db
|
||||
.log
|
||||
.git
|
||||
*.db
|
||||
*.log
|
||||
redis
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.gitignore
|
||||
.gitea
|
||||
28
.gitea/workflows/build_push.yaml
Normal file
28
.gitea/workflows/build_push.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Build and Push Docker Image
|
||||
run-name: Docker image is being constructed 🚀
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Gitea Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.maximilian.link
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: gitea.maximilian.link/maxi/isc:dev
|
||||
2
.idea/.gitignore
generated
vendored
2
.idea/.gitignore
generated
vendored
@@ -6,3 +6,5 @@
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# GitHub Copilot persisted chat sessions
|
||||
/copilot/chatSessions
|
||||
|
||||
6
.idea/InfantrySkillCalculator.iml
generated
6
.idea/InfantrySkillCalculator.iml
generated
@@ -5,9 +5,11 @@
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="bootstrap" level="application" />
|
||||
<orderEntry type="library" name="bootstrap-icons" level="application" />
|
||||
<orderEntry type="library" name="sweetalert2" level="application" />
|
||||
<orderEntry type="library" name="@sweetalert2/theme-dark" level="application" />
|
||||
<orderEntry type="library" name="htmx.org" level="application" />
|
||||
<orderEntry type="library" name="bootstrap" level="application" />
|
||||
<orderEntry type="library" name="bootstrap-icons" level="application" />
|
||||
<orderEntry type="library" name="html2canvas" level="application" />
|
||||
</component>
|
||||
</module>
|
||||
2
.idea/jsLibraryMappings.xml
generated
2
.idea/jsLibraryMappings.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="PROJECT" libraries="{@sweetalert2/theme-dark, bootstrap, bootstrap-icons, sweetalert2}" />
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
13
.idea/runConfigurations/Dockerfile_dev.xml
generated
Normal file
13
.idea/runConfigurations/Dockerfile_dev.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Dockerfile-dev" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="isc:dev" />
|
||||
<option name="buildCliOptions" value="--platform linux/arm64" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
2
.idea/runConfigurations/Redis.xml
generated
2
.idea/runConfigurations/Redis.xml
generated
@@ -3,7 +3,7 @@
|
||||
<deployment type="docker-image">
|
||||
<settings>
|
||||
<option name="imageTag" value="redis:alpine" />
|
||||
<option name="command" value="redis-server --save 5 1 --loglevel warning" />
|
||||
<option name="command" value="redis-server --save 5 1 --loglevel warning --notify-keyspace-events Ex" />
|
||||
<option name="containerName" value="redis-isc" />
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<kind value="PACKAGE" />
|
||||
<package value="InfantrySkillCalculator" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/main.go" />
|
||||
<filePath value="$PROJECT_DIR$/cmd/main.go" />
|
||||
<method v="2">
|
||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Redis" run_configuration_type="docker-deploy" />
|
||||
<option name="RunConfigurationTask" enabled="false" run_configuration_name="Redis" run_configuration_type="docker-deploy" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
15
.idea/webResources.xml
generated
Normal file
15
.idea/webResources.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WebResourcesPaths">
|
||||
<contentEntries>
|
||||
<entry url="file://$PROJECT_DIR$">
|
||||
<entryData>
|
||||
<resourceRoots>
|
||||
<path value="file://$PROJECT_DIR$/static" />
|
||||
<path value="file://$PROJECT_DIR$/templates" />
|
||||
</resourceRoots>
|
||||
</entryData>
|
||||
</entry>
|
||||
</contentEntries>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,14 +1,10 @@
|
||||
FROM golang:1.21-alpine AS builder
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum internal/**/go.mod ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o isc .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -v -o isc .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
|
||||
30
auth.go
30
auth.go
@@ -3,13 +3,13 @@ package main
|
||||
import (
|
||||
"InfantrySkillCalculator/controllers"
|
||||
"InfantrySkillCalculator/models"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"internal/session"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ func getUserPassword(username string) (string, error) {
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatalf("[AUTH] Failed to get password for user %s: %s", username, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func hashPassword(password string) (string, error) {
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
func AuthRequired() gin.HandlerFunc {
|
||||
func ReaderAuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
auth, okAuth := session.GetAuthenticated(c)
|
||||
username, okUser := session.GetUsername(c)
|
||||
@@ -60,6 +60,20 @@ func AuthRequired() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func AuthorAuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
auth, okAuth := session.GetAuthenticated(c)
|
||||
username, okUser := session.GetUsername(c)
|
||||
|
||||
if !okAuth || !okUser || !auth || !controllers.IsUserEnabled(username) || controllers.GetUserRole(username) == models.ReaderRole {
|
||||
redirectToLogin(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminAuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
auth, okAuth := session.GetAuthenticated(c)
|
||||
@@ -74,17 +88,9 @@ func AdminAuthRequired() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func isUserAdmin(c *gin.Context) bool {
|
||||
username, ok := session.GetUsername(c)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return controllers.IsUserAdmin(username)
|
||||
}
|
||||
|
||||
func redirectToLogin(c *gin.Context) {
|
||||
if err := session.InvalidateSession(c); err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatalf("[AUTH] Failed to invalidate session: %s", err)
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
|
||||
@@ -3,10 +3,7 @@ package controllers
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"internal/cache"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -20,14 +17,14 @@ type UpdateCacheInput struct {
|
||||
Score float32 `json:"score" gorm:"default:-1.0"`
|
||||
}
|
||||
|
||||
// GetCacheByPlayerID GET /cache/:player_id?game_tag=TAG
|
||||
// GetCacheByPlayerID GET /cache/:player_id
|
||||
func GetCacheByPlayerID(c *gin.Context) {
|
||||
playerId := utils.StringToUint(c.Param("player_id"))
|
||||
gameTag := c.Request.URL.Query().Get("game_tag")
|
||||
|
||||
val, err := cache.GetScore(playerId, gameTag)
|
||||
val, err := models.PlayerCache.GetScore(playerId)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Warnf("[CACHE] Record not found! Error: %s", err.Error())
|
||||
} else {
|
||||
c.JSON(http.StatusOK, val)
|
||||
}
|
||||
@@ -36,47 +33,51 @@ func GetCacheByPlayerID(c *gin.Context) {
|
||||
// AddCache POST /cache
|
||||
func AddCache(c *gin.Context) {
|
||||
var input AddCacheInput
|
||||
var game models.Game
|
||||
|
||||
if err := c.BindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Warnf("[CACHE] Failed to bind JSON! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var game models.Game
|
||||
if err := FindGameByTag(&game, input.GameTag).Error; err != nil {
|
||||
if err := models.DB.Where("tag = ?", input.GameTag).First(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Game not found!"})
|
||||
utils.Logger.Warnf("[CACHE] Game not found! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := cache.SetScore(input.PlayerID, input.GameTag, input.Score)
|
||||
err := models.PlayerCache.SetScore(input.PlayerID, input.Score)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
|
||||
utils.Logger.Warnf("[CACHE] Cache update failed! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// UpdateCacheByPlayerID PATCH /cache/:id?game=TAG
|
||||
// UpdateCacheByPlayerID PATCH /cache/:id
|
||||
func UpdateCacheByPlayerID(c *gin.Context) {
|
||||
playerID := utils.StringToUint(c.Param("id"))
|
||||
gameTag := c.Request.URL.Query().Get("game")
|
||||
score := utils.StringToFloat(c.PostForm("score"))
|
||||
|
||||
err := cache.SetScore(playerID, gameTag, score)
|
||||
err := models.PlayerCache.SetScore(playerID, score)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Cache update failed! Error: " + err.Error()})
|
||||
utils.Logger.Warnf("[CACHE] Cache update failed! Error: %s", err.Error())
|
||||
} else {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteCacheByPlayerID DELETE /cache/:id?game=TAG
|
||||
// DeleteCacheByPlayerID DELETE /cache/:id
|
||||
func DeleteCacheByPlayerID(c *gin.Context) {
|
||||
playerID := utils.StringToUint(c.Param("id"))
|
||||
gameTag := c.Request.URL.Query().Get("game")
|
||||
|
||||
if err := cache.DeleteScore(playerID, gameTag); err != nil {
|
||||
if err := models.PlayerCache.DeleteScore(playerID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Warnf("[CACHE] Cache deletion failed! Error: %s", err.Error())
|
||||
} else {
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
@@ -84,42 +85,42 @@ func DeleteCacheByPlayerID(c *gin.Context) {
|
||||
|
||||
// DeleteAllCaches DELETE /cache
|
||||
func DeleteAllCaches(c *gin.Context) {
|
||||
if err := cache.PurgeCache(); err != nil {
|
||||
if err := models.PlayerCache.PurgeCache(); err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
utils.Logger.Warnf("[CACHE] Cache purge failed! Error: %s", err.Error())
|
||||
} else {
|
||||
c.String(http.StatusOK, "Purged all caches!")
|
||||
}
|
||||
}
|
||||
|
||||
// GetScoreByPlayerID GET /score/:player_id?game_tag=TAG
|
||||
func GetScoreByPlayerID(c *gin.Context) {
|
||||
func UpdateCacheAfterExpiry(key string) {
|
||||
utils.Logger.Infof("[KeepUpdated] Updating cache for key %s", key)
|
||||
|
||||
playerId, err := models.PlayerCache.GetPlayerIdFromCacheKey(key)
|
||||
if err != nil {
|
||||
utils.Logger.Fatal(err)
|
||||
}
|
||||
|
||||
var player models.Player
|
||||
var gameTag = c.Request.URL.Query().Get("game_tag")
|
||||
var playerId = utils.StringToUint(c.Param("player_id"))
|
||||
if err := models.DB.
|
||||
Model(&models.Player{}).
|
||||
Preload("Clan").
|
||||
Where("id = ?", playerId).
|
||||
First(&player).Error; err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"})
|
||||
utils.Logger.Errorf("[KeepUpdated] Failed to find player with ID %d! Error: %s", playerId, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
score, err := cache.GetScore(player.ID, gameTag)
|
||||
if err != nil || score == -1 {
|
||||
score = utils.CalcPlayerScore(player.Name, gameTag)
|
||||
if player.Clan.KeepUpdated {
|
||||
score, statusCode := GetPlayerScoreNew(player.Name)
|
||||
if score == score && score != -1 { // not NaN
|
||||
if err := cache.SetScore(player.ID, gameTag, score); err != nil {
|
||||
log.Fatal(err)
|
||||
if err := models.PlayerCache.SetScore(player.ID, score); err != nil {
|
||||
utils.Logger.Warnf("[KeepUpdated] Failed to update cache for player %s! Error: %s", player.Name, err.Error())
|
||||
} else {
|
||||
utils.Logger.Infof("[KeepUpdated] Updated cache for player %s", player.Name)
|
||||
}
|
||||
} else {
|
||||
utils.Logger.Warnf("[KeepUpdated] Failed to calculate score for player %s! Status code: %d", player.Name, statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
if score != score || score == -1 { // NaN
|
||||
c.String(http.StatusOK, "<i class=\"bi bi-person-x-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\"></i>")
|
||||
} else if score != -1 {
|
||||
c.String(http.StatusOK, fmt.Sprintf("%.2f", score))
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, "Invalid request!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -26,7 +25,11 @@ type UpdateClanInput struct {
|
||||
// GetAllClans GET /clan
|
||||
func GetAllClans(c *gin.Context) {
|
||||
var clans []models.Clan
|
||||
models.DB.Find(&clans)
|
||||
if err := models.DB.Find(&clans).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[CLAN] Error while getting all clans: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, clans)
|
||||
}
|
||||
@@ -34,7 +37,11 @@ func GetAllClans(c *gin.Context) {
|
||||
// GetAllClansHTML GET /clans_html
|
||||
func GetAllClansHTML(c *gin.Context) {
|
||||
var clans []models.Clan
|
||||
models.DB.Find(&clans)
|
||||
if err := models.DB.Find(&clans).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[CLAN] Error while getting all clans: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var htmlOptions string
|
||||
htmlOptions = `<option disabled selected value>Auswählen...</option>`
|
||||
@@ -49,37 +56,34 @@ func GetAllClansHTML(c *gin.Context) {
|
||||
// AddClan POST /clan
|
||||
func AddClan(c *gin.Context) {
|
||||
var input AddClanInput
|
||||
var clan models.Clan
|
||||
|
||||
if err := c.BindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var clan models.Clan
|
||||
if err := FindClanByName(&clan, input.Name).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Clan with this name already exists!"})
|
||||
return
|
||||
} else if err := FindClanByTag(&clan, input.Tag).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Clan with this tag already exists!"})
|
||||
utils.Logger.Errorf("[CLAN] Error while binding JSON: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
clan = models.Clan{Name: input.Name, Tag: input.Tag, KeepUpdated: input.KeepUpdated}
|
||||
models.DB.Create(&clan)
|
||||
|
||||
if err := models.DB.Create(&clan).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[CLAN] Error while adding clan '[%s] %s': %s", input.Tag, input.Name, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, clan)
|
||||
|
||||
_, err := fmt.Fprintf(utils.GinWriter, "Added clan '"+clan.Name+"' with tag '"+clan.Tag+"'\n")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Logger.Infof("[CLAN] Added clan: [%s] %s", clan.Tag, clan.Name)
|
||||
}
|
||||
|
||||
// GetClanByID GET /clan/:id
|
||||
func GetClanByID(c *gin.Context) {
|
||||
var clan models.Clan
|
||||
|
||||
if err := FindClanByID(&clan, c).Error; err != nil {
|
||||
if err := models.DB.Where("id = ?", c.Param("id")).First(&clan).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[CLAN] Error while getting clan by ID: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,47 +93,57 @@ func GetClanByID(c *gin.Context) {
|
||||
// UpdateClanByID PATCH /clan/:id
|
||||
func UpdateClanByID(c *gin.Context) {
|
||||
var input UpdateClanInput
|
||||
var clan models.Clan
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[CLAN] Error while binding JSON: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
res := models.DB.Model(&models.Clan{}).
|
||||
Where("id = ?", c.Param("id")).
|
||||
Updates(map[string]interface{}{
|
||||
"Name": input.Name,
|
||||
"Tag": input.Tag,
|
||||
"KeepUpdated": input.KeepUpdated,
|
||||
})
|
||||
if res.Error != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": res.Error.Error()})
|
||||
if err := models.DB.First(&clan, c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[CLAN] Error while getting clan by ID: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
oldValues := models.Clan{
|
||||
Name: clan.Name,
|
||||
Tag: clan.Tag,
|
||||
KeepUpdated: clan.KeepUpdated,
|
||||
}
|
||||
|
||||
clan.Name = input.Name
|
||||
clan.Tag = input.Tag
|
||||
clan.KeepUpdated = input.KeepUpdated
|
||||
|
||||
if err := models.DB.Save(&clan).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[CLAN] Error while updating clan '%s': %s", input.Name, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
|
||||
_, err := fmt.Fprintf(utils.GinWriter, "Updated clan '"+input.Name+"' with tag '"+input.Tag+"'\n")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Logger.Infof("[CLAN] Updated clan: [%s] %s -> [%s] %s", oldValues.Tag, oldValues.Name, clan.Tag, clan.Name)
|
||||
}
|
||||
|
||||
// DeleteClanByID DELETE /clan/:id
|
||||
func DeleteClanByID(c *gin.Context) {
|
||||
var clan models.Clan
|
||||
if err := FindClanByID(&clan, c).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
|
||||
if err := models.DB.Clauses(clause.Returning{}).
|
||||
Where("id = ?", c.Param("id")).
|
||||
Delete(&clan).Error; err != nil {
|
||||
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[CLAN] Error while deleting clan '%s': %s", clan.Name, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.DB.Delete(&clan)
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
|
||||
_, err := fmt.Fprintf(utils.GinWriter, "Deleted clan '"+clan.Name+"' with tag '"+clan.Tag+"'\n")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Logger.Infof("[CLAN] Deleted clan: [%s] %s", clan.Tag, clan.Name)
|
||||
}
|
||||
|
||||
// DeleteAllClans DELETE /admin/clan
|
||||
@@ -139,21 +153,12 @@ func DeleteAllClans(c *gin.Context) {
|
||||
Session(&gorm.Session{AllowGlobalUpdate: true}).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(&clans).Error; err != nil {
|
||||
|
||||
c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error())
|
||||
utils.Logger.Errorf("[CLAN] Error while purging all clans: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(clans)))+" clans!")
|
||||
}
|
||||
|
||||
func FindClanByName(out interface{}, name string) *gorm.DB {
|
||||
return models.DB.Where("name = ?", name).First(out)
|
||||
}
|
||||
|
||||
func FindClanByTag(out interface{}, tag string) *gorm.DB {
|
||||
return models.DB.Where("tag = ?", tag).First(out)
|
||||
}
|
||||
|
||||
func FindClanByID(out interface{}, c *gin.Context) *gorm.DB {
|
||||
return models.DB.Where("id = ?", c.Param("id")).First(out)
|
||||
utils.Logger.Infof("[CLAN] Purged %d clans!", len(clans))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"InfantrySkillCalculator/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateCode POST /code
|
||||
@@ -12,6 +13,7 @@ func CreateCode(c *gin.Context) {
|
||||
userRole, ok := c.GetPostForm("user_role")
|
||||
if !ok {
|
||||
c.String(http.StatusBadRequest, "Missing user role")
|
||||
utils.Logger.Error("Missing user role")
|
||||
return
|
||||
}
|
||||
var role models.Role
|
||||
@@ -24,14 +26,18 @@ func CreateCode(c *gin.Context) {
|
||||
role = models.ReaderRole
|
||||
default:
|
||||
c.String(http.StatusInternalServerError, "Invalid user role: "+userRole)
|
||||
utils.Logger.Error("Invalid user role: " + userRole)
|
||||
return
|
||||
}
|
||||
|
||||
newCode := utils.GenerateActivationCode()
|
||||
newCodeObj := models.ActivationCode{Code: newCode, UserRole: role}
|
||||
if err := models.DB.Create(&newCodeObj).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to create new code: "+err.Error())
|
||||
utils.Logger.Error("Failed to create new code: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "Activation code for role '"+string(role)+"': "+newCode)
|
||||
utils.Logger.Info("Activation code for role '" + string(role) + "' created: " + newCode[:4] + strings.Repeat("*", 28))
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package controllers
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -21,7 +22,11 @@ type UpdateGameInput struct {
|
||||
// GetGames GET /game
|
||||
func GetGames(c *gin.Context) {
|
||||
var games []models.Game
|
||||
models.DB.Find(&games)
|
||||
if err := models.DB.Find(&games).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[GAME] Record not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, games)
|
||||
}
|
||||
@@ -29,7 +34,11 @@ func GetGames(c *gin.Context) {
|
||||
// GetGamesHTML GET /game_html
|
||||
func GetGamesHTML(c *gin.Context) {
|
||||
var games []models.Game
|
||||
models.DB.Find(&games)
|
||||
if err := models.DB.Find(&games).Error; err != nil {
|
||||
c.String(http.StatusBadRequest, "")
|
||||
utils.Logger.Errorf("[GAME] Record not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var htmlOptions string
|
||||
htmlOptions = `<option disabled selected value>Auswählen...</option>`
|
||||
@@ -46,6 +55,7 @@ func GetGameByID(c *gin.Context) {
|
||||
var game models.Game
|
||||
if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[GAME] Record not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,11 +67,16 @@ func AddGame(c *gin.Context) {
|
||||
var input AddGameInput
|
||||
if err := c.BindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[GAME] Record not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
game := models.Game{Name: input.Name, Tag: input.Tag}
|
||||
models.DB.Create(&game)
|
||||
if err := models.DB.Create(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[GAME] Record not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, game)
|
||||
}
|
||||
@@ -71,12 +86,14 @@ func UpdateGameByID(c *gin.Context) {
|
||||
var game models.Game
|
||||
if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[GAME] Record not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var input UpdateGameInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[GAME] Could not parse input: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,20 +105,13 @@ func UpdateGameByID(c *gin.Context) {
|
||||
// DeleteGameByID DELETE /game/:id
|
||||
func DeleteGameByID(c *gin.Context) {
|
||||
var game models.Game
|
||||
if err := models.DB.Where("id = ?", c.Param("id")).First(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
|
||||
if err := models.DB.Clauses(clause.Returning{}).Delete(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[GAME] Error while deleting game: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
models.DB.Delete(&game)
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
}
|
||||
|
||||
func FindGame(out interface{}, c *gin.Context) *gorm.DB {
|
||||
return models.DB.Where("id = ?", c.Param("id")).First(&out)
|
||||
}
|
||||
|
||||
func FindGameByTag(out interface{}, tag string) *gorm.DB {
|
||||
return models.DB.Where("tag = ?", tag).First(out)
|
||||
utils.Logger.Infof("[GAME] Deleted game: %s", game.Name)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ import (
|
||||
"InfantrySkillCalculator/utils"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var gameMetrics models.GameMetrics
|
||||
|
||||
func LoadMetrics() {
|
||||
f, err := os.Open("./config/metrics.json")
|
||||
if err != nil {
|
||||
log.Fatal("Failed to open metrics.json: ", err)
|
||||
utils.Logger.Fatal("[METRICS] Failed to open metrics.json: ", err)
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
_ = f.Close()
|
||||
@@ -20,13 +21,31 @@ func LoadMetrics() {
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to read metrics.json: ", err)
|
||||
utils.Logger.Fatal("[METRICS] Failed to read metrics.json: ", err)
|
||||
}
|
||||
|
||||
var metrics models.GameMetrics
|
||||
if err := json.Unmarshal(data, &metrics); err != nil {
|
||||
log.Fatal("Failed to deserialize metrics.json: ", err)
|
||||
utils.Logger.Fatal("[METRICS] Failed to deserialize metrics.json: ", err)
|
||||
}
|
||||
|
||||
utils.GameMetrics = metrics
|
||||
gameMetrics = metrics
|
||||
}
|
||||
|
||||
func GetGameMetric(gameTag string) *models.GameMetric {
|
||||
for _, metric := range gameMetrics.GameMetrics {
|
||||
if metric.GameName == gameTag {
|
||||
return &metric
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindWeaponMetric(weaponMetrics []models.WeaponMetric, weaponCategory string) *models.WeaponMetric {
|
||||
for _, metric := range weaponMetrics {
|
||||
if metric.WeaponCategory == weaponCategory {
|
||||
return &metric
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"internal/cache"
|
||||
"log"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AddPlayerInput struct {
|
||||
@@ -26,7 +24,11 @@ type UpdatePlayerInput struct {
|
||||
// GetAllPlayers GET /player
|
||||
func GetAllPlayers(c *gin.Context) {
|
||||
var players []models.Player
|
||||
models.DB.Find(&players)
|
||||
if err := models.DB.Find(&players).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[PLAYER] Record not found! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, players)
|
||||
}
|
||||
@@ -35,136 +37,143 @@ func GetAllPlayers(c *gin.Context) {
|
||||
func GetPlayersByClanHTML(c *gin.Context) {
|
||||
var players []models.Player
|
||||
clanId := c.Request.URL.Query().Get("clan_id")
|
||||
if clanId == "NaN" {
|
||||
c.String(http.StatusBadRequest, "")
|
||||
return
|
||||
}
|
||||
if err := models.DB.
|
||||
Where("clan_id = ?", utils.StringToUint(clanId)).
|
||||
Find(&players).Error; err != nil {
|
||||
|
||||
c.String(http.StatusBadRequest, "")
|
||||
log.Fatal(err)
|
||||
utils.Logger.Errorf("[PLAYER] Clan not found! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.ReadFile("./templates/player_list_item.html")
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "")
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
playerItem := string(file)
|
||||
|
||||
game, err := GetActiveGame(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "")
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var htmlOptions string
|
||||
var playerIDs []uint
|
||||
for _, player := range players {
|
||||
var score string
|
||||
if val, err := cache.GetScore(player.ID, game.Tag); err != nil || val == -1.0 {
|
||||
score = "<i class=\"bi bi-dash me-2\" style=\"margin-left:0.7rem;\"></i>"
|
||||
} else {
|
||||
score = fmt.Sprintf("%.2f", val)
|
||||
}
|
||||
htmlOptions += fmt.Sprintf(playerItem, player.Name, score, utils.UintToString(player.ID)+"?game_tag="+game.Tag, player.ID, player.ID)
|
||||
playerIDs = append(playerIDs, player.ID)
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/html")
|
||||
c.String(http.StatusOK, htmlOptions)
|
||||
scores, err := models.PlayerCache.GetScores(playerIDs)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "")
|
||||
utils.Logger.Errorf("[PLAYER] Could not get scores! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
userRole := GetUserRoleByCtx(c)
|
||||
|
||||
var data []map[string]interface{}
|
||||
for i, player := range players {
|
||||
|
||||
score := scores[i]
|
||||
var scoreStr string
|
||||
if score == -1.0 {
|
||||
scoreStr = "<i class=\"bi bi-dash me-2\" style=\"margin-left:0.7rem;\"></i>"
|
||||
} else {
|
||||
scoreStr = fmt.Sprintf("%.2f", score)
|
||||
}
|
||||
|
||||
data = append(data, map[string]interface{}{
|
||||
"PlayerName": player.Name,
|
||||
"Score": template.HTML(scoreStr),
|
||||
"PlayerID": player.ID,
|
||||
"UserRole": userRole,
|
||||
})
|
||||
}
|
||||
|
||||
err = utils.PlayerItemTemplate.Execute(c.Writer, data)
|
||||
if err != nil {
|
||||
utils.Logger.Errorf("[PLAYER] Could not execute template! Error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// AddPlayer POST /player
|
||||
func AddPlayer(c *gin.Context) {
|
||||
var input AddPlayerInput
|
||||
var player models.Player
|
||||
|
||||
if err := c.BindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var player models.Player
|
||||
if err := models.DB.Where("name = ?", input.Name).First(&player).Error; err == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Player with this name already exists!"})
|
||||
utils.Logger.Errorf("[PLAYER] Could not bind JSON! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
player = models.Player{Name: input.Name, ClanID: input.ClanID}
|
||||
models.DB.Create(&player)
|
||||
if err := models.DB.Create(&player); err.Error != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error.Error()})
|
||||
utils.Logger.Errorf("[PLAYER] Could not create player! Error: %s", err.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, player)
|
||||
|
||||
_, err := fmt.Fprintf(utils.GinWriter, "Added player '"+player.Name+"'\n")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Logger.Infof("[PLAYER] Added player '%s' in clan %d", input.Name, input.ClanID)
|
||||
}
|
||||
|
||||
// GetPlayerByID GET /player/:id
|
||||
func GetPlayerByID(c *gin.Context) {
|
||||
player := FindPlayerByID(utils.StringToUint(c.Param("id")))
|
||||
if player == nil {
|
||||
var player models.Player
|
||||
|
||||
if err := models.DB.Where("id = ?", c.Param("id")).First(&player).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[PLAYER] Could not find player! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, player)
|
||||
}
|
||||
|
||||
// GetPlayerIDByName GET /playerid/:name
|
||||
func GetPlayerIDByName(c *gin.Context) {
|
||||
var player models.Player
|
||||
|
||||
if err := FindPlayerByName(&player, c).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, player.ID)
|
||||
}
|
||||
|
||||
// UpdatePlayerByID PATCH /player/:id
|
||||
func UpdatePlayerByID(c *gin.Context) {
|
||||
var input UpdatePlayerInput
|
||||
var player models.Player
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[PLAYER] Could not bind JSON! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
res := models.DB.Model(&models.Player{}).
|
||||
Where("id = ?", utils.StringToUint(c.Param("id"))).
|
||||
Updates(map[string]interface{}{
|
||||
"Name": input.Name,
|
||||
"ClanID": input.ClanID,
|
||||
})
|
||||
if res.Error != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": res.Error.Error()})
|
||||
if err := models.DB.First(&player, c.Param("id")).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
utils.Logger.Errorf("[PLAYER] Could not find player! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
oldValues := models.Player{
|
||||
Name: player.Name,
|
||||
ClanID: player.ClanID,
|
||||
}
|
||||
|
||||
player.Name = input.Name
|
||||
player.ClanID = input.ClanID
|
||||
|
||||
if err := models.DB.Save(&player).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[PLAYER] Could not update player! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
|
||||
_, err := fmt.Fprintf(utils.GinWriter, "Updated player '"+input.Name+"'\n")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Logger.Infof("[PLAYER] Updated player: %s -> %s", oldValues.Name, player.Name)
|
||||
}
|
||||
|
||||
// DeletePlayerByID DELETE /player/:id
|
||||
func DeletePlayerByID(c *gin.Context) {
|
||||
player := FindPlayerByID(utils.StringToUint(c.Param("id")))
|
||||
if player == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
|
||||
var player models.Player
|
||||
|
||||
if err := models.DB.Clauses(clause.Returning{}).Where("id = ?", c.Param("id")).Delete(&player).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[PLAYER] Could not delete player! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.DB.Delete(&player)
|
||||
|
||||
c.JSON(http.StatusOK, true)
|
||||
|
||||
_, err := fmt.Fprintf(utils.GinWriter, "Deleted player '"+player.Name+"'\n")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
utils.Logger.Infof("[PLAYER] Deleted player: %s", player.Name)
|
||||
}
|
||||
|
||||
// DeleteAllPlayers DELETE /admin/player
|
||||
@@ -174,22 +183,12 @@ func DeleteAllPlayers(c *gin.Context) {
|
||||
Session(&gorm.Session{AllowGlobalUpdate: true}).
|
||||
Clauses(clause.Returning{}).
|
||||
Delete(&players).Error; err != nil {
|
||||
|
||||
c.String(http.StatusBadRequest, "Purge failed! Error: "+err.Error())
|
||||
utils.Logger.Errorf("[PLAYER] Could not purge players! Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "Purged "+utils.UintToString(uint(len(players)))+" players!")
|
||||
}
|
||||
|
||||
func FindPlayerByID(id uint) *models.Player {
|
||||
var res models.Player
|
||||
if err := models.DB.Where("id = ?", id).First(&res).Error; err != nil {
|
||||
return nil
|
||||
} else {
|
||||
return &res
|
||||
}
|
||||
}
|
||||
|
||||
func FindPlayerByName(out interface{}, c *gin.Context) *gorm.DB {
|
||||
return models.DB.Where("name = ?", c.Param("name")).First(out)
|
||||
utils.Logger.Infof("[PLAYER] Purged %d players!", len(players))
|
||||
}
|
||||
|
||||
223
controllers/tracker_controller.go
Normal file
223
controllers/tracker_controller.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetScoreByPlayerID GET /score/:player_id
|
||||
func GetScoreByPlayerID(c *gin.Context) {
|
||||
var player models.Player
|
||||
|
||||
if err := models.DB.Where("id = ?", c.Param("player_id")).First(&player).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Player not found!"})
|
||||
utils.Logger.Errorf("[SCORE] Player not found: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
score, err := models.PlayerCache.GetScore(player.ID)
|
||||
var statusCode = http.StatusOK
|
||||
if err != nil || score == -1 {
|
||||
score, statusCode = GetPlayerScoreNew(player.Name)
|
||||
if score == score && score != -1 { // not NaN
|
||||
if err := models.PlayerCache.SetScore(player.ID, score); err != nil {
|
||||
utils.Logger.Errorf("[SCORE] Failed to cache player score: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case 200:
|
||||
c.String(200, fmt.Sprintf("%.2f", score))
|
||||
case 404:
|
||||
c.String(200, "<i class=\"bi bi-person-x-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\" data-bs-action=\"tooltip\" data-bs-title=\"Spieler nicht gefunden\"></i>")
|
||||
case 503, 504, 408:
|
||||
c.String(statusCode, "<i class=\"bi bi-cloud-slash-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\"data-bs-action=\"tooltip\" data-bs-title=\"Tracker nicht erreichbar\"></i>")
|
||||
default:
|
||||
c.String(statusCode, "<i class=\"bi bi-exclamation-circle-fill me-2 text-danger fs-5\" style=\"margin-left: 0.69rem;\" data-bs-action=\"tooltip\" data-bs-title=\"Unbekannter Fehler\"></i>")
|
||||
utils.Logger.Warnf("[SCORE] Invalid request! Player: %s, Score: %f", player.Name, score)
|
||||
}
|
||||
}
|
||||
|
||||
// GetScoreByPlayerName POST /score/:player_name
|
||||
func GetScoreByPlayerName(c *gin.Context) {
|
||||
playerName := c.Param("player_name")
|
||||
score, statusCode := GetPlayerScoreNew(playerName)
|
||||
|
||||
switch statusCode {
|
||||
case 200:
|
||||
c.String(200, fmt.Sprintf("%.2f", score))
|
||||
case 404:
|
||||
c.String(200, "Spieler nicht gefunden!")
|
||||
case 503, 504, 408:
|
||||
c.String(statusCode, "Tracker nicht erreichbar!")
|
||||
default:
|
||||
c.String(statusCode, "Ungültige Abfrage!")
|
||||
utils.Logger.Warnf("[SCORE] Invalid request! Player: %s, Score: %f", playerName, score)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPlayerScore(playerName string) (float32, int) {
|
||||
playerData, statusCode := getPlayerData(playerName)
|
||||
if statusCode != 200 {
|
||||
return -1, statusCode
|
||||
}
|
||||
return calcPlayerScore(playerData), statusCode
|
||||
}
|
||||
|
||||
func GetPlayerScoreNew(playerName string) (float32, int) {
|
||||
playerData, statusCode := getPlayerData(playerName)
|
||||
//if statusCode == 408 { // retry once
|
||||
// playerData, statusCode = getPlayerData(playerName)
|
||||
//}
|
||||
if statusCode != 200 {
|
||||
return -1, statusCode
|
||||
}
|
||||
return calcPlayerScoreNew(playerData), statusCode
|
||||
}
|
||||
|
||||
func getPlayerData(playerName string) (*models.TrackerDataJSON, int) {
|
||||
c := http.Client{
|
||||
Timeout: 14 * time.Second, // Cloudflare timeout is 15s
|
||||
}
|
||||
|
||||
reqUri := "https://api.gametools.network/bf2042/stats/?raw=false&format_values=false&name=" + playerName + "&platform=pc"
|
||||
req, err := http.NewRequest("GET", reqUri, nil)
|
||||
if err != nil {
|
||||
utils.Logger.Errorf("[SCORE] Failed to create request: %s", err.Error())
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
var e net.Error
|
||||
if errors.As(err, &e) && e.Timeout() {
|
||||
utils.Logger.Errorf("[SCORE] Request timeout!")
|
||||
return nil, 408
|
||||
}
|
||||
|
||||
utils.Logger.Errorf("[SCORE] Failed to send request: %s", err.Error())
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(res.Body)
|
||||
if res.StatusCode == 404 {
|
||||
utils.Logger.Errorf("[SCORE] User does not exist!")
|
||||
return nil, res.StatusCode
|
||||
} else if res.StatusCode == 503 || res.StatusCode == 504 {
|
||||
utils.Logger.Errorf("[SCORE] Service unavailable!")
|
||||
return nil, res.StatusCode
|
||||
} else if res.StatusCode != 200 {
|
||||
utils.Logger.Errorf("[SCORE] Status code error: %d %s", res.StatusCode, res.Status)
|
||||
return nil, res.StatusCode
|
||||
}
|
||||
|
||||
encoding := res.Header.Get("Content-Encoding")
|
||||
reader, err := utils.DecompressResponseBody(encoding, res.Body)
|
||||
if err != nil {
|
||||
utils.Logger.Errorf("[SCORE] Failed to decompress response body: %s", err.Error())
|
||||
return nil, 0
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
var response models.TrackerDataJSON
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
utils.Logger.Errorf("[SCORE] Failed to read response body: %s", err.Error())
|
||||
return nil, 0
|
||||
}
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
utils.Logger.Errorf("[SCORE] Failed to deserialize tracker API response: %s", err)
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
return &response, 200
|
||||
}
|
||||
|
||||
func calcPlayerScoreNew(playerData *models.TrackerDataJSON) float32 {
|
||||
const KdFactor = 0.5
|
||||
const KpmFactor = 0.3
|
||||
const ObjectiveFactor = 0.15
|
||||
const SupportFactor = 0.05
|
||||
const NormalizeFactor = 1.3
|
||||
|
||||
cleanedKills := playerData.DivKills.ADS + playerData.DivKills.Hip - playerData.DivKills.AI
|
||||
kd := float64(cleanedKills) / float64(playerData.Deaths)
|
||||
kpm := float64(cleanedKills) / float64(playerData.SecondsPlayed/60.0)
|
||||
objective := float64(playerData.XP[0].Ribbons.Objective) / float64(playerData.XP[0].Ribbons.Total)
|
||||
support := float64(playerData.XP[0].Ribbons.Support+playerData.XP[0].Ribbons.Squad) / float64(playerData.XP[0].Ribbons.Total)
|
||||
|
||||
score := (kd * KdFactor) + (kpm * KpmFactor) + (objective * ObjectiveFactor) + (support * SupportFactor)
|
||||
score *= NormalizeFactor
|
||||
|
||||
return float32(score)
|
||||
}
|
||||
|
||||
func calcPlayerScore(playerData *models.TrackerDataJSON) float32 {
|
||||
gameMetrics := GetGameMetric("BF2042")
|
||||
if gameMetrics == nil {
|
||||
utils.Logger.Errorf("[SCORE] No game metrics specified for '%s'", "BF2042")
|
||||
return -1
|
||||
}
|
||||
|
||||
normalizeFactor := gameMetrics.NormalizeFactor
|
||||
topWeaponCount := gameMetrics.TopWeaponCount
|
||||
|
||||
sort.SliceStable(playerData.Weapons, func(i, j int) bool { return playerData.Weapons[i].Kills > playerData.Weapons[j].Kills })
|
||||
var top []models.Weapon
|
||||
|
||||
for _, weapon := range playerData.Weapons {
|
||||
if len(top) >= topWeaponCount || weapon.Kills < 100 {
|
||||
break
|
||||
}
|
||||
|
||||
acc := (float64(weapon.ShotsHit) / float64(weapon.ShotsFired)) * 100
|
||||
kpm := weapon.KPM
|
||||
|
||||
weaponMetrics := FindWeaponMetric(gameMetrics.WeaponMetrics, weapon.Type)
|
||||
if weaponMetrics == nil {
|
||||
utils.Logger.Errorf("[SCORE] No weapon metrics specified for '%s'", weapon.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
accFactor := weaponMetrics.AccuracyFactor
|
||||
kpmFactor := weaponMetrics.KpmFactor
|
||||
|
||||
acc /= accFactor
|
||||
kpm /= kpmFactor
|
||||
weapon.Accuracy = acc
|
||||
weapon.KPM = kpm
|
||||
|
||||
top = append(top, weapon)
|
||||
}
|
||||
|
||||
sumAcc := 0.0
|
||||
sumKpm := 0.0
|
||||
|
||||
for _, w := range top {
|
||||
sumAcc += w.Accuracy
|
||||
sumKpm += w.KPM
|
||||
}
|
||||
accAvg := sumAcc / float64(len(top))
|
||||
kpmAvg := sumKpm / float64(len(top))
|
||||
score := math.Round(((accAvg*kpmAvg)/normalizeFactor)*100) / 100
|
||||
|
||||
return float32(score)
|
||||
}
|
||||
@@ -2,44 +2,38 @@ package controllers
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"log"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"session"
|
||||
)
|
||||
|
||||
func CreateUser(username string, hashedPassword string, enabled bool, usedCode string) {
|
||||
user := models.User{Username: username, Password: hashedPassword, Enabled: enabled}
|
||||
err := models.DB.Create(&user).Error
|
||||
if err != nil {
|
||||
log.Fatalf("Error while creating user: %v", err)
|
||||
var code models.ActivationCode
|
||||
var bf2042 models.Game
|
||||
|
||||
if err := models.DB.Create(&user).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] Error while creating user: %s", err.Error())
|
||||
}
|
||||
|
||||
var code models.ActivationCode
|
||||
err = models.DB.
|
||||
Model(&models.ActivationCode{}).
|
||||
Where("code = ?", usedCode).
|
||||
First(&code).Error
|
||||
if err != nil {
|
||||
log.Fatalf("Error while getting activation code: %v", err)
|
||||
if err := models.DB.Where("code = ?", usedCode).First(&code).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] Error while getting activation code: %s", err.Error())
|
||||
}
|
||||
|
||||
code.UsedForUsername = username
|
||||
err = models.DB.Save(&code).Error
|
||||
if err != nil {
|
||||
log.Fatalf("Error while updating activation code: %v", err)
|
||||
if err := models.DB.Save(&code).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] Error while updating activation code: %s", err.Error())
|
||||
}
|
||||
|
||||
user.UserRole = code.UserRole
|
||||
err = models.DB.Save(&user).Error
|
||||
if err != nil {
|
||||
log.Fatalf("Error while updating user role: %v", err)
|
||||
if err := models.DB.Save(&user).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] Error while updating user role: %s", err.Error())
|
||||
}
|
||||
|
||||
var bf2042 models.Game
|
||||
err = models.DB.
|
||||
Where("tag = ?", "BF2042").
|
||||
First(&bf2042).Error
|
||||
if err != nil {
|
||||
log.Fatalf("Error while getting game: %v", err)
|
||||
if err := models.DB.Where("tag = ?", "BF2042").First(&bf2042).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] Error while getting game: %v", err)
|
||||
}
|
||||
|
||||
userSettings := models.UserSettings{
|
||||
Username: username,
|
||||
ActiveGameID: bf2042.ID,
|
||||
@@ -47,21 +41,51 @@ func CreateUser(username string, hashedPassword string, enabled bool, usedCode s
|
||||
CalcMedian: false,
|
||||
UseCache: true,
|
||||
}
|
||||
models.DB.Create(&userSettings)
|
||||
|
||||
if err := models.DB.Create(&userSettings).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] Error while creating user settings: %s", err.Error())
|
||||
}
|
||||
|
||||
utils.Logger.Infof("[USER] Created user: %s", username)
|
||||
}
|
||||
|
||||
func IsUserEnabled(username string) bool {
|
||||
var user models.User
|
||||
models.DB.Where("username = ?", username).First(&user)
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
utils.Logger.Warnf("[USER] IsUserEnabled: Error while getting user: %s", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return user.Enabled
|
||||
}
|
||||
|
||||
func GetUserRole(username string) models.Role {
|
||||
var user models.User
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] GetUserRole: Error while getting user: %s", err.Error())
|
||||
}
|
||||
|
||||
return user.UserRole
|
||||
}
|
||||
|
||||
func GetUserRoleByCtx(c *gin.Context) models.Role {
|
||||
username, ok := session.GetUsername(c)
|
||||
|
||||
if !ok {
|
||||
return models.ReaderRole
|
||||
}
|
||||
|
||||
return GetUserRole(username)
|
||||
}
|
||||
|
||||
func IsUserAdmin(username string) bool {
|
||||
var user models.User
|
||||
err := models.DB.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return false
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
utils.Logger.Fatalf("[USER] IsUserAdmin: Error while getting user: %s", err.Error())
|
||||
}
|
||||
|
||||
return user.UserRole == models.AdminRole
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"errors"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"session"
|
||||
@@ -22,11 +22,13 @@ func GetSettings(c *gin.Context) {
|
||||
username, ok := session.GetUsername(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"})
|
||||
utils.Logger.Errorf("[SETTINGS] User not logged in: %s", username)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
|
||||
utils.Logger.Errorf("[SETTINGS] No settings available for user %s", username)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,30 +42,6 @@ func GetSettings(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, sanitizedSettings)
|
||||
}
|
||||
|
||||
func GetActiveGame(c *gin.Context) (models.Game, error) {
|
||||
var settings models.UserSettings
|
||||
var game models.Game
|
||||
username, ok := session.GetUsername(c)
|
||||
|
||||
if !ok {
|
||||
err := errors.New("not logged in")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return models.Game{}, err
|
||||
}
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
|
||||
return models.Game{}, err
|
||||
}
|
||||
|
||||
if err := models.DB.Where("id = ?", settings.ActiveGameID).First(&game).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No active game available!"})
|
||||
return models.Game{}, err
|
||||
}
|
||||
|
||||
return game, nil
|
||||
}
|
||||
|
||||
// UpdateSettings PATCH /settings
|
||||
func UpdateSettings(c *gin.Context) {
|
||||
var settings models.UserSettings
|
||||
@@ -71,21 +49,28 @@ func UpdateSettings(c *gin.Context) {
|
||||
username, ok := session.GetUsername(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Not logged in!"})
|
||||
utils.Logger.Errorf("[SETTINGS] User not logged in: %s", username)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.DB.Where("username = ?", username).First(&settings).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No settings available!"})
|
||||
utils.Logger.Errorf("[SETTINGS] No settings available for user %s", username)
|
||||
return
|
||||
}
|
||||
|
||||
var input UpdateSettingsInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[SETTINGS] Failed to bind JSON: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.DB.Model(&settings).Updates(input)
|
||||
if err := models.DB.Model(&settings).Updates(input).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
utils.Logger.Errorf("[SETTINGS] Failed to update settings: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
- "127.0.0.1:8000:8000"
|
||||
environment:
|
||||
- GO_ENV=production
|
||||
- REDIS_ADDRESS=redis
|
||||
- REDIS_ADDRESS=redis:6379
|
||||
depends_on:
|
||||
- redis
|
||||
volumes:
|
||||
@@ -16,3 +16,6 @@ services:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
command: redis-server --save 5 1 --loglevel warning
|
||||
volumes:
|
||||
- ./redis:/data
|
||||
58
go.mod
58
go.mod
@@ -1,58 +1,66 @@
|
||||
module InfantrySkillCalculator
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.6
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
golang.org/x/crypto v0.9.0
|
||||
gorm.io/gorm v1.25.5
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require internal/cache v1.0.0
|
||||
|
||||
replace internal/cache => ./internal/cache
|
||||
|
||||
require (
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
internal/session v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
)
|
||||
|
||||
replace internal/session => ./internal/session
|
||||
|
||||
replace internal/burst_queue => ./internal/burst_queue
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/klauspost/compress v1.17.11
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
117
go.sum
117
go.sum
@@ -1,43 +1,38 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
|
||||
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -49,76 +44,83 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
@@ -127,4 +129,5 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
5
go.work
5
go.work
@@ -1,7 +1,10 @@
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
toolchain go1.23.6
|
||||
|
||||
use (
|
||||
.
|
||||
internal/burst_queue
|
||||
internal/cache
|
||||
internal/session
|
||||
)
|
||||
42
go.work.sum
42
go.work.sum
@@ -1,15 +1,56 @@
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
@@ -28,4 +69,5 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
||||
|
||||
88
internal/burst_queue/burst_queue.go
Normal file
88
internal/burst_queue/burst_queue.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package burst_queue
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BurstQueue is a queue that will flush its entries when it reaches a certain size or after a certain timeout.
|
||||
type BurstQueue struct {
|
||||
maxQueueSize uint
|
||||
burstTimeout time.Duration
|
||||
burstCallback func(entries []interface{}, elapsed time.Duration)
|
||||
entries []interface{}
|
||||
startTime time.Time
|
||||
flushMu sync.Mutex
|
||||
timerMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewBurstQueue creates a new BurstQueue.
|
||||
// maxQueueSize is the maximum number of entries to queue before flushing.
|
||||
// burstTimeout is the maximum time to wait before flushing.
|
||||
// burstCallback is the function to call when flushing.
|
||||
func NewBurstQueue(maxQueueSize uint, burstTimeout time.Duration, burstCallback func(entries []interface{}, elapsed time.Duration)) *BurstQueue {
|
||||
return &BurstQueue{
|
||||
maxQueueSize: maxQueueSize,
|
||||
burstTimeout: burstTimeout,
|
||||
burstCallback: burstCallback,
|
||||
}
|
||||
}
|
||||
|
||||
// startTimeout starts the timeout.
|
||||
func (bq *BurstQueue) startTimeout() {
|
||||
newStart := time.Now()
|
||||
|
||||
bq.timerMu.Lock()
|
||||
bq.startTime = newStart
|
||||
bq.timerMu.Unlock()
|
||||
|
||||
time.Sleep(bq.burstTimeout)
|
||||
|
||||
bq.timerMu.Lock()
|
||||
shouldFlush := bq.startTime.Equal(newStart) && !bq.startTime.IsZero()
|
||||
bq.timerMu.Unlock()
|
||||
|
||||
if shouldFlush {
|
||||
bq.FlushEntries()
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds an entry to the queue and restarts the timeout.
|
||||
func (bq *BurstQueue) Add(entry interface{}) {
|
||||
go bq.startTimeout()
|
||||
|
||||
bq.entries = append(bq.entries, entry)
|
||||
|
||||
if len(bq.entries) >= int(bq.maxQueueSize) {
|
||||
bq.FlushEntries()
|
||||
}
|
||||
}
|
||||
|
||||
// FlushEntries flushes the queue.
|
||||
func (bq *BurstQueue) FlushEntries() {
|
||||
bq.timerMu.Lock()
|
||||
if bq.startTime.IsZero() || bq.entries == nil {
|
||||
bq.timerMu.Unlock()
|
||||
return // not started yet or already flushed
|
||||
}
|
||||
actualTimeout := time.Since(bq.startTime)
|
||||
bq.startTime = time.Time{}
|
||||
bq.timerMu.Unlock()
|
||||
|
||||
bq.flushMu.Lock()
|
||||
bq.burstCallback(bq.entries, actualTimeout)
|
||||
bq.entries = nil
|
||||
bq.flushMu.Unlock()
|
||||
}
|
||||
|
||||
func ConvertSlice[T any](src []interface{}) []T {
|
||||
dst := make([]T, len(src))
|
||||
for i, v := range src {
|
||||
var ok bool
|
||||
if dst[i], ok = v.(T); !ok {
|
||||
log.Fatalf("item at index %d is not of type %T", i, dst[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
3
internal/burst_queue/go.mod
Normal file
3
internal/burst_queue/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module burst_queue
|
||||
|
||||
go 1.23
|
||||
116
internal/cache/cache.go
vendored
116
internal/cache/cache.go
vendored
@@ -1,18 +1,59 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
func GetValue(key string) (string, error) {
|
||||
val, err := models.Cache.Get(ctx, key).Result()
|
||||
type PlayerCache struct {
|
||||
cache *redis.Client
|
||||
lifetime time.Duration
|
||||
expireSub *redis.PubSub
|
||||
expireCallback func(key string)
|
||||
}
|
||||
|
||||
func NewPlayerCache(address string, lifetime time.Duration, expireCallback func(key string)) *PlayerCache {
|
||||
pc := &PlayerCache{
|
||||
redis.NewClient(
|
||||
&redis.Options{
|
||||
Addr: address,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
}),
|
||||
lifetime,
|
||||
nil,
|
||||
expireCallback,
|
||||
}
|
||||
pc.expireSub = pc.cache.PSubscribe(ctx, "__keyevent@0__:expired")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
msg, err := pc.expireSub.ReceiveMessage(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
pc.expireCallback(msg.Payload)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return pc
|
||||
}
|
||||
|
||||
func (pc *PlayerCache) Connect() error {
|
||||
_, err := pc.cache.Ping(ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func (pc *PlayerCache) GetValue(key string) (string, error) {
|
||||
val, err := pc.cache.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return "", err // cache miss or error
|
||||
} else {
|
||||
@@ -20,36 +61,71 @@ func GetValue(key string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func SetValue(key string, value interface{}) error {
|
||||
return models.Cache.Set(ctx, key, value, utils.PlayerCacheLifetime).Err()
|
||||
func (pc *PlayerCache) SetValue(key string, value interface{}, lifetime time.Duration) error {
|
||||
return pc.cache.Set(ctx, key, value, lifetime).Err()
|
||||
}
|
||||
|
||||
func SetScore(playerId uint, gameTag string, score float32) error {
|
||||
key := GetPlayerCacheKey(playerId, gameTag)
|
||||
return SetValue(key, score)
|
||||
func (pc *PlayerCache) SetScore(playerId uint, score float32) error {
|
||||
key := getPlayerCacheKey(playerId)
|
||||
return pc.SetValue(key, score, pc.lifetime)
|
||||
}
|
||||
|
||||
func GetScore(playerId uint, gameTag string) (float32, error) {
|
||||
key := GetPlayerCacheKey(playerId, gameTag)
|
||||
val, err := GetValue(key)
|
||||
func (pc *PlayerCache) GetScore(playerId uint) (float32, error) {
|
||||
key := getPlayerCacheKey(playerId)
|
||||
val, err := pc.GetValue(key)
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return -1.0, nil // cache miss
|
||||
} else if err != nil {
|
||||
return -1.0, err // cache error
|
||||
} else {
|
||||
return utils.StringToFloat(val), nil // cache hit
|
||||
valFloat, _ := strconv.ParseFloat(val, 32)
|
||||
return float32(valFloat), nil // cache hit
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteScore(playerId uint, gameTag string) error {
|
||||
key := GetPlayerCacheKey(playerId, gameTag)
|
||||
return models.Cache.Del(ctx, key).Err()
|
||||
func (pc *PlayerCache) GetScores(playerIds []uint) ([]float32, error) {
|
||||
vals, err := pc.cache.Pipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||
for _, id := range playerIds {
|
||||
key := getPlayerCacheKey(id)
|
||||
pipe.Get(ctx, key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var scores []float32
|
||||
for _, val := range vals {
|
||||
score, err := val.(*redis.StringCmd).Float32()
|
||||
|
||||
if errors.Is(err, redis.Nil) { // cache miss
|
||||
score = -1
|
||||
err = nil
|
||||
} else if err != nil { // cache error
|
||||
score = -1
|
||||
}
|
||||
|
||||
scores = append(scores, score)
|
||||
}
|
||||
return scores, nil
|
||||
}
|
||||
|
||||
func PurgeCache() error {
|
||||
return models.Cache.FlushAll(ctx).Err()
|
||||
func (pc *PlayerCache) DeleteScore(playerId uint) error {
|
||||
key := getPlayerCacheKey(playerId)
|
||||
return pc.cache.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
func GetPlayerCacheKey(playerId uint, gameTag string) string {
|
||||
return fmt.Sprintf("player:%d:game:%s", playerId, gameTag)
|
||||
func (pc *PlayerCache) PurgeCache() error {
|
||||
return pc.cache.FlushAll(ctx).Err()
|
||||
}
|
||||
|
||||
func getPlayerCacheKey(playerId uint) string {
|
||||
return fmt.Sprintf("player:%d", playerId)
|
||||
}
|
||||
|
||||
func (pc *PlayerCache) GetPlayerIdFromCacheKey(key string) (uint, error) {
|
||||
var playerId uint
|
||||
_, err := fmt.Sscanf(key, "player:%d", &playerId)
|
||||
return playerId, err
|
||||
}
|
||||
|
||||
9
internal/cache/go.mod
vendored
9
internal/cache/go.mod
vendored
@@ -1,3 +1,10 @@
|
||||
module cache
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
require github.com/redis/go-redis/v9 v9.4.0
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
)
|
||||
|
||||
10
internal/cache/go.sum
vendored
Normal file
10
internal/cache/go.sum
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
@@ -1,3 +1,3 @@
|
||||
module session
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
160
main.go
160
main.go
@@ -6,19 +6,35 @@ import (
|
||||
"InfantrySkillCalculator/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/gorilla/sessions"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"session"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var mainPageTemplates *template.Template
|
||||
var loginPageTemplates *template.Template
|
||||
var registerPageTemplates *template.Template
|
||||
|
||||
func init() {
|
||||
fileLogger := &lumberjack.Logger{
|
||||
Filename: "isc_rest.log",
|
||||
MaxSize: 50, // megabytes
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28, //days
|
||||
Compress: true,
|
||||
}
|
||||
ginWriter := io.MultiWriter(fileLogger, os.Stdout)
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(ginWriter)
|
||||
logger.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
utils.Logger = logger
|
||||
|
||||
var err error
|
||||
mainPageTemplates, err = template.ParseFiles(
|
||||
utils.MainPageTemplates, err = template.ParseFiles(
|
||||
"./templates/index.html",
|
||||
"./templates/components/home_clan_bar.html",
|
||||
"./templates/components/opp_clan_bar.html",
|
||||
@@ -31,32 +47,71 @@ func init() {
|
||||
"./templates/modals/add_player.html",
|
||||
"./templates/modals/delete_player.html",
|
||||
"./templates/modals/edit_player.html",
|
||||
"./templates/modals/settings.html",
|
||||
"./templates/modals/single_calc.html",
|
||||
//"./templates/modals/settings.html",
|
||||
"./templates/modals/full_calc.html",
|
||||
"./templates/components/header.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatalf("[MAIN] Error parsing main page templates: %v", err)
|
||||
}
|
||||
|
||||
loginPageTemplates, err = template.ParseFiles(
|
||||
utils.LoginPageTemplates, err = template.ParseFiles(
|
||||
"./templates/login.html",
|
||||
"./templates/components/header.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatalf("[MAIN] Error parsing login page templates: %v", err)
|
||||
}
|
||||
|
||||
registerPageTemplates, err = template.ParseFiles(
|
||||
utils.RegisterPageTemplates, err = template.ParseFiles(
|
||||
"./templates/register.html",
|
||||
"./templates/components/header.html",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
logger.Fatalf("[MAIN] Error parsing register page templates: %v", err)
|
||||
}
|
||||
|
||||
utils.PlayerItemTemplate, err = template.ParseFiles(
|
||||
"./templates/shards/player_list_item.html",
|
||||
)
|
||||
if err != nil {
|
||||
logger.Fatalf("[MAIN] Error parsing player list item template: %v", err)
|
||||
}
|
||||
|
||||
controllers.LoadMetrics()
|
||||
}
|
||||
|
||||
func customLogrusLogger(logger *logrus.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
c.Next()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if c.Request.URL.Path == "/static" || c.Request.URL.Path == "/favicon.ico" ||
|
||||
strings.Contains(c.Request.URL.Path, "_html") {
|
||||
return
|
||||
}
|
||||
|
||||
username, exists := session.GetUsername(c)
|
||||
if !exists {
|
||||
username = "Anonymous"
|
||||
}
|
||||
|
||||
logger.Infof("[GIN] %3d | %10.3fms | %15s | %15s | %15s | %-7s | %s",
|
||||
c.Writer.Status(),
|
||||
float64(elapsed.Microseconds())/1000.0,
|
||||
c.ClientIP(),
|
||||
c.Request.Header.Get("X-Forwarded-For"),
|
||||
username,
|
||||
c.Request.Method,
|
||||
c.Request.RequestURI,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Getenv("GO_ENV") == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -65,79 +120,76 @@ func main() {
|
||||
router := gin.New()
|
||||
err := router.SetTrustedProxies([]string{"127.0.0.1"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatalf("[MAIN] Error setting trusted proxies: %v", err)
|
||||
}
|
||||
router.LoadHTMLGlob("templates/**/*")
|
||||
protected := router.Group("/")
|
||||
protected.Use(AuthRequired())
|
||||
reader := router.Group("/")
|
||||
reader.Use(ReaderAuthRequired())
|
||||
author := router.Group("/")
|
||||
author.Use(AuthorAuthRequired())
|
||||
admin := router.Group("/admin")
|
||||
admin.Use(AdminAuthRequired())
|
||||
|
||||
models.ConnectDatabase()
|
||||
models.ConnectCache()
|
||||
models.ConnectCache(utils.PlayerCacheLifetime, controllers.UpdateCacheAfterExpiry)
|
||||
|
||||
var code models.ActivationCode
|
||||
if err := models.DB.First(&code).Error; err != nil {
|
||||
firstCode := utils.GenerateActivationCode()
|
||||
models.DB.Create(&models.ActivationCode{Code: firstCode, UserRole: models.AdminRole})
|
||||
log.Println("Created first activation code with ADMIN role:\n" + firstCode)
|
||||
utils.Logger.Println("[MAIN] Created first activation code with ADMIN role:\n" + firstCode)
|
||||
}
|
||||
|
||||
f, _ := os.OpenFile("isc_rest.log", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660)
|
||||
utils.GinWriter = io.MultiWriter(f, os.Stdout)
|
||||
router.Use(
|
||||
gin.LoggerWithWriter(utils.GinWriter, "/static"),
|
||||
gin.Recovery(),
|
||||
)
|
||||
protected.Use(
|
||||
gin.LoggerWithWriter(utils.GinWriter),
|
||||
gin.Recovery(),
|
||||
)
|
||||
admin.Use(
|
||||
gin.LoggerWithWriter(utils.GinWriter),
|
||||
gin.Recovery(),
|
||||
)
|
||||
|
||||
router.Static("/static", "./static")
|
||||
|
||||
middlewares := []gin.HandlerFunc{
|
||||
customLogrusLogger(utils.Logger),
|
||||
gin.Recovery(),
|
||||
}
|
||||
|
||||
router.Use(middlewares...)
|
||||
reader.Use(middlewares...)
|
||||
author.Use(middlewares...)
|
||||
admin.Use(middlewares...)
|
||||
|
||||
router.GET("/login", loginPage)
|
||||
router.POST("/login", loginPost)
|
||||
router.GET("/logout", logout)
|
||||
router.GET("/register", registerPage)
|
||||
router.POST("/register", registerPost)
|
||||
|
||||
protected.GET("/", mainPage)
|
||||
reader.GET("/", mainPage)
|
||||
|
||||
protected.GET("/clans", controllers.GetAllClans)
|
||||
protected.GET("/clans_html", controllers.GetAllClansHTML)
|
||||
protected.GET("/clan/:id", controllers.GetClanByID)
|
||||
protected.POST("/clan", controllers.AddClan)
|
||||
protected.PATCH("/clan/:id", controllers.UpdateClanByID)
|
||||
protected.DELETE("/clan/:id", controllers.DeleteClanByID)
|
||||
reader.GET("/clans", controllers.GetAllClans)
|
||||
reader.GET("/clans_html", controllers.GetAllClansHTML)
|
||||
reader.GET("/clan/:id", controllers.GetClanByID)
|
||||
author.POST("/clan", controllers.AddClan)
|
||||
author.PATCH("/clan/:id", controllers.UpdateClanByID)
|
||||
author.DELETE("/clan/:id", controllers.DeleteClanByID)
|
||||
|
||||
protected.GET("/players", controllers.GetAllPlayers)
|
||||
protected.GET("/players_html", controllers.GetPlayersByClanHTML)
|
||||
protected.GET("/player/:id", controllers.GetPlayerByID)
|
||||
protected.GET("/playerid/:name", controllers.GetPlayerIDByName)
|
||||
protected.POST("/player", controllers.AddPlayer)
|
||||
protected.PATCH("/player/:id", controllers.UpdatePlayerByID)
|
||||
protected.DELETE("/player/:id", controllers.DeletePlayerByID)
|
||||
reader.GET("/players", controllers.GetAllPlayers)
|
||||
reader.GET("/players_html", controllers.GetPlayersByClanHTML)
|
||||
reader.GET("/player/:id", controllers.GetPlayerByID)
|
||||
author.POST("/player", controllers.AddPlayer)
|
||||
author.PATCH("/player/:id", controllers.UpdatePlayerByID)
|
||||
author.DELETE("/player/:id", controllers.DeletePlayerByID)
|
||||
|
||||
protected.GET("/cache/:player_id", controllers.GetCacheByPlayerID)
|
||||
reader.GET("/cache/:player_id", controllers.GetCacheByPlayerID)
|
||||
|
||||
protected.GET("/score/:player_id", controllers.GetScoreByPlayerID)
|
||||
reader.GET("/score/:player_id", controllers.GetScoreByPlayerID)
|
||||
reader.POST("/score/:player_name", controllers.GetScoreByPlayerName)
|
||||
|
||||
protected.GET("/game", controllers.GetGames)
|
||||
protected.GET("/game_html", controllers.GetGamesHTML)
|
||||
reader.GET("/game", controllers.GetGames)
|
||||
reader.GET("/game_html", controllers.GetGamesHTML)
|
||||
|
||||
protected.GET("/settings", controllers.GetSettings)
|
||||
protected.PATCH("/settings", controllers.UpdateSettings)
|
||||
reader.GET("/settings", controllers.GetSettings)
|
||||
reader.PATCH("/settings", controllers.UpdateSettings)
|
||||
|
||||
admin.DELETE("/clear_cache", controllers.DeleteAllCaches)
|
||||
admin.DELETE("/purge_players", controllers.DeleteAllPlayers)
|
||||
admin.DELETE("/purge_clans", controllers.DeleteAllClans)
|
||||
admin.POST("/create_code", controllers.CreateCode)
|
||||
|
||||
log.Println("Running on 8000...")
|
||||
log.Fatal(router.Run(":8000"))
|
||||
utils.Logger.Println("[MAIN] Running on 8000...")
|
||||
utils.Logger.Fatalf("[MAIN] %v", router.Run(":8000"))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
type Clan struct {
|
||||
ID uint `json:"id" gorm:"primary_key"`
|
||||
Name string `json:"name"`
|
||||
Tag string `json:"tag"`
|
||||
Name string `json:"name" gorm:"unique"`
|
||||
Tag string `json:"tag" gorm:"unique"`
|
||||
KeepUpdated bool `json:"keep_updated"`
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
type Player struct {
|
||||
ID uint `json:"id" gorm:"primary_key"`
|
||||
Name string `json:"name"`
|
||||
ClanID uint `json:"clan_id"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
ClanID uint `json:"clan_id" gorm:"not null"`
|
||||
Clan Clan `gorm:"references:ID"`
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"cache"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
var ctx = context.Background()
|
||||
var Cache *redis.Client
|
||||
var PlayerCache *cache.PlayerCache
|
||||
|
||||
func ConnectDatabase() {
|
||||
database, err := gorm.Open(sqlite.Open("isc_data.db"), &gorm.Config{
|
||||
@@ -22,64 +21,54 @@ func ConnectDatabase() {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic("Failed to connect to database! " + err.Error())
|
||||
utils.Logger.Fatal("[SETUP] Failed to connect to database! " + err.Error())
|
||||
}
|
||||
|
||||
err = database.AutoMigrate(&Clan{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
|
||||
}
|
||||
err = database.AutoMigrate(&Player{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
|
||||
}
|
||||
err = database.AutoMigrate(&User{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
|
||||
}
|
||||
err = database.AutoMigrate(&ActivationCode{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
|
||||
}
|
||||
err = database.AutoMigrate(&Game{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
|
||||
} else {
|
||||
var game Game
|
||||
if err := database.First(&game).Error; err != nil {
|
||||
database.Create(&Game{Name: "Battlefield V", Tag: "BFV"})
|
||||
database.Create(&Game{Name: "Battlefield 2042", Tag: "BF2042"})
|
||||
log.Println("Created first games")
|
||||
utils.Logger.Println("[SETUP] Created first games")
|
||||
}
|
||||
}
|
||||
|
||||
err = database.AutoMigrate(&UserSettings{})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatal("[SETUP] Failed to migrate database! " + err.Error())
|
||||
}
|
||||
|
||||
DB = database
|
||||
}
|
||||
|
||||
func ConnectCache() {
|
||||
func ConnectCache(playerCacheLifetime time.Duration, expireCallback func(key string)) {
|
||||
address := os.Getenv("REDIS_ADDRESS")
|
||||
if address == "" {
|
||||
address = "127.0.0.1"
|
||||
}
|
||||
port := os.Getenv("REDIS_PORT")
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
address = "127.0.0.1:6379"
|
||||
}
|
||||
|
||||
Cache = redis.NewClient(&redis.Options{
|
||||
Addr: address + ":" + port,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
|
||||
_, err := Cache.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
Cache = nil
|
||||
log.Fatal("Failed to connect to Redis! " + err.Error())
|
||||
PlayerCache = cache.NewPlayerCache(address, playerCacheLifetime, expireCallback)
|
||||
if err := PlayerCache.Connect(); err != nil {
|
||||
PlayerCache = nil
|
||||
utils.Logger.Fatal("[SETUP] Failed to connect to Redis! " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
package models
|
||||
|
||||
type TrackerWeaponJSON struct {
|
||||
Weapons []Weapon `json:"weapons"`
|
||||
type TrackerDataJSON struct {
|
||||
Weapons []Weapon `json:"weapons"`
|
||||
DivKills DividedKills `json:"dividedKills"`
|
||||
Deaths int `json:"deaths"`
|
||||
SecondsPlayed int `json:"secondsPlayed"`
|
||||
XP []XP `json:"XP"`
|
||||
}
|
||||
|
||||
type DividedKills struct {
|
||||
ADS int `json:"ads"`
|
||||
Hip int `json:"hip"`
|
||||
AI int `json:"ai"`
|
||||
}
|
||||
|
||||
type XP struct {
|
||||
Ribbons Ribbons `json:"ribbons"`
|
||||
}
|
||||
|
||||
type Ribbons struct {
|
||||
Objective int `json:"objective"`
|
||||
Squad int `json:"squad"`
|
||||
Support int `json:"support"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type Weapon struct {
|
||||
|
||||
30
pages.go
30
pages.go
@@ -2,20 +2,26 @@ package main
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/controllers"
|
||||
"InfantrySkillCalculator/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"net/http"
|
||||
"session"
|
||||
)
|
||||
|
||||
func mainPage(c *gin.Context) {
|
||||
data := map[string]interface{}{
|
||||
"isAdmin": isUserAdmin(c),
|
||||
username, ok := session.GetUsername(c)
|
||||
if !ok {
|
||||
username = ""
|
||||
}
|
||||
|
||||
err := mainPageTemplates.Execute(c.Writer, data)
|
||||
data := map[string]interface{}{
|
||||
"UserRole": controllers.GetUserRoleByCtx(c),
|
||||
"Username": username,
|
||||
}
|
||||
|
||||
err := utils.MainPageTemplates.Execute(c.Writer, data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatalf("[MAIN] Error while executing template: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +31,9 @@ func loginPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := loginPageTemplates.Execute(c.Writer, nil)
|
||||
err := utils.LoginPageTemplates.Execute(c.Writer, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatalf("[LOGIN] Error while executing template: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +43,13 @@ func loginPost(c *gin.Context) {
|
||||
|
||||
if !checkUserCredentials(username, password) {
|
||||
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültige Logindaten!"})
|
||||
utils.Logger.Warnf("[LOGIN] User %s tried to login with invalid credentials", username)
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.SetLoginSession(username, c); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, nil)
|
||||
utils.Logger.Errorf("[LOGIN] Error while setting login session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,6 +60,7 @@ func loginPost(c *gin.Context) {
|
||||
func logout(c *gin.Context) {
|
||||
if err := session.InvalidateSession(c); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, nil)
|
||||
utils.Logger.Errorf("[LOGOUT] Error while invalidating session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -64,9 +73,9 @@ func registerPage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := registerPageTemplates.Execute(c.Writer, nil)
|
||||
err := utils.RegisterPageTemplates.Execute(c.Writer, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
utils.Logger.Fatalf("[REGISTER] Error while executing template: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,12 +86,14 @@ func registerPost(c *gin.Context) {
|
||||
|
||||
if !isValidCode(code) {
|
||||
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Ungültiger Aktivierungscode!"})
|
||||
utils.Logger.Warnf("[REGISTER] User %s tried to register with invalid code %s", username, code)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := hashPassword(password)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusOK, "login_error.html", gin.H{"message": "Fehler beim Registrieren!"})
|
||||
utils.Logger.Errorf("[REGISTER] Error while hashing password: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,6 +101,7 @@ func registerPost(c *gin.Context) {
|
||||
|
||||
if err := session.SetLoginSession(username, c); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, nil)
|
||||
utils.Logger.Errorf("[REGISTER] Error while setting login session: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
62
static/dialogs.js
Normal file
62
static/dialogs.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const swalClasses = {
|
||||
container: 'text-center',
|
||||
confirmButton: 'btn btn-lg btn-primary',
|
||||
cancelButton: 'btn btn-lg btn-secondary ms-3',
|
||||
denyButton: 'btn btn-lg btn-secondary ms-3',
|
||||
popup: 'border p-5',
|
||||
title: 'fs-2',
|
||||
inputLabel: 'fs-5 text-secondary-emphasis',
|
||||
htmlContainer: 'fs-5 text-center text-secondary-emphasis',
|
||||
};
|
||||
|
||||
function showAdminActionExecutedDialog(xhr, method) {
|
||||
Swal.fire({
|
||||
title: 'Action executed',
|
||||
text: xhr.response,
|
||||
icon: xhr.status === 200 ? 'success' : 'error'
|
||||
}).then(() => {
|
||||
if (method === "delete") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function confirmAndTrigger(btn) {
|
||||
Swal.fire({
|
||||
title: btn.innerText,
|
||||
text: 'Do you want to continue?',
|
||||
confirmButtonText: 'Yes',
|
||||
confirmButtonColor: '#dd6b55',
|
||||
denyButtonColor: '#3085d6',
|
||||
icon: 'warning',
|
||||
showDenyButton: true,
|
||||
customClass: swalClasses,
|
||||
buttonsStyling: false
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
htmx.trigger(btn, 'confirmed', null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createCodeDialog(btn) {
|
||||
Swal.fire({
|
||||
title: btn.innerText,
|
||||
input: 'select',
|
||||
inputOptions: {
|
||||
'READER': 'Reader',
|
||||
'AUTHOR': 'Author',
|
||||
'ADMIN': 'Admin'
|
||||
},
|
||||
inputPlaceholder: 'Select a role',
|
||||
confirmButtonText: 'Generate',
|
||||
showCancelButton: true,
|
||||
customClass: swalClasses,
|
||||
buttonsStyling: false
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
btn.setAttribute('hx-vals', '{"user_role": "' + result.value + '"}');
|
||||
htmx.trigger(btn, 'confirmed', null);
|
||||
}
|
||||
});
|
||||
}
|
||||
12
static/icons/average.svg
Normal file
12
static/icons/average.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>average</title>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="drop" fill="#000000" transform="translate(60.496777, 60.491376)">
|
||||
<path d="M362.038672,4.40536496e-13 L392.208561,30.1698893 L330.849151,91.530257 C352.999344,120.319635 366.169889,156.376056 366.169889,195.508624 C366.169889,289.765221 289.75982,366.17529 195.503223,366.17529 C156.370421,366.17529 120.313802,353.004587 91.5243389,330.854154 L30.1698893,392.208561 L-1.42108547e-14,362.038672 L61.2047929,300.834525 C38.4229489,271.826458 24.836556,235.254177 24.836556,195.508624 C24.836556,101.252026 101.246625,24.841957 195.503223,24.841957 C235.248317,24.841957 271.820213,38.4280364 300.82812,61.2094052 L362.038672,4.40536496e-13 Z M300.334816,122.043508 L122.038107,300.340217 C142.831803,314.939228 168.166768,323.508624 195.503223,323.508624 C266.195671,323.508624 323.503223,266.201072 323.503223,195.508624 C323.503223,168.172169 314.933827,142.837204 300.334816,122.043508 Z M195.503223,67.5086237 C124.810775,67.5086237 67.5032227,124.816176 67.5032227,195.508624 C67.5032227,223.460895 76.4630581,249.320485 91.66741,270.372075 L270.365674,91.6720892 C249.314266,76.4681756 223.455052,67.5086237 195.503223,67.5086237 Z" id="Combined-Shape">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,10 +1,18 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tooltipTriggerList = document.querySelectorAll('[config-bs-toggle="tooltip"]');
|
||||
tooltipTriggerList.forEach((elem) => {
|
||||
new bootstrap.Tooltip(elem);
|
||||
});
|
||||
initTooltips(document);
|
||||
});
|
||||
|
||||
function initTooltips(elementRoot) {
|
||||
const tooltipTriggerList = elementRoot.querySelectorAll('[data-bs-action="tooltip"]');
|
||||
tooltipTriggerList.forEach((elem) => {
|
||||
new bootstrap.Tooltip(elem, {
|
||||
trigger: 'hover',
|
||||
delay: {show: 500, hide: 0},
|
||||
container: 'body'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupClanButtons(dropdownId, delBtnId, editBtnId) {
|
||||
const dropdown = document.getElementById(dropdownId);
|
||||
const deleteButton = document.getElementById(delBtnId);
|
||||
@@ -111,7 +119,7 @@ function loadClans() {
|
||||
}
|
||||
|
||||
function updateSelectedPlayers(sender) {
|
||||
const playerList = sender.parentElement.parentElement.parentElement;
|
||||
const playerList = sender.parentElement.parentElement.parentElement.parentElement;
|
||||
const checkCounter = playerList.parentElement.parentElement.querySelector('span.badge');
|
||||
let counter = 0;
|
||||
|
||||
@@ -145,42 +153,6 @@ function deselectAllPlayers(playerListId) {
|
||||
checkCounter.innerText = 0;
|
||||
}
|
||||
|
||||
function confirmAndTrigger(btn) {
|
||||
Swal.fire({
|
||||
title: btn.innerText,
|
||||
text: 'Do you want to continue?',
|
||||
confirmButtonText: 'Yes',
|
||||
confirmButtonColor: '#dd6b55',
|
||||
denyButtonColor: '#3085d6',
|
||||
icon: 'warning',
|
||||
showDenyButton: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
htmx.trigger(btn, 'confirmed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createCodeDialog(btn) {
|
||||
Swal.fire({
|
||||
title: btn.innerText,
|
||||
input: 'select',
|
||||
inputOptions: {
|
||||
'READER': 'Reader',
|
||||
'AUTHOR': 'Author',
|
||||
'ADMIN': 'Admin'
|
||||
},
|
||||
inputPlaceholder: 'Select a role',
|
||||
confirmButtonText: 'Generate',
|
||||
showCancelButton: true
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
btn.setAttribute('hx-vals', '{"user_role": "' + result.value + '"}');
|
||||
htmx.trigger(btn, 'confirmed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function singleCalcSpinner(sender) {
|
||||
const spinner = '<i class="spinner-grow spinner-grow-sm text-info align-baseline me-2" style="margin-left: 0.91rem;" role="status"></i>';
|
||||
const score = sender.previousElementSibling.children[1];
|
||||
@@ -188,5 +160,13 @@ function singleCalcSpinner(sender) {
|
||||
sender.disabled = true;
|
||||
sender.addEventListener('htmx:afterRequest', function () {
|
||||
sender.disabled = false;
|
||||
const homeClan = document.getElementById('home-clan');
|
||||
const oppClan = document.getElementById('opponent-clan');
|
||||
if (homeClan.selectedIndex === oppClan.selectedIndex) {
|
||||
if (sender.parentElement.parentElement.id === 'home-player-list')
|
||||
oppClan.dispatchEvent(new Event('change'));
|
||||
else
|
||||
homeClan.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}, {once: true});
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
{{ define "bottom_controls" }}
|
||||
|
||||
<div class="row justify-content-between border-top pt-4 position-relative mb-5">
|
||||
<div class="row justify-content-between border-top border-secondary-subtle pt-4 mx-0 position-relative mb-5">
|
||||
<div class="col-auto position-absolute start-0">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<a class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" href="/logout" data-bs-toggle="tooltip" data-bs-title="Abmelden">
|
||||
<i class="bi bi-door-closed"></i>
|
||||
<a class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" href="/logout" data-bs-action="tooltip" data-bs-title="Abmelden">
|
||||
<i class="bi bi-door-closed-fill"></i>
|
||||
</a>
|
||||
<!--
|
||||
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis me-2" data-bs-toggle="modal" data-bs-target="#settingsModal">
|
||||
<i class="bi bi-gear-fill"></i>
|
||||
<i class="bi bi-gear-fill" data-bs-action="tooltip" data-bs-title="Einstellungen"></i>
|
||||
</button>
|
||||
{{ if .isAdmin }}
|
||||
-->
|
||||
{{ if (eq .UserRole "ADMIN") }}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-tools"></i>
|
||||
@@ -61,14 +63,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto position-absolute start-50 translate-middle-x">
|
||||
<button class="btn btn-lg btn-outline-primary" type="button" disabled>
|
||||
<button class="btn btn-lg btn-primary" id="fullCalcBtn" data-bs-toggle="modal" data-bs-target="#fullCalcModal" data-bs-action="tooltip" data-bs-title="Ausgewählte Spieler berechnen">
|
||||
<i class="bi bi-calculator-fill me-2"></i>
|
||||
Berechnen
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto position-absolute end-0">
|
||||
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis" type="button" disabled>
|
||||
<i class="bi bi-person me-2"></i>
|
||||
<button class="btn btn-lg btn-outline-secondary text-secondary-emphasis" id="singleCalcBtn" data-bs-toggle="modal" data-bs-target="#singleCalcModal" data-bs-action="tooltip" data-bs-title="Einzelnen Spieler berechnen">
|
||||
<i class="bi bi-person-fill me-2"></i>
|
||||
Einzel-Abfrage
|
||||
</button>
|
||||
</div>
|
||||
@@ -76,16 +78,13 @@
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('htmx:afterRequest', function (event) {
|
||||
if (event.detail.pathInfo.requestPath.startsWith("/admin/")) {
|
||||
Swal.fire({
|
||||
title: 'Action executed',
|
||||
text: event.detail.xhr.response,
|
||||
icon: event.detail.xhr.status === 200 ? 'success' : 'error'
|
||||
}).then(() => {
|
||||
if (event.detail.requestConfig.verb === "delete") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
let detail = event.detail;
|
||||
let reqPath = detail.pathInfo.requestPath;
|
||||
let method = detail.requestConfig.verb;
|
||||
let xhr = detail.xhr;
|
||||
|
||||
if (reqPath.startsWith("/admin/")) {
|
||||
showAdminActionExecutedDialog(xhr, method);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
{{ define "header" }}
|
||||
|
||||
<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">
|
||||
<title>Infantry Skill Calculator</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="../static/index.js"></script>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="../static/dialogs.js"></script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@4/dark.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
<link rel="stylesheet" href="../static/index.css">
|
||||
|
||||
{{ end }}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{ define "home_clan_bar" }}
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<div class="col-auto pe-2">
|
||||
<label for="home-clan" class="col-form-label col-form-label-lg">Clans:</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -10,15 +10,20 @@
|
||||
<option disabled selected value>Auswählen...</option>
|
||||
<!-- Options will be loaded dynamically -->
|
||||
</select>
|
||||
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="home-delete" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#deleteClanModal" disabled>
|
||||
<i class="bi bi-trash3" data-bs-toggle="tooltip" data-bs-title="Clan löschen"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#editClanModal" id="home-edit" disabled>
|
||||
<i class="bi bi-pencil-fill" data-bs-toggle="tooltip" data-bs-title="Clan bearbeiten"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#addClanModal" id="home-add">
|
||||
<i class="bi bi-plus-lg" data-bs-toggle="tooltip" data-bs-title="Clan hinzufügen"></i>
|
||||
</button>
|
||||
{{ if not (eq .UserRole "READER") }}
|
||||
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="home-delete"
|
||||
data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#deleteClanModal" data-bs-action="tooltip" data-bs-title="Clan löschen" disabled>
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" id="home-edit"
|
||||
data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#editClanModal" data-bs-action="tooltip" data-bs-title="Clan bearbeiten" disabled>
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-success" type="button" id="home-add"
|
||||
data-bs-toggle="modal" data-bs-list="#home-clan" data-bs-target="#addClanModal" data-bs-action="tooltip" data-bs-title="Clan hinzufügen">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,6 +31,11 @@
|
||||
<script lang="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupClanButtons('home-clan', 'home-delete', 'home-edit');
|
||||
|
||||
const homePlayerList = document.getElementById('home-player-list');
|
||||
homePlayerList.addEventListener('htmx:afterSwap', function() {
|
||||
initTooltips(homePlayerList);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,25 +2,28 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div id="home-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;" oninput="updateTooltips(this)">
|
||||
<div id="home-player-list" class="border border-secondary rounded p-1 overflow-auto" style="height: 47vh;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ps-0">
|
||||
<div class="btn-group-vertical btn-group-lg" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('home-player-list')">
|
||||
<i class="bi bi-check-square fs-4" data-bs-toggle="tooltip" data-bs-title="Alle auswählen"></i>
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('home-player-list')" data-bs-action="tooltip" data-bs-title="Alle auswählen">
|
||||
<i class="bi bi-check-square fs-4"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('home-player-list')">
|
||||
<i class="bi bi-square fs-4" data-bs-toggle="tooltip" data-bs-title="Nichts auswählen"></i>
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('home-player-list')" data-bs-action="tooltip" data-bs-title="Nichts auswählen">
|
||||
<i class="bi bi-square fs-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
{{ if not (eq .UserRole "READER") }}
|
||||
<br>
|
||||
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add"
|
||||
data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" data-bs-action="tooltip" data-bs-title="Spieler hinzufügen" disabled>
|
||||
<i class="bi bi-person-fill-add fs-4"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
<br>
|
||||
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="home-player-add" data-bs-toggle="modal" data-bs-list="#home-player-list" data-bs-target="#addPlayerModal" disabled>
|
||||
<i class="bi bi-person-add fs-4" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen"></i>
|
||||
</button>
|
||||
<br>
|
||||
<div class="vstack text-center border border-secondary rounded mt-2" data-bs-toggle="tooltip" data-bs-title="Ausgwählte Spieler">
|
||||
<div class="vstack text-center border border-secondary rounded mt-2" data-bs-action="tooltip" data-bs-title="Ausgewählte Spieler">
|
||||
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
|
||||
<span class="badge fs-5 mb-2" id="home-player-selected">0</span>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
{{ define "opp_clan_bar" }}
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<div class="col-auto pe-2">
|
||||
<label for="opponent-clan" class="col-form-label col-form-label-lg">Clans:</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="input-group input-group-lg mb-3">
|
||||
<select class="form-select form-control border-secondary" id="opponent-clan" hx-get="/players_html" hx-target="#opponent-player-list" hx-vals='js:{"clan_id": getSelectedClanId("opponent-clan")}'>
|
||||
<option disabled selected value>Auswählen...</option>
|
||||
<!-- Options will be loaded dynamically -->
|
||||
</select>
|
||||
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="opponent-delete" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" disabled>
|
||||
<i class="bi bi-trash3" data-bs-toggle="tooltip" data-bs-title="Clan löschen"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#editClanModal" id="opponent-edit" disabled>
|
||||
<i class="bi bi-pencil-fill" data-bs-toggle="tooltip" data-bs-title="Clan bearbeiten"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-success" type="button" data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#addClanModal" id="opponent-add">
|
||||
<i class="bi bi-plus-lg" data-bs-toggle="tooltip" data-bs-title="Clan hinzufügen"></i>
|
||||
</button>
|
||||
{{ if not (eq .UserRole "READER") }}
|
||||
<button class="btn btn-lg btn-outline-secondary text-danger bg-secondary-subtle" type="button" id="opponent-delete"
|
||||
data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#deleteClanModal" data-bs-action="tooltip" data-bs-title="Clan löschen" disabled>
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-primary bg-secondary-subtle" type="button" id="opponent-edit"
|
||||
data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#editClanModal" data-bs-action="tooltip" data-bs-title="Clan bearbeiten" disabled>
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-secondary text-success" type="button" id="opponent-add"
|
||||
data-bs-toggle="modal" data-bs-list="#opponent-clan" data-bs-target="#addClanModal" data-bs-action="tooltip" data-bs-title="Clan hinzufügen">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,6 +31,11 @@
|
||||
<script lang="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupClanButtons('opponent-clan', 'opponent-delete', 'opponent-edit');
|
||||
|
||||
const oppPlayerList = document.getElementById('opponent-player-list');
|
||||
oppPlayerList.addEventListener('htmx:afterSwap', function() {
|
||||
initTooltips(oppPlayerList);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,25 +2,28 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div id="opponent-player-list" class="border rounded p-1 overflow-auto" style="height: 47vh;">
|
||||
<div id="opponent-player-list" class="border border-secondary rounded p-1 overflow-auto" style="height: 47vh;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ps-0">
|
||||
<div class="btn-group-vertical btn-group-lg" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('opponent-player-list')">
|
||||
<i class="bi bi-check-square fs-4" data-bs-toggle="tooltip" data-bs-title="Alle auswählen"></i>
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="selectAllPlayers('opponent-player-list')" data-bs-action="tooltip" data-bs-title="Alle auswählen">
|
||||
<i class="bi bi-check-square fs-4"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('opponent-player-list')">
|
||||
<i class="bi bi-square fs-4" data-bs-toggle="tooltip" data-bs-title="Nichts auswählen"></i>
|
||||
<button type="button" class="btn btn-outline-secondary text-primary-emphasis" onclick="deselectAllPlayers('opponent-player-list')" data-bs-action="tooltip" data-bs-title="Nichts auswählen">
|
||||
<i class="bi bi-square fs-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
{{ if not (eq .UserRole "READER") }}
|
||||
<br>
|
||||
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add"
|
||||
data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" data-bs-action="tooltip" data-bs-title="Spieler hinzufügen" disabled>
|
||||
<i class="bi bi-person-fill-add fs-4"></i>
|
||||
</button>
|
||||
{{ end }}
|
||||
<br>
|
||||
<button type="button" class="btn btn-outline-secondary text-success px-3 mt-2 bg-secondary-subtle" id="opponent-player-add" data-bs-toggle="modal" data-bs-list="#opponent-player-list" data-bs-target="#addPlayerModal" disabled>
|
||||
<i class="bi bi-person-add fs-4" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen"></i>
|
||||
</button>
|
||||
<br>
|
||||
<div class="vstack text-center border border-secondary rounded mt-2" data-bs-toggle="tooltip" data-bs-title="Spieler hinzufügen">
|
||||
<div class="vstack text-center border border-secondary rounded mt-2" data-bs-action="tooltip" data-bs-title="Ausgewählte Spieler">
|
||||
<i class="bi bi-ui-checks fs-4 mt-2 mb-1"></i>
|
||||
<span class="badge fs-5 mb-2" id="opponent-player-selected">0</span>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
{{ template "header" . }}
|
||||
</head>
|
||||
<body data-bs-theme="dark" class="h-auto">
|
||||
<div class="container-xxl bg-dark mt-5 p-4 rounded-3 text-light">
|
||||
<div class="container-xxl bg-dark mt-4 py-4 px-3 rounded-3 text-light">
|
||||
<div class="text-secondary-emphasis border-bottom border-secondary-subtle">
|
||||
<h1 class="text-center fw-bold">Infantry Skill Calculator</h1>
|
||||
<p class="lead text-center fs-6 text-secondary fst-italic">By [GCG]FinalEnd4</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Home-Clan Column -->
|
||||
<div class="col-md-6 d-flex flex-column border-end px-3 pb-4">
|
||||
<h4 class="text-center mt-2 pb-3 mb-3 border-bottom"><i class="bi bi-people me-3"></i>Heim-Team</h4>
|
||||
<div class="col-md-6 border-end border-secondary-subtle px-3 pb-4">
|
||||
<h4 class="text-center my-3 text-primary fs-3">
|
||||
<i class="bi bi-people-fill me-3"></i>Heim-Team<i class="bi bi-people-fill ms-3"></i>
|
||||
</h4>
|
||||
<!-- Clan Selection -->
|
||||
{{ template "home_clan_bar" . }}
|
||||
|
||||
@@ -17,8 +23,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Opponent-Clan Column -->
|
||||
<div class="col-md-6 d-flex flex-column px-3 pb-4">
|
||||
<h4 class="text-center mt-2 pb-3 mb-3 border-bottom"><i class="bi bi-people me-3"></i>Gegner-Team</h4>
|
||||
<div class="col-md-6 px-3 pb-4">
|
||||
<h4 class="text-center my-3 text-danger fs-3">
|
||||
<i class="bi bi-people-fill me-3"></i>Gegner-Team<i class="bi bi-people-fill ms-3"></i>
|
||||
</h4>
|
||||
<!-- Clan Selection -->
|
||||
{{ template "opp_clan_bar" . }}
|
||||
|
||||
@@ -31,22 +39,20 @@
|
||||
{{ template "bottom_controls" . }}
|
||||
</div>
|
||||
|
||||
<!-- Delete Clan Modal -->
|
||||
{{ template "delete_clan" . }}
|
||||
<!-- Add Clan Modal -->
|
||||
{{ template "add_clan" . }}
|
||||
<!-- Edit Clan Modal -->
|
||||
{{ template "edit_clan" . }}
|
||||
{{ if not (eq .UserRole "READER")}}
|
||||
{{ template "delete_clan" . }}
|
||||
{{ template "add_clan" . }}
|
||||
{{ template "edit_clan" . }}
|
||||
|
||||
<!-- Add Player Modal -->
|
||||
{{ template "add_player" . }}
|
||||
<!-- Delete Player Modal -->
|
||||
{{ template "delete_player" . }}
|
||||
<!-- Edit Player Modal -->
|
||||
{{ template "edit_player" . }}
|
||||
{{ template "add_player" . }}
|
||||
{{ template "delete_player" . }}
|
||||
{{ template "edit_player" . }}
|
||||
{{ end}}
|
||||
|
||||
<!-- Settings Modal -->
|
||||
{{ template "settings" . }}
|
||||
{{/* template "settings" . */}}
|
||||
|
||||
{{ template "full_calc" . }}
|
||||
{{ template "single_calc" . }}
|
||||
|
||||
<script lang="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<hr>
|
||||
<form hx-post="/login" hx-target="#login-result" class="position-relative">
|
||||
<div class="form-floating mb-3">
|
||||
<input class="form-control form-control-lg" type="text" id="username" name="username" placeholder="Username" required>
|
||||
<input class="form-control form-control-lg pb-1" type="text" id="username" name="username" placeholder="Username" required>
|
||||
<label for="username">Username</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input class="form-control form-control-lg" type="password" id="password" name="password" placeholder="Passwort" required>
|
||||
<input class="form-control form-control-lg pb-1" type="password" id="password" name="password" placeholder="Passwort" required>
|
||||
<label for="password">Passwort</label>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-door-open me-3"></i>Anmelden</button>
|
||||
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/register" data-bs-toggle="tooltip" data-bs-title="Zur Registrierung"><i class="bi bi-key"></i></a>
|
||||
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/register" data-bs-action="tooltip" data-bs-title="Zur Registrierung"><i class="bi bi-key"></i></a>
|
||||
</form>
|
||||
<div id="login-result" class="fs-4 mt-2"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{ define "add_clan" }}
|
||||
|
||||
|
||||
<div class="modal fade" id="addClanModal" tabindex="-1">
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="addClanModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -10,11 +10,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control form-control-lg" id="clanName" placeholder="Clan-Name" minlength="2" maxlength="30" required>
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="clanName" placeholder="Clan-Name" minlength="2" maxlength="30" required>
|
||||
<label for="clanName">Clan-Name</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-lg" id="clanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required>
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="clanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required>
|
||||
<label for="clanTag">Clan-Tag</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mt-3 fs-5">
|
||||
@@ -130,6 +130,12 @@
|
||||
submitButton.addEventListener('click', submitClanHandler);
|
||||
});
|
||||
|
||||
addClanModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
addClanModal.addEventListener('hidden.bs.modal', _ => {
|
||||
submitButton.removeEventListener('click', submitClanHandler);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "add_player" }}
|
||||
|
||||
<div class="modal fade" id="addPlayerModal" tabindex="-1">
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="addPlayerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -9,12 +9,12 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control form-control-lg" id="playerClanName" placeholder="Clan-Name" disabled>
|
||||
<label for="playerClanName">Clan-Name</label>
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="addPlayerClanName" placeholder="Clan-Name" disabled>
|
||||
<label for="addPlayerClanName">Clan-Name</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-lg" id="playerName" placeholder="Spieler-Name" required>
|
||||
<label for="playerName">Spieler-Name</label>
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="addPlayerName" placeholder="Spieler-Name" required>
|
||||
<label for="addPlayerName">Spieler-Name</label>
|
||||
</div>
|
||||
<div class="error-message text-danger fs-5 badge" style="display: none;"></div>
|
||||
</div>
|
||||
@@ -35,8 +35,8 @@
|
||||
let selectedClan = null;
|
||||
|
||||
const submitButton = addPlayerModal.querySelector('button[name="submit"]');
|
||||
const playerName = addPlayerModal.querySelector('#playerName');
|
||||
const clanName = addPlayerModal.querySelector('#playerClanName');
|
||||
const playerName = addPlayerModal.querySelector('#addPlayerName');
|
||||
const clanName = addPlayerModal.querySelector('#addPlayerClanName');
|
||||
const errorDiv = addPlayerModal.querySelector('.error-message');
|
||||
const homeClanList = document.getElementById('home-clan');
|
||||
const oppClanList = document.getElementById('opponent-clan');
|
||||
@@ -68,8 +68,12 @@
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Hinzufügen fehlgeschlagen!\nSpielername existiert möglichweise bereits.');
|
||||
if (response.status === 404) {
|
||||
throw new Error('Hinzufügen fehlgeschlagen!\nSpielername existiert nicht.');
|
||||
} else if (response.status === 503 || response.status === 504) {
|
||||
throw new Error('Hinzufügen fehlgeschlagen!\nTracker überlastet. Versuche es später erneut.');
|
||||
} else if (!response.ok) {
|
||||
throw new Error('Hinzufügen fehlgeschlagen!\nSpielername existiert möglicherweise bereits.');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
@@ -98,6 +102,12 @@
|
||||
clanName.value = selectedClan.innerText;
|
||||
});
|
||||
|
||||
addPlayerModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
addPlayerModal.addEventListener('hidden.bs.modal', _ => {
|
||||
submitButton.removeEventListener('click', submitPlayerHandler);
|
||||
|
||||
@@ -106,6 +116,7 @@
|
||||
errorDiv.innerText = "";
|
||||
errorDiv.style.display = 'none';
|
||||
playerName.classList.remove('is-invalid');
|
||||
initTooltips(playerList);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{{ define "delete_clan" }}
|
||||
|
||||
<div class="modal fade" id="deleteClanModal" tabindex="-1">
|
||||
<div class="modal modal-lg fade" data-bs-backdrop="static" data-bs-keyboard="false" id="deleteClanModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-3 text-danger fw-bold" id="deleteClanModalLabel">Clan löschen</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body fs-5">
|
||||
<div class="modal-body fs-5 text-center">
|
||||
Möchtest du den Clan <span class="text-warning fw-bold" id="clan"></span> wirklich löschen?
|
||||
<br><br>
|
||||
Die Aktion kann nicht rückgängig gemacht werden.
|
||||
@@ -24,6 +24,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const deleteClanModal = document.getElementById('deleteClanModal')
|
||||
const deleteClanModalBS = new bootstrap.Modal('#deleteClanModal');
|
||||
const submitButton = deleteClanModal.querySelector('button[name="submit"]');
|
||||
|
||||
if (deleteClanModal) {
|
||||
deleteClanModal.addEventListener('show.bs.modal', event => {
|
||||
@@ -33,7 +34,6 @@
|
||||
const modalBodyInput = deleteClanModal.querySelector('#clan');
|
||||
modalBodyInput.innerText = selectedClan;
|
||||
|
||||
const submitButton = deleteClanModal.querySelector('button[name="submit"]');
|
||||
submitButton.addEventListener('click', function () {
|
||||
const clanId = parseInt(clanList.value);
|
||||
|
||||
@@ -69,6 +69,12 @@
|
||||
});
|
||||
}, { once: true });
|
||||
});
|
||||
|
||||
deleteClanModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{{ define "delete_player" }}
|
||||
|
||||
<div class="modal fade" id="deletePlayerModal" tabindex="-1">
|
||||
<div class="modal modal-lg fade" data-bs-backdrop="static" data-bs-keyboard="false" id="deletePlayerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-3 text-danger fw-bold" id="deletePlayerModalLabel">Spieler löschen</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body fs-5">
|
||||
<div class="modal-body fs-5 text-center">
|
||||
Möchtest du den Spieler <span class="text-warning fw-bold" id="player"></span> wirklich löschen?
|
||||
<br><br>
|
||||
Die Aktion kann nicht rückgängig gemacht werden.
|
||||
@@ -24,6 +24,8 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const deletePlayerModal = document.getElementById('deletePlayerModal')
|
||||
const deletePlayerModalBS = new bootstrap.Modal('#deletePlayerModal');
|
||||
const submitButton = deletePlayerModal.querySelector('button[name="submit"]');
|
||||
|
||||
if (deletePlayerModal) {
|
||||
deletePlayerModal.addEventListener('show.bs.modal', event => {
|
||||
const button = event.relatedTarget;
|
||||
@@ -37,7 +39,6 @@
|
||||
const homeClanList = document.getElementById('home-clan');
|
||||
const oppClanList = document.getElementById('opponent-clan');
|
||||
|
||||
const submitButton = deletePlayerModal.querySelector('button[name="submit"]');
|
||||
submitButton.addEventListener('click', function () {
|
||||
fetch("/player/" + playerId, {
|
||||
method: "DELETE",
|
||||
@@ -59,6 +60,12 @@
|
||||
});
|
||||
}, { once: true });
|
||||
});
|
||||
|
||||
deletePlayerModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "edit_clan" }}
|
||||
|
||||
<div class="modal fade" id="editClanModal" tabindex="-1">
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="editClanModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -9,11 +9,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control form-control-lg" id="editClanName" placeholder="Clan-Name" minlength="2" maxlength="30" required>
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="editClanName" placeholder="Clan-Name" minlength="2" maxlength="30" required>
|
||||
<label for="editClanName">Clan-Name</label>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-lg" id="editClanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required>
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="editClanTag" placeholder="Clan-Tag" minlength="2" maxlength="30" required>
|
||||
<label for="editClanTag">Clan-Tag</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mt-3 fs-5">
|
||||
@@ -149,6 +149,12 @@
|
||||
});
|
||||
});
|
||||
|
||||
editClanModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
editClanModal.addEventListener('hidden.bs.modal', _ => {
|
||||
submitButton.removeEventListener('click', submitClanHandler);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "edit_player" }}
|
||||
|
||||
<div class="modal fade" id="editPlayerModal" tabindex="-1">
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="editPlayerModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control form-control-lg" id="editPlayerName" placeholder="Spieler-Name">
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="editPlayerName" placeholder="Spieler-Name">
|
||||
<label for="editPlayerName">Spieler-Name</label>
|
||||
</div>
|
||||
<div class="error-message text-danger fs-5 badge" style="display: none;"></div>
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
|
||||
function createSubmitPlayerHandler(modalEvent) {
|
||||
return function submitPlayerHandler(e) {
|
||||
return function submitPlayerHandler(_) {
|
||||
if (!validateInput())
|
||||
return;
|
||||
|
||||
@@ -95,6 +95,12 @@
|
||||
playerName.value = event.relatedTarget.closest('.input-group').querySelector('span').innerText;
|
||||
});
|
||||
|
||||
editPlayerModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
editPlayerModal.addEventListener('hide.bs.modal', _ => {
|
||||
submitButton.removeEventListener('click', submitPlayerHandler);
|
||||
|
||||
|
||||
357
templates/modals/full_calc.html
Normal file
357
templates/modals/full_calc.html
Normal file
@@ -0,0 +1,357 @@
|
||||
{{ define "full_calc" }}
|
||||
|
||||
<div class="modal modal-lg fade" data-bs-backdrop="static" data-bs-keyboard="false" id="fullCalcModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-3 text-secondary-emphasis fw-bold" id="fullCalcModalLabel">Infantryskill-Ergebnis</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body px-0">
|
||||
<div class="row px-2">
|
||||
<div class="col-6 border-end border-secondary px-1 pb-1" id="home-clan-results">
|
||||
<h3 class="pb-3 text-center text-primary border-bottom border-primary-subtle fw-bold" id="home-clan-name">a</h3>
|
||||
<!-- Dynamically added -->
|
||||
</div>
|
||||
<div class="col-6 px-1 mb-1" id="opponent-clan-results">
|
||||
<h3 class="pb-3 text-center text-danger border-bottom border-danger-subtle fw-bold" id="opp-clan-name">b</h3>
|
||||
<!-- Dynamically added -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-light-emphasis">
|
||||
<hr class="mt-0">
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-3 text-center fs-5 me-4 ps-0">
|
||||
<i class="bi bi-graph-up me-2" data-bs-action="tooltip" data-bs-title="Durchschnitt Heim-Team"></i>
|
||||
<span id="home-avg-score" class="text-warning fw-bold">
|
||||
<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-2 text-center fs-5">
|
||||
<i class="bi bi-plus-slash-minus me-2" data-bs-action="tooltip" data-bs-title="Differenz der Durchschnitte" ></i>
|
||||
<span id="diff-score" class="text-warning fw-bold">
|
||||
<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-3 text-center fs-5 ms-4 pe-0">
|
||||
<i class="bi bi-graph-up me-2" data-bs-action="tooltip" data-bs-title="Durchschnitt Gegner-Team"></i>
|
||||
<span id="opp-avg-score" class="text-warning fw-bold">
|
||||
<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<div class="text-center text-secondary ms-3" id="fullCalcInfo" hidden></div>
|
||||
<div class="progress w-100" style="height: 30px;" id="fullCalcProgressbar" role="progressbar">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0">Lade...</div>
|
||||
</div>
|
||||
<button type="submit" name="submit" class="btn btn-lg btn-outline-primary" onclick="downloadScreenshot()" data-bs-action="tooltip" data-bs-title="Als Bild herunterladen" hidden>
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list-group list-group-horizontal fs-5 px-2 visually-hidden" id="result-item">
|
||||
<li class="list-group-item border-0 rounded-0 border-bottom border-secondary-subtle w-75 text-light">
|
||||
<i class="bi bi-question-square text-secondary me-2"></i>
|
||||
<span id="username"></span>
|
||||
</li>
|
||||
<li class="list-group-item border-0 rounded-0 border-bottom border-secondary-subtle text-end w-25 ps-1 pe-3 text-warning">
|
||||
<i class="bi bi-trophy text-warning me-2" hidden></i>
|
||||
<span id="score"></span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<script lang="javascript">
|
||||
function getSelectedPlayers(listId) {
|
||||
const list = document.getElementById(listId);
|
||||
const selectedPlayerNodes = list.querySelectorAll('input[type="checkbox"]:checked');
|
||||
const selectedPlayers = [];
|
||||
selectedPlayerNodes.forEach(p => {
|
||||
p = p.parentElement.parentElement.parentElement.querySelector('span');
|
||||
selectedPlayers.push([p.parentElement.getAttribute("data-id"), p.innerText]);
|
||||
});
|
||||
return selectedPlayers;
|
||||
}
|
||||
|
||||
function getAndInsertScore(playerID, resultList) {
|
||||
return fetch('/score/' + playerID)
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
const score = resultList.querySelector('ul[data-id="' + playerID + '"] #score');
|
||||
score.innerHTML = text;
|
||||
if (!isNaN(parseFloat(text)))
|
||||
score.previousElementSibling.hidden = false;
|
||||
else
|
||||
initTooltips(score);
|
||||
return text;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadScreenshot() {
|
||||
const submitBtn = document.querySelector('#fullCalcModal button[name="submit"]');
|
||||
submitBtn.hidden = true;
|
||||
html2canvas(document.querySelector('#fullCalcModal .modal-content'), {
|
||||
backgroundColor: null,
|
||||
scale: 1
|
||||
}).then(canvas => {
|
||||
let link = document.createElement('a');
|
||||
let date = new Date().toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }).replace(', ', '_').replace(':', '-');
|
||||
link.download = 'InfantrySkill_' + date + '.png';
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
}).catch(error => {
|
||||
alert('Fehler beim Erstellen des Screenshots: ' + error);
|
||||
}).finally(_ => {
|
||||
submitBtn.hidden = false;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fullCalcModal = document.getElementById('fullCalcModal');
|
||||
const downloadBtn = fullCalcModal.querySelector('button[name="submit"]');
|
||||
const progressbar = document.getElementById('fullCalcProgressbar');
|
||||
const resultItem = document.getElementById('result-item');
|
||||
const homeClanResults = document.getElementById('home-clan-results');
|
||||
const oppClanResults = document.getElementById('opponent-clan-results');
|
||||
const homeClanList = document.getElementById('home-clan');
|
||||
const oppClanList = document.getElementById('opponent-clan');
|
||||
const spinnerBorder = '<i class="spinner-border spinner-border-sm text-warning align-baseline me-2" style="margin-left: 0.91rem;" role="status"></i>';
|
||||
const spinnerGrow = '<i class="spinner-grow spinner-grow-sm text-secondary align-baseline mx-2" role="status"></i>';
|
||||
const dash = '<i class="bi bi-dash mx-2"></i>';
|
||||
|
||||
const homeAvgScore = document.getElementById('home-avg-score');
|
||||
const diffScore = document.getElementById('diff-score');
|
||||
const oppAvgScore = document.getElementById('opp-avg-score');
|
||||
const fullCalcInfo = document.getElementById('fullCalcInfo');
|
||||
|
||||
let calcMutex = false;
|
||||
let sortMutex = false;
|
||||
|
||||
function addResultItem(resultList, playerID, playerName) {
|
||||
let item = resultList.appendChild(resultItem.cloneNode(true));
|
||||
item.querySelector('#username').innerText = playerName;
|
||||
item.querySelector('#score').innerHTML = spinnerBorder;
|
||||
item.setAttribute('data-id', playerID);
|
||||
item.classList.remove('visually-hidden');
|
||||
}
|
||||
|
||||
function calcStatistics() {
|
||||
if (calcMutex) {
|
||||
setTimeout(calcStatistics, 100);
|
||||
return;
|
||||
}
|
||||
calcMutex = true;
|
||||
|
||||
let homeScores = [];
|
||||
let oppScores = [];
|
||||
homeClanResults.querySelectorAll('ul').forEach(e => {
|
||||
let score = parseFloat(e.querySelector('#score').innerText);
|
||||
if (!isNaN(score)) {
|
||||
homeScores.push(score);
|
||||
}
|
||||
});
|
||||
oppClanResults.querySelectorAll('ul').forEach(e => {
|
||||
let score = parseFloat(e.querySelector('#score').innerText);
|
||||
if (!isNaN(score)) {
|
||||
oppScores.push(score);
|
||||
}
|
||||
});
|
||||
|
||||
homeScores.sort((a, b) => a - b);
|
||||
oppScores.sort((a, b) => a - b);
|
||||
let homeAvg = homeScores.reduce((a, b) => a + b, 0) / homeScores.length;
|
||||
let oppAvg = oppScores.reduce((a, b) => a + b, 0) / oppScores.length;
|
||||
|
||||
if (!isNaN(homeAvg))
|
||||
homeAvgScore.innerText = homeAvg.toFixed(2);
|
||||
if (!isNaN(oppAvg))
|
||||
oppAvgScore.innerText = oppAvg.toFixed(2);
|
||||
if (!isNaN(homeAvg) && !isNaN(oppAvg)) {
|
||||
const diff = (((homeAvg / oppAvg) - 1.0) * 100).toFixed(1);
|
||||
diffScore.classList.remove('text-warning');
|
||||
diffScore.classList.remove('text-success');
|
||||
diffScore.classList.remove('text-danger');
|
||||
if (diff > 0.0)
|
||||
diffScore.classList.add('text-success');
|
||||
else if (diff < 0.0)
|
||||
diffScore.classList.add('text-danger');
|
||||
diffScore.innerText = diff + "%";
|
||||
}
|
||||
|
||||
calcMutex = false;
|
||||
}
|
||||
|
||||
function sortPlayers() {
|
||||
if (sortMutex) {
|
||||
setTimeout(sortPlayers, 100);
|
||||
return;
|
||||
}
|
||||
sortMutex = true;
|
||||
|
||||
let homePlayers = Array.from(homeClanResults.querySelectorAll('ul'));
|
||||
let oppPlayers = Array.from(oppClanResults.querySelectorAll('ul'));
|
||||
homePlayers.sort((a, b) => {
|
||||
let aScore = parseFloat(a.querySelector('#score').innerText);
|
||||
let bScore = parseFloat(b.querySelector('#score').innerText);
|
||||
if (isNaN(aScore)) {
|
||||
return 1;
|
||||
} else if (isNaN(bScore)) {
|
||||
return -1;
|
||||
} else {
|
||||
return bScore - aScore;
|
||||
}
|
||||
});
|
||||
oppPlayers.sort((a, b) => {
|
||||
let aScore = parseFloat(a.querySelector('#score').innerText);
|
||||
let bScore = parseFloat(b.querySelector('#score').innerText);
|
||||
if (isNaN(aScore)) {
|
||||
return 1;
|
||||
} else if (isNaN(bScore)) {
|
||||
return -1;
|
||||
} else {
|
||||
return bScore - aScore;
|
||||
}
|
||||
});
|
||||
homePlayers.forEach(p => homeClanResults.appendChild(p));
|
||||
oppPlayers.forEach(p => oppClanResults.appendChild(p));
|
||||
|
||||
sortMutex = false;
|
||||
}
|
||||
|
||||
function setSquadNumbers() {
|
||||
let homePlayers = Array.from(homeClanResults.querySelectorAll('ul'));
|
||||
let oppPlayers = Array.from(oppClanResults.querySelectorAll('ul'));
|
||||
let playerCounter = 0;
|
||||
|
||||
function setSquadNumber(player, squadNum) {
|
||||
player.querySelector('#username').previousElementSibling.classList.replace('bi-question-square', 'bi-' + squadNum + '-square');
|
||||
}
|
||||
|
||||
homePlayers.forEach((p, _) => {
|
||||
if (p.querySelector('#score').innerText === '' && p.querySelector('#score').innerHTML !== spinnerBorder) {
|
||||
setSquadNumber(p, 'x');
|
||||
} else {
|
||||
let squadNum = Math.floor(playerCounter / 4) + 1;
|
||||
setSquadNumber(p, squadNum);
|
||||
playerCounter++;
|
||||
}
|
||||
});
|
||||
playerCounter = 0;
|
||||
oppPlayers.forEach((p, _) => {
|
||||
if (p.querySelector('#score').innerText === '' && p.querySelector('#score').innerHTML !== spinnerBorder) {
|
||||
setSquadNumber(p, 'x');
|
||||
} else {
|
||||
let squadNum = Math.floor(playerCounter / 4) + 1;
|
||||
setSquadNumber(p, squadNum);
|
||||
playerCounter++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupStatistic(statisticField) {
|
||||
if (statisticField.innerHTML.trim() === spinnerGrow)
|
||||
statisticField.innerHTML = dash;
|
||||
}
|
||||
|
||||
function updateScoreInClanList(clanListId, playerID, score) {
|
||||
let clanPlayers = document.getElementById(clanListId).querySelectorAll('div[data-id]');
|
||||
clanPlayers.forEach(p => {
|
||||
if (p.getAttribute('data-id') === playerID) {
|
||||
p.querySelector('#quickScore').innerHTML = score;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getClanName(clanList) {
|
||||
let clanName = clanList.options[clanList.selectedIndex].innerText;
|
||||
return clanList.selectedIndex <= 0 ? dash : clanName.replace(/\[.*]/, '').trim();
|
||||
}
|
||||
|
||||
function updateProgress(progress) {
|
||||
progressbar.firstElementChild.style.width = progress + '%';
|
||||
}
|
||||
|
||||
if (fullCalcModal) {
|
||||
fullCalcModal.addEventListener('show.bs.modal', _ => {
|
||||
let homePlayers = getSelectedPlayers('home-player-list');
|
||||
let oppPlayers = getSelectedPlayers('opponent-player-list');
|
||||
let promises = [];
|
||||
let doneCounter = 0;
|
||||
|
||||
homeClanResults.querySelector('#home-clan-name').innerHTML = getClanName(homeClanList);
|
||||
oppClanResults.querySelector('#opp-clan-name').innerHTML = getClanName(oppClanList);
|
||||
|
||||
homePlayers.forEach(p => {
|
||||
addResultItem(homeClanResults, p[0], p[1]);
|
||||
promises.push(
|
||||
getAndInsertScore(p[0], homeClanResults)
|
||||
.then(score => {
|
||||
calcStatistics();
|
||||
sortPlayers();
|
||||
updateScoreInClanList('home-player-list', p[0], score);
|
||||
updateProgress(100 * (++doneCounter / (homePlayers.length + oppPlayers.length)));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
oppPlayers.forEach(p => {
|
||||
addResultItem(oppClanResults, p[0], p[1]);
|
||||
promises.push(
|
||||
getAndInsertScore(p[0], oppClanResults)
|
||||
.then(score => {
|
||||
calcStatistics();
|
||||
sortPlayers();
|
||||
updateScoreInClanList('opponent-player-list', p[0], score);
|
||||
updateProgress(100 * (++doneCounter / (homePlayers.length + oppPlayers.length)));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Promise.all(promises).then(_ => {
|
||||
let liItems = homeClanResults.querySelectorAll('li');
|
||||
if (liItems.length > 1) {
|
||||
liItems[liItems.length - 1].classList.remove('border-bottom');
|
||||
liItems[liItems.length - 2].classList.remove('border-bottom');
|
||||
}
|
||||
liItems = oppClanResults.querySelectorAll('li');
|
||||
if (liItems.length > 1) {
|
||||
liItems[liItems.length - 1].classList.remove('border-bottom');
|
||||
liItems[liItems.length - 2].classList.remove('border-bottom');
|
||||
}
|
||||
|
||||
setSquadNumbers();
|
||||
|
||||
cleanupStatistic(homeAvgScore);
|
||||
cleanupStatistic(diffScore);
|
||||
cleanupStatistic(oppAvgScore);
|
||||
|
||||
progressbar.hidden = true;
|
||||
downloadBtn.hidden = false;
|
||||
fullCalcInfo.hidden = false;
|
||||
fullCalcInfo.innerHTML = 'Erstellt von <b>{{ .Username }}</b> am ' +
|
||||
new Date().toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'short' }) + ' Uhr';
|
||||
});
|
||||
});
|
||||
|
||||
fullCalcModal.addEventListener('hidden.bs.modal', _ => {
|
||||
homeClanResults.querySelectorAll('ul').forEach(e => e.remove());
|
||||
oppClanResults.querySelectorAll('ul').forEach(e => e.remove());
|
||||
homeAvgScore.innerHTML = spinnerGrow;
|
||||
diffScore.innerHTML = spinnerGrow;
|
||||
oppAvgScore.innerHTML = spinnerGrow;
|
||||
downloadBtn.hidden = true;
|
||||
fullCalcInfo.hidden = true;
|
||||
progressbar.hidden = false;
|
||||
progressbar.firstElementChild.style.width = '0';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
@@ -1,6 +1,6 @@
|
||||
{{ define "settings" }}
|
||||
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1">
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="settingsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -18,15 +18,15 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mt-3 fs-5">
|
||||
<div class="form-check form-check-inline mt-3 fs-5 visually-hidden">
|
||||
<input class="form-check-input" type="checkbox" id="settingsSquadColors" value="squadColors" checked>
|
||||
<label class="form-check-label" for="settingsSquadColors">Squad-Farben aktivieren</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mt-3 fs-5">
|
||||
<div class="form-check form-check-inline mt-3 fs-5 visually-hidden" data-bs-action="tooltip" data-bs-title="Median zusätzlich berechnen">
|
||||
<input class="form-check-input" type="checkbox" id="settingsCalcMedian" value="calcMedian">
|
||||
<label class="form-check-label" for="settingsCalcMedian">Median aktivieren</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mt-3 fs-5">
|
||||
<div class="form-check form-check-inline mt-3 fs-5 visually-hidden">
|
||||
<input class="form-check-input" type="checkbox" id="settingsUseCache" value="useCache" checked>
|
||||
<label class="form-check-label" for="settingsUseCache">Infantryskill-Cache benutzen (empfohlen)</label>
|
||||
</div>
|
||||
@@ -102,7 +102,6 @@
|
||||
return response.json();
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
games.selectedIndex = games.querySelector(`option[value="${result['active_game_id']}"]`).index;
|
||||
squadColors.checked = result['squad_colors'];
|
||||
calcMedian.value = result['calc_median'];
|
||||
@@ -112,6 +111,12 @@
|
||||
alert('Fehler beim Laden der Einstellungen: ' + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
settingsModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
115
templates/modals/single_calc.html
Normal file
115
templates/modals/single_calc.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{{ define "single_calc" }}
|
||||
|
||||
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="singleCalcModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-3 text-success fw-bold" id="singleCalcModalLabel">Einzel-Abfrage</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-lg pb-1" id="singleCalcPlayerName" placeholder="Spieler-Name" required>
|
||||
<label for="singleCalcPlayerName">Spieler-Name</label>
|
||||
</div>
|
||||
<div class="text-warning fs-5 badge mt-2" id="resultScore" style="display: none;"></div>
|
||||
<div class="error-message text-danger fs-5 badge mt-2" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="submit" class="btn btn-lg btn-primary">Berechnen</button>
|
||||
<button type="button" class="btn btn-lg btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script lang="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const singleCalcModal = document.getElementById('singleCalcModal');
|
||||
let submitScoreHandler = null;
|
||||
|
||||
const submitButton = singleCalcModal.querySelector('button[name="submit"]');
|
||||
const playerName = singleCalcModal.querySelector('#singleCalcPlayerName');
|
||||
const errorDiv = singleCalcModal.querySelector('.error-message');
|
||||
const resultScore = singleCalcModal.querySelector('#resultScore');
|
||||
const spinner = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>Lade Daten...'
|
||||
|
||||
function validateInput() {
|
||||
if (playerName.value.length < 1) {
|
||||
playerName.classList.add('is-invalid');
|
||||
return false;
|
||||
}
|
||||
playerName.classList.remove('is-invalid');
|
||||
return true;
|
||||
}
|
||||
|
||||
function createSubmitScoreHandler(_) {
|
||||
return function submitScoreHandler(_) {
|
||||
if (!validateInput())
|
||||
return;
|
||||
|
||||
submitButton.innerHTML = spinner;
|
||||
submitButton.disabled = true;
|
||||
resultScore.style.display = 'none';
|
||||
playerName.disabled = true;
|
||||
|
||||
fetch("/score/" + playerName.value, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: playerName.value
|
||||
}),
|
||||
headers: {
|
||||
"Content-type": "application/json; charset=UTF-8"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Berechnung fehlgeschlagen!\nSpielername existiert nicht.');
|
||||
} else if (response.status === 503 || response.status === 504) {
|
||||
throw new Error('Berechnung fehlgeschlagen!\nTracker überlastet. Versuche es später erneut.');
|
||||
} else if (!response.ok) {
|
||||
throw new Error('Berechnung fehlgeschlagen!\nName ungültig oder Tracker nicht erreichbar.');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((response) => {
|
||||
resultScore.innerText = 'Spieler hat ' + response + ' Punkte.';
|
||||
resultScore.style.display = 'block';
|
||||
}).catch((error) => {
|
||||
errorDiv.innerText = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
}).finally(() => {
|
||||
submitButton.innerHTML = 'Berechnen';
|
||||
submitButton.disabled = false;
|
||||
playerName.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (singleCalcModal) {
|
||||
singleCalcModal.addEventListener('show.bs.modal', event => {
|
||||
submitScoreHandler = createSubmitScoreHandler(event);
|
||||
submitButton.addEventListener('click', submitScoreHandler);
|
||||
});
|
||||
|
||||
singleCalcModal.addEventListener('keypress', event => {
|
||||
if (event.key === 'Enter') {
|
||||
submitButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
singleCalcModal.addEventListener('hidden.bs.modal', _ => {
|
||||
submitButton.removeEventListener('click', submitScoreHandler);
|
||||
|
||||
playerName.value = "";
|
||||
errorDiv.innerText = "";
|
||||
errorDiv.style.display = 'none';
|
||||
resultScore.innerText = "";
|
||||
resultScore.style.display = 'none';
|
||||
playerName.classList.remove('is-invalid');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{ end }}
|
||||
@@ -1,21 +0,0 @@
|
||||
<div class="input-group input-group-lg mb-1">
|
||||
<div class="input-group-text py-1 px-2">
|
||||
<input class="form-check-input fs-4 border-secondary mt-0" type="checkbox" value="" onchange="updateSelectedPlayers(this)">
|
||||
</div>
|
||||
<span class="form-control py-2 px-3" style="width: 10em">%s</span>
|
||||
<div class="form-control text-center px-2 text-secondary">
|
||||
<i class="bi bi-trophy me-3 text-warning"></i><span id="quickScore" class="text-secondary-emphasis">%s</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary" hx-get="/score/%s" hx-target="previous #quickScore" onclick="singleCalcSpinner(this)">
|
||||
<i class="bi bi-calculator text-info"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item text-primary fs-5" href="#" data-bs-toggle="modal" data-bs-id="%d" data-bs-target="#editPlayerModal">
|
||||
<i class="bi bi-person-gear fs-4 me-2"></i>Bearbeiten
|
||||
</a></li>
|
||||
<li><a class="dropdown-item text-danger fs-5" href="#" data-bs-toggle="modal" data-bs-id="%d" data-bs-target="#deletePlayerModal">
|
||||
<i class="bi bi-person-dash fs-4 me-2"></i>Löschen
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -9,28 +9,28 @@
|
||||
<hr>
|
||||
<form hx-post="/register" hx-target="#register-result" class="position-relative needs-validation" novalidate>
|
||||
<div class="form-floating mb-3">
|
||||
<input class="form-control form-control-lg" type="text" minlength="4" id="username" name="username" placeholder="Username" required>
|
||||
<input class="form-control form-control-lg pb-1" type="text" minlength="4" id="username" name="username" placeholder="Username" required>
|
||||
<label for="username">Username</label>
|
||||
<div class="invalid-feedback">
|
||||
Mindestlänge: 4 Zeichen
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input class="form-control form-control-lg" type="password" minlength="8" id="password" name="password" placeholder="Passwort" required>
|
||||
<input class="form-control form-control-lg pb-1" type="password" minlength="8" maxlength="72" id="password" name="password" placeholder="Passwort" required>
|
||||
<label for="password">Passwort</label>
|
||||
<div class="invalid-feedback">
|
||||
Mindestlänge: 8 Zeichen
|
||||
Min. 8 Zeichen, max. 72 Zeichen
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-floating">
|
||||
<input class="form-control form-control-lg" type="password" minlength="32" maxlength="32" id="code" name="code" placeholder="Aktivierungscode" required>
|
||||
<input class="form-control form-control-lg pb-1" type="password" minlength="32" maxlength="32" id="code" name="code" placeholder="Aktivierungscode" required>
|
||||
<label for="code">Aktivierungscode</label>
|
||||
<div class="invalid-feedback">
|
||||
Ungültiges Format
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary mt-4" type="submit"><i class="bi bi-key me-3"></i>Registrieren</button>
|
||||
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/login" data-bs-toggle="tooltip" data-bs-title="Zurück zur Anmeldung"><i class="bi bi-door-open"></i></a>
|
||||
<a class="btn btn-lg btn-outline-primary mt-4 ms-2 position-absolute end-0" href="/login" data-bs-action="tooltip" data-bs-title="Zurück zur Anmeldung"><i class="bi bi-door-open"></i></a>
|
||||
</form>
|
||||
<div id="register-result" class="fs-4 mt-2"></div>
|
||||
</div>
|
||||
|
||||
27
templates/shards/player_list_item.html
Normal file
27
templates/shards/player_list_item.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{{ range . }}
|
||||
<div class="input-group input-group-lg mb-1" data-id="{{ .PlayerID }}">
|
||||
<div class="input-group-text py-1 px-2">
|
||||
<label>
|
||||
<input class="form-check-input fs-4 border-secondary mt-1" type="checkbox" value="" onchange="updateSelectedPlayers(this)">
|
||||
</label>
|
||||
</div>
|
||||
<span class="form-control py-2 px-3" style="width: 10em">{{ .PlayerName }}</span>
|
||||
<div class="form-control text-center px-2 text-secondary border-start-0">
|
||||
<i class="bi bi-trophy me-3 text-warning"></i><span id="quickScore" class="text-secondary-emphasis">{{ .Score }}</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary" hx-get="/score/{{ .PlayerID }}" hx-target="previous #quickScore" onclick="singleCalcSpinner(this)" data-bs-action="tooltip" data-bs-title="Schnell-Berechnung">
|
||||
<i class="bi bi-calculator-fill text-warning"></i>
|
||||
</button>
|
||||
{{ if not (eq .UserRole "READER") }}
|
||||
<button class="btn btn-outline-secondary text-secondary-emphasis dropdown-toggle py-1" type="button" data-bs-toggle="dropdown"></button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><button class="dropdown-item text-primary fs-5" data-bs-toggle="modal" data-bs-id="{{ .PlayerID }}" data-bs-target="#editPlayerModal">
|
||||
<i class="bi bi-person-fill-gear fs-4 me-2"></i>Bearbeiten
|
||||
</button></li>
|
||||
<li><button class="dropdown-item text-danger fs-5" data-bs-toggle="modal" data-bs-id="{{ .PlayerID }}" data-bs-target="#deletePlayerModal">
|
||||
<i class="bi bi-person-fill-dash fs-4 me-2"></i>Löschen
|
||||
</button></li>
|
||||
</ul>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -10,7 +10,11 @@ func UintToString(val uint) string {
|
||||
}
|
||||
|
||||
func StringToUint(val string) uint {
|
||||
res, _ := strconv.ParseUint(val, 10, 16)
|
||||
res, err := strconv.ParseUint(val, 10, 16)
|
||||
if err != nil {
|
||||
Logger.Warnf("StringToUint error for %s: %s", val, err.Error())
|
||||
return 0
|
||||
}
|
||||
return uint(res)
|
||||
}
|
||||
|
||||
@@ -19,6 +23,10 @@ func FloatToString(val float32) string {
|
||||
}
|
||||
|
||||
func StringToFloat(val string) float32 {
|
||||
res, _ := strconv.ParseFloat(val, 32)
|
||||
res, err := strconv.ParseFloat(val, 32)
|
||||
if err != nil {
|
||||
Logger.Warnf("StringToFloat error for %s: %s", val, err.Error())
|
||||
return 0
|
||||
}
|
||||
return float32(res)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"io"
|
||||
"github.com/sirupsen/logrus"
|
||||
"html/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
var GinWriter io.Writer = nil
|
||||
var GameMetrics models.GameMetrics
|
||||
var Logger *logrus.Logger
|
||||
var PlayerCacheLifetime = 24 * time.Hour
|
||||
|
||||
var MainPageTemplates *template.Template
|
||||
var LoginPageTemplates *template.Template
|
||||
var RegisterPageTemplates *template.Template
|
||||
var PlayerItemTemplate *template.Template
|
||||
|
||||
@@ -3,12 +3,30 @@ package utils
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/klauspost/compress/flate"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
)
|
||||
|
||||
func GenerateActivationCode() string {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
panic(err) // Handle the error appropriately in production
|
||||
Logger.Fatalf("Error generating activation code: %v", err)
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func DecompressResponseBody(encoding string, body io.Reader) (io.ReadCloser, error) {
|
||||
switch encoding {
|
||||
case "gzip":
|
||||
return gzip.NewReader(body)
|
||||
case "deflate":
|
||||
return flate.NewReader(body), nil
|
||||
case "br":
|
||||
return io.NopCloser(brotli.NewReader(body)), nil
|
||||
default:
|
||||
return io.NopCloser(body), nil
|
||||
}
|
||||
}
|
||||
129
utils/score.go
129
utils/score.go
@@ -1,129 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"InfantrySkillCalculator/models"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func GetGameMetric(gameTag string) *models.GameMetric {
|
||||
for _, metric := range GameMetrics.GameMetrics {
|
||||
if metric.GameName == gameTag {
|
||||
return &metric
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindWeaponMetric(weaponMetrics []models.WeaponMetric, weaponCategory string) *models.WeaponMetric {
|
||||
for _, metric := range weaponMetrics {
|
||||
if metric.WeaponCategory == weaponCategory {
|
||||
return &metric
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CalcPlayerScore(playerName string, gameTag string) float32 {
|
||||
if gameTag != "BF5" && gameTag != "BF2042" {
|
||||
_, _ = fmt.Fprintf(GinWriter, "UNSUPPORTED GAME: "+gameTag+"\n")
|
||||
return -1
|
||||
}
|
||||
|
||||
gameMetrics := GetGameMetric(gameTag)
|
||||
if gameMetrics == nil {
|
||||
_, _ = fmt.Fprintf(GinWriter, "No game metrics specified for '"+gameTag+"'\n")
|
||||
return -1
|
||||
}
|
||||
|
||||
normalizeFactor := gameMetrics.NormalizeFactor
|
||||
topWeaponCount := gameMetrics.TopWeaponCount
|
||||
|
||||
c := http.Client{}
|
||||
|
||||
var reqUri string
|
||||
if gameTag == "BF5" {
|
||||
reqUri = "https://api.gametools.network/bfv/weapons/?raw=false&format_values=false&name=" + playerName + "&platform=pc"
|
||||
} else if gameTag == "BF2042" {
|
||||
reqUri = "https://api.gametools.network/bf2042/stats/?raw=false&format_values=false&name=" + playerName + "&platform=pc"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", reqUri, nil)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(GinWriter, err.Error()+"\n")
|
||||
return -1
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36")
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(GinWriter, err.Error()+"\n")
|
||||
return -1
|
||||
}
|
||||
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(res.Body)
|
||||
if res.StatusCode == 404 {
|
||||
_, _ = fmt.Fprintf(GinWriter, "User '"+playerName+"' does not exist!\n")
|
||||
return -1
|
||||
} else if res.StatusCode != 200 {
|
||||
log.Fatalf("Status code error: %d %s", res.StatusCode, res.Status)
|
||||
}
|
||||
|
||||
var data models.TrackerWeaponJSON
|
||||
var body, _ = io.ReadAll(res.Body)
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
log.Fatalf("Failed to deserialize tracker API response: %s", err)
|
||||
}
|
||||
|
||||
sort.SliceStable(data.Weapons, func(i, j int) bool { return data.Weapons[i].Kills > data.Weapons[j].Kills })
|
||||
var top []models.Weapon
|
||||
|
||||
for _, weapon := range data.Weapons {
|
||||
if len(top) >= topWeaponCount {
|
||||
break
|
||||
}
|
||||
if gameTag == "BF2042" && weapon.Kills < 100 {
|
||||
break
|
||||
}
|
||||
|
||||
acc := (float64(weapon.ShotsHit) / float64(weapon.ShotsFired)) * 100
|
||||
kpm := weapon.KPM
|
||||
|
||||
weaponMetrics := FindWeaponMetric(gameMetrics.WeaponMetrics, weapon.Type)
|
||||
if weaponMetrics == nil {
|
||||
_, _ = fmt.Fprintf(GinWriter, "No weapon metrics specified for '"+gameTag+"', WType '"+weapon.Type+"'\n")
|
||||
continue
|
||||
}
|
||||
|
||||
accFactor := weaponMetrics.AccuracyFactor
|
||||
kpmFactor := weaponMetrics.KpmFactor
|
||||
|
||||
acc /= accFactor
|
||||
kpm /= kpmFactor
|
||||
weapon.Accuracy = acc
|
||||
weapon.KPM = kpm
|
||||
|
||||
top = append(top, weapon)
|
||||
}
|
||||
|
||||
sumAcc := 0.0
|
||||
sumKpm := 0.0
|
||||
|
||||
for _, w := range top {
|
||||
sumAcc += w.Accuracy
|
||||
sumKpm += w.KPM
|
||||
}
|
||||
accAvg := sumAcc / float64(len(top))
|
||||
kpmAvg := sumKpm / float64(len(top))
|
||||
score := math.Round(((accAvg*kpmAvg)/normalizeFactor)*100) / 100
|
||||
|
||||
return float32(score)
|
||||
}
|
||||
Reference in New Issue
Block a user