[add] better log page, [add] admin users page, [add] admin nav
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Evan Reichard 2024-01-20 14:26:26 -05:00
parent a65750ae21
commit f0a2d2cf69
20 changed files with 265 additions and 77 deletions

View File

@ -3,7 +3,7 @@ FROM alpine AS certs
RUN apk update && apk add ca-certificates RUN apk update && apk add ca-certificates
# Build Image # Build Image
FROM golang:1.20 AS build FROM golang:1.21 AS build
# Copy Source # Copy Source
WORKDIR /src WORKDIR /src
@ -13,7 +13,9 @@ COPY . .
RUN mkdir -p /opt/antholume RUN mkdir -p /opt/antholume
# Compile # Compile
RUN go build -o /opt/antholume/server RUN go build \
-ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" \
-o /opt/antholume/server
# Create Image # Create Image
FROM busybox:1.36 FROM busybox:1.36

View File

@ -3,7 +3,7 @@ FROM alpine AS certs
RUN apk update && apk add ca-certificates RUN apk update && apk add ca-certificates
# Build Image # Build Image
FROM --platform=$BUILDPLATFORM golang:1.20 AS build FROM --platform=$BUILDPLATFORM golang:1.21 AS build
# Create Package Directory # Create Package Directory
WORKDIR /src WORKDIR /src
@ -15,7 +15,9 @@ ARG TARGETARCH
RUN --mount=target=. \ RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \ --mount=type=cache,target=/go/pkg \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /opt/antholume/server GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
-ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" \
-o /opt/antholume/server
# Create Image # Create Image
FROM busybox:1.36 FROM busybox:1.36

View File

@ -3,10 +3,10 @@ build_local: build_tailwind
rm -r ./build || true rm -r ./build || true
mkdir -p ./build mkdir -p ./build
env GOOS=linux GOARCH=amd64 go build -o ./build/server_linux_amd64 env GOOS=linux GOARCH=amd64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_linux_amd64
env GOOS=linux GOARCH=arm64 go build -o ./build/server_linux_arm64 env GOOS=linux GOARCH=arm64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_linux_arm64
env GOOS=darwin GOARCH=arm64 go build -o ./build/server_darwin_arm64 env GOOS=darwin GOARCH=arm64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_darwin_arm64
env GOOS=darwin GOARCH=amd64 go build -o ./build/server_darwin_amd64 env GOOS=darwin GOARCH=amd64 go build -ldflags "-X reichard.io/antholume/config.version=`git describe --tags`" -o ./build/server_darwin_amd64
docker_build_local: build_tailwind docker_build_local: build_tailwind
docker build -t antholume:latest . docker build -t antholume:latest .

View File

@ -112,6 +112,7 @@ func (api *API) registerWebAppRoutes() {
api.Router.GET("/register", api.appGetRegister) api.Router.GET("/register", api.appGetRegister)
api.Router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings) api.Router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings)
api.Router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs) api.Router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
api.Router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers)
api.Router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin) api.Router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
api.Router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction) api.Router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
api.Router.POST("/login", api.appAuthFormLogin) api.Router.POST("/login", api.appAuthFormLogin)
@ -184,6 +185,7 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
"GetUTCOffsets": getUTCOffsets, "GetUTCOffsets": getUTCOffsets,
"NiceSeconds": niceSeconds, "NiceSeconds": niceSeconds,
"dict": dict, "dict": dict,
"hasPrefix": strings.HasPrefix,
} }
// Load Base // Load Base

View File

@ -2,6 +2,7 @@ package api
import ( import (
"archive/zip" "archive/zip"
"bufio"
"crypto/md5" "crypto/md5"
"database/sql" "database/sql"
"fmt" "fmt"
@ -132,8 +133,7 @@ func (api *API) appDocumentReader(c *gin.Context) {
} }
func (api *API) appGetDocuments(c *gin.Context) { func (api *API) appGetDocuments(c *gin.Context) {
templateVars := api.getBaseTemplateVars("documents", c) templateVars, auth := api.getBaseTemplateVars("documents", c)
auth := templateVars["Authorization"].(authData)
qParams := bindQueryParams(c, 9) qParams := bindQueryParams(c, 9)
var query *string var query *string
@ -184,8 +184,7 @@ func (api *API) appGetDocuments(c *gin.Context) {
} }
func (api *API) appGetDocument(c *gin.Context) { func (api *API) appGetDocument(c *gin.Context) {
templateVars := api.getBaseTemplateVars("document", c) templateVars, auth := api.getBaseTemplateVars("document", c)
auth := templateVars["Authorization"].(authData)
var rDocID requestDocumentID var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil { if err := c.ShouldBindUri(&rDocID); err != nil {
@ -211,8 +210,7 @@ func (api *API) appGetDocument(c *gin.Context) {
} }
func (api *API) appGetProgress(c *gin.Context) { func (api *API) appGetProgress(c *gin.Context) {
templateVars := api.getBaseTemplateVars("progress", c) templateVars, auth := api.getBaseTemplateVars("progress", c)
auth := templateVars["Authorization"].(authData)
qParams := bindQueryParams(c, 15) qParams := bindQueryParams(c, 15)
@ -240,8 +238,7 @@ func (api *API) appGetProgress(c *gin.Context) {
} }
func (api *API) appGetActivity(c *gin.Context) { func (api *API) appGetActivity(c *gin.Context) {
templateVars := api.getBaseTemplateVars("activity", c) templateVars, auth := api.getBaseTemplateVars("activity", c)
auth := templateVars["Authorization"].(authData)
qParams := bindQueryParams(c, 15) qParams := bindQueryParams(c, 15)
activityFilter := database.GetActivityParams{ activityFilter := database.GetActivityParams{
@ -268,8 +265,7 @@ func (api *API) appGetActivity(c *gin.Context) {
} }
func (api *API) appGetHome(c *gin.Context) { func (api *API) appGetHome(c *gin.Context) {
templateVars := api.getBaseTemplateVars("home", c) templateVars, auth := api.getBaseTemplateVars("home", c)
auth := templateVars["Authorization"].(authData)
start := time.Now() start := time.Now()
graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, auth.UserName) graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, auth.UserName)
@ -293,8 +289,7 @@ func (api *API) appGetHome(c *gin.Context) {
} }
func (api *API) appGetSettings(c *gin.Context) { func (api *API) appGetSettings(c *gin.Context) {
templateVars := api.getBaseTemplateVars("settings", c) templateVars, auth := api.getBaseTemplateVars("settings", c)
auth := templateVars["Authorization"].(authData)
user, err := api.DB.Queries.GetUser(api.DB.Ctx, auth.UserName) user, err := api.DB.Queries.GetUser(api.DB.Ctx, auth.UserName)
if err != nil { if err != nil {
@ -319,11 +314,13 @@ func (api *API) appGetSettings(c *gin.Context) {
} }
func (api *API) appGetAdmin(c *gin.Context) { func (api *API) appGetAdmin(c *gin.Context) {
templateVars := api.getBaseTemplateVars("admin", c) templateVars, _ := api.getBaseTemplateVars("admin", c)
c.HTML(http.StatusOK, "page/admin", templateVars) c.HTML(http.StatusOK, "page/admin", templateVars)
} }
func (api *API) appGetAdminLogs(c *gin.Context) { func (api *API) appGetAdminLogs(c *gin.Context) {
templateVars, _ := api.getBaseTemplateVars("admin-logs", c)
// Open Log File // Open Log File
logPath := path.Join(api.Config.ConfigPath, "logs/antholume.log") logPath := path.Join(api.Config.ConfigPath, "logs/antholume.log")
logFile, err := os.Open(logPath) logFile, err := os.Open(logPath)
@ -333,18 +330,38 @@ func (api *API) appGetAdminLogs(c *gin.Context) {
} }
defer logFile.Close() defer logFile.Close()
// Write Log File // Log Lines
c.Stream(func(w io.Writer) bool { var logLines []string
_, err = io.Copy(w, logFile) scanner := bufio.NewScanner(logFile)
if err != nil { for scanner.Scan() {
return true logLines = append(logLines, scanner.Text())
} }
return false templateVars["Data"] = logLines
})
c.HTML(http.StatusOK, "page/admin-logs", templateVars)
} }
func (api *API) appGetAdminUsers(c *gin.Context) {
templateVars, _ := api.getBaseTemplateVars("admin-users", c)
users, err := api.DB.Queries.GetUsers(api.DB.Ctx)
if err != nil {
log.Error("[appGetAdminUsers] GetUsers DB Error:", err)
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUsers DB Error: %v", err))
return
}
templateVars["Data"] = users
c.HTML(http.StatusOK, "page/admin-users", templateVars)
}
// Tabs:
// - General (Import, Backup & Restore, Version (githash?), Stats?)
// - Users
// - Metadata
func (api *API) appPerformAdminAction(c *gin.Context) { func (api *API) appPerformAdminAction(c *gin.Context) {
templateVars := api.getBaseTemplateVars("admin", c) templateVars, _ := api.getBaseTemplateVars("admin", c)
var rAdminAction requestAdminAction var rAdminAction requestAdminAction
if err := c.ShouldBind(&rAdminAction); err != nil { if err := c.ShouldBind(&rAdminAction); err != nil {
@ -438,7 +455,7 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
} }
func (api *API) appGetSearch(c *gin.Context) { func (api *API) appGetSearch(c *gin.Context) {
templateVars := api.getBaseTemplateVars("search", c) templateVars, _ := api.getBaseTemplateVars("search", c)
var sParams searchParams var sParams searchParams
c.BindQuery(&sParams) c.BindQuery(&sParams)
@ -462,7 +479,7 @@ func (api *API) appGetSearch(c *gin.Context) {
} }
func (api *API) appGetLogin(c *gin.Context) { func (api *API) appGetLogin(c *gin.Context) {
templateVars := api.getBaseTemplateVars("login", c) templateVars, _ := api.getBaseTemplateVars("login", c)
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
c.HTML(http.StatusOK, "page/login", templateVars) c.HTML(http.StatusOK, "page/login", templateVars)
} }
@ -473,7 +490,7 @@ func (api *API) appGetRegister(c *gin.Context) {
return return
} }
templateVars := api.getBaseTemplateVars("login", c) templateVars, _ := api.getBaseTemplateVars("login", c)
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
templateVars["Register"] = true templateVars["Register"] = true
c.HTML(http.StatusOK, "page/login", templateVars) c.HTML(http.StatusOK, "page/login", templateVars)
@ -842,8 +859,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
} }
// Get Template Variables // Get Template Variables
templateVars := api.getBaseTemplateVars("document", c) templateVars, auth := api.getBaseTemplateVars("document", c)
auth := templateVars["Authorization"].(authData)
// Get Metadata // Get Metadata
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{ metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
@ -900,12 +916,15 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
} }
// Render Initial Template // Render Initial Template
templateVars := api.getBaseTemplateVars("search", c) templateVars, _ := api.getBaseTemplateVars("search", c)
c.HTML(http.StatusOK, "page/search", templateVars) c.HTML(http.StatusOK, "page/search", templateVars)
// Create Streamer // Create Streamer
stream := api.newStreamer(c) stream := api.newStreamer(c, `
defer stream.close() <div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div id="stream-main" class="relative max-h-[95%] -translate-x-2/4 top-1/2 left-1/2 w-5/6">`)
defer stream.close(`</div></div>`)
// Stream Helper Function // Stream Helper Function
sendDownloadMessage := func(msg string, args ...map[string]any) { sendDownloadMessage := func(msg string, args ...map[string]any) {
@ -1061,8 +1080,7 @@ func (api *API) appEditSettings(c *gin.Context) {
return return
} }
templateVars := api.getBaseTemplateVars("settings", c) templateVars, auth := api.getBaseTemplateVars("settings", c)
auth := templateVars["Authorization"].(authData)
newUserSettings := database.UpdateUserParams{ newUserSettings := database.UpdateUserParams{
UserID: auth.UserName, UserID: auth.UserName,
@ -1167,7 +1185,7 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats
return nil return nil
} }
func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) gin.H { func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) (gin.H, authData) {
var auth authData var auth authData
if data, _ := c.Get("Authorization"); data != nil { if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData) auth = data.(authData)
@ -1181,7 +1199,7 @@ func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) gin.H {
"SearchEnabled": api.Config.SearchEnabled, "SearchEnabled": api.Config.SearchEnabled,
"RegistrationEnabled": api.Config.RegistrationEnabled, "RegistrationEnabled": api.Config.RegistrationEnabled,
}, },
} }, auth
} }
func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams { func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {

View File

@ -141,7 +141,7 @@ func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
} }
func (api *API) appAuthFormLogin(c *gin.Context) { func (api *API) appAuthFormLogin(c *gin.Context) {
templateVars := api.getBaseTemplateVars("login", c) templateVars, _ := api.getBaseTemplateVars("login", c)
username := strings.TrimSpace(c.PostForm("username")) username := strings.TrimSpace(c.PostForm("username"))
rawPassword := strings.TrimSpace(c.PostForm("password")) rawPassword := strings.TrimSpace(c.PostForm("password"))
@ -179,7 +179,7 @@ func (api *API) appAuthFormRegister(c *gin.Context) {
return return
} }
templateVars := api.getBaseTemplateVars("login", c) templateVars, _ := api.getBaseTemplateVars("login", c)
templateVars["Register"] = true templateVars["Register"] = true
username := strings.TrimSpace(c.PostForm("username")) username := strings.TrimSpace(c.PostForm("username"))

View File

@ -17,7 +17,7 @@ type streamer struct {
completeCh chan struct{} completeCh chan struct{}
} }
func (api *API) newStreamer(c *gin.Context) *streamer { func (api *API) newStreamer(c *gin.Context, data string) *streamer {
stream := &streamer{ stream := &streamer{
templates: api.Templates, templates: api.Templates,
writer: c.Writer, writer: c.Writer,
@ -32,10 +32,7 @@ func (api *API) newStreamer(c *gin.Context) *streamer {
stream.writer.WriteHeader(http.StatusOK) stream.writer.WriteHeader(http.StatusOK)
// Send Open Element Tags // Send Open Element Tags
stream.write(` stream.write(data)
<div class="absolute top-0 left-0 w-full h-full z-50">
<div class="fixed top-0 left-0 bg-black opacity-50 w-screen h-screen"></div>
<div id="stream-main" class="relative max-h-[95%] -translate-x-2/4 top-1/2 left-1/2 w-5/6">`)
// Keep Alive // Keep Alive
go func() { go func() {
@ -70,9 +67,9 @@ func (stream *streamer) send(templateName string, templateVars gin.H) {
stream.write(buf.String()) stream.write(buf.String())
} }
func (stream *streamer) close() { func (stream *streamer) close(data string) {
// Send Close Element Tags // Send Close Element Tags
stream.write(`</div></div>`) stream.write(data)
// Close // Close
close(stream.completeCh) close(stream.completeCh)

File diff suppressed because one or more lines are too long

View File

@ -43,9 +43,12 @@ func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
return u.Formatter.Format(e) return u.Formatter.Format(e)
} }
// Set at runtime
var version string = "develop"
func Load() *Config { func Load() *Config {
c := &Config{ c := &Config{
Version: "0.0.1", Version: version,
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")), DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")), DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
ConfigPath: getEnv("CONFIG_PATH", "/config"), ConfigPath: getEnv("CONFIG_PATH", "/config"),

View File

@ -305,6 +305,9 @@ WHERE id = $user_id LIMIT 1;
SELECT * FROM user_streaks SELECT * FROM user_streaks
WHERE user_id = $user_id; WHERE user_id = $user_id;
-- name: GetUsers :many
SELECT * FROM users;
-- name: GetWPMLeaderboard :many -- name: GetWPMLeaderboard :many
SELECT SELECT
user_id, user_id,

View File

@ -1008,6 +1008,39 @@ func (q *Queries) GetUserStreaks(ctx context.Context, userID string) ([]UserStre
return items, nil return items, nil
} }
const getUsers = `-- name: GetUsers :many
SELECT id, pass, admin, time_offset, created_at FROM users
`
func (q *Queries) GetUsers(ctx context.Context) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getUsers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.Pass,
&i.Admin,
&i.TimeOffset,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWPMLeaderboard = `-- name: GetWPMLeaderboard :many const getWPMLeaderboard = `-- name: GetWPMLeaderboard :many
SELECT SELECT
user_id, user_id,

2
go.mod
View File

@ -1,6 +1,6 @@
module reichard.io/antholume module reichard.io/antholume
go 1.19 go 1.21
require ( require (
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1

8
go.sum
View File

@ -43,6 +43,7 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -63,8 +64,10 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 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/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@ -193,6 +196,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -254,7 +258,9 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@ -268,8 +274,10 @@ modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 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 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= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -6,7 +6,11 @@ module.exports = {
"./assets/reader/*.{html,htm,svg,js}", "./assets/reader/*.{html,htm,svg,js}",
], ],
theme: { theme: {
extend: {}, extend: {
minWidth: {
40: "10rem",
},
},
}, },
plugins: [], plugins: [],
}; };

View File

@ -141,28 +141,28 @@
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "home"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}" class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "home"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/" href="/"
> >
<span class="text-left">{{ template "svg/home" (dict "Size" 20) }}</span> {{ template "svg/home" (dict "Size" 20) }}
<span class="mx-4 text-sm font-normal">Home</span> <span class="mx-4 text-sm font-normal">Home</span>
</a> </a>
<a <a
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "documents"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}" class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "documents"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/documents" href="/documents"
> >
<span class="text-left">{{ template "svg/documents" (dict "Size" 20) }}</span> {{ template "svg/documents" (dict "Size" 20) }}
<span class="mx-4 text-sm font-normal">Documents</span> <span class="mx-4 text-sm font-normal">Documents</span>
</a> </a>
<a <a
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "progress"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}" class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "progress"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/progress" href="/progress"
> >
<span class="text-left">{{ template "svg/activity" (dict "Size" 20) }}</span> {{ template "svg/activity" (dict "Size" 20) }}
<span class="mx-4 text-sm font-normal">Progress</span> <span class="mx-4 text-sm font-normal">Progress</span>
</a> </a>
<a <a
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "activity"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}" class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "activity"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/activity" href="/activity"
> >
<span class="text-left">{{ template "svg/activity" (dict "Size" 20) }}</span> {{ template "svg/activity" (dict "Size" 20) }}
<span class="mx-4 text-sm font-normal">Activity</span> <span class="mx-4 text-sm font-normal">Activity</span>
</a> </a>
{{ if .Config.SearchEnabled }} {{ if .Config.SearchEnabled }}
@ -170,12 +170,36 @@
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "search"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}" class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "search"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/search" href="/search"
> >
<span class="text-left">{{ template "svg/search" (dict "Size" 20) }}</span> {{ template "svg/search" (dict "Size" 20) }}
<span class="mx-4 text-sm font-normal">Search</span> <span class="mx-4 text-sm font-normal">Search</span>
</a> </a>
{{ end }} {{ end }}
{{ if .Authorization.IsAdmin }}
<div class="flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if hasPrefix .RouteName "admin"}}dark:text-white border-purple-500{{else}}border-transparent text-gray-400{{end}}">
<a href="/admin" class="flex justify-start w-full {{if not (hasPrefix .RouteName "admin")}}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}">
{{ template "svg/settings" (dict "Size" 20) }}
<span class="mx-4 text-sm font-normal">Admin</span>
</a>
{{ if hasPrefix .RouteName "admin" }}
<a href="/admin" style="padding-left: 1.75em;" class="flex justify-start w-full {{if not (eq .RouteName "admin")}}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}">
<span class="mx-4 text-sm font-normal">General</span>
</a>
<a href="/admin/users" style="padding-left: 1.75em;" class="flex justify-start w-full {{if not (eq .RouteName "admin-users")}}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}">
<span class="mx-4 text-sm font-normal">Users</span>
</a>
<a href="/admin/logs" style="padding-left: 1.75em;" class="flex justify-start w-full {{if not (eq .RouteName "admin-logs")}}text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}">
<span class="mx-4 text-sm font-normal">Logs</span>
</a>
{{ end }}
</div>
{{ end }}
</div> </div>
<a class="flex justify-center items-center p-6 w-full absolute bottom-0" target="_blank" href="https://gitea.va.reichard.io/evan/AnthoLume"> <a class="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white" target="_blank" href="https://gitea.va.reichard.io/evan/AnthoLume">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="text-black dark:text-white" class="text-black dark:text-white"
@ -211,6 +235,7 @@
/> />
</g> </g>
</svg> </svg>
<span class="text-xs">{{ .Config.Version }}</span>
</a> </a>
</div> </div>
</div> </div>
@ -231,19 +256,6 @@
aria-orientation="vertical" aria-orientation="vertical"
aria-labelledby="options-menu" aria-labelledby="options-menu"
> >
{{ if .Authorization.IsAdmin }}
<a
href="/admin"
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
role="menuitem"
>
<span class="flex flex-col">
<span>Administration</span>
</span>
</a>
{{ end }}
<a <a
href="/settings" href="/settings"
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600" class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"

View File

@ -0,0 +1,15 @@
{{template "base" .}} {{define "title"}}Admin - Logs{{end}} {{define
"header"}}
<a class="whitespace-pre" href="../admin">Admin - Logs</a>
{{end}} {{define "content"}}
<div
class="flex flex-col-reverse text-black dark:text-white"
style="font-family: monospace"
>
{{range $log := .Data }}
<span class="whitespace-pre">{{ $log }}</span>
{{end}}
</div>
{{end}}

View File

@ -0,0 +1,50 @@
{{template "base" .}} {{define "title"}}Admin - Users{{end}} {{define "header"}}
<a class="whitespace-pre" href="../admin">Admin - Users</a>
{{end}} {{define "content"}}
<div class="overflow-x-auto">
<div class="inline-block min-w-full overflow-hidden rounded shadow">
<table class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
<thead class="text-gray-800 dark:text-gray-400">
<tr>
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-12">
{{ template "svg/add" }}
</th>
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">
User
</th>
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 text-center">
Permissions
</th>
<th class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-48">
Created
</th>
</tr>
</thead>
<tbody class="text-black dark:text-white">
{{ if not .Data }}
<tr>
<td class="text-center p-3" colspan="2">No Results</td>
</tr>
{{ end }}
{{range $user := .Data }}
<tr>
<td class="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400">
{{ template "svg/delete" }}
</td>
<td class="p-3 border-b border-gray-200">
<p>{{ $user.ID }}</p>
</td>
<td class="p-3 border-b border-gray-200 text-center min-w-40">
<span class="px-2 py-1 rounded-md text-white dark:text-black {{if $user.Admin}}bg-gray-800 dark:bg-gray-100{{else}}bg-gray-400 dark:bg-gray-600 cursor-pointer{{end}}">admin</span>
<span class="px-2 py-1 rounded-md text-white dark:text-black {{if $user.Admin}}bg-gray-400 dark:bg-gray-600 cursor-pointer{{else}}bg-gray-800 dark:bg-gray-100{{end}}">user</span>
</td>
<td class="p-3 border-b border-gray-200">
<p>{{ $user.CreatedAt }}</p>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{end}}

View File

@ -1,6 +1,6 @@
{{template "base" .}} {{define "title"}}Administration{{end}} {{define {{template "base" .}} {{define "title"}}Admin - General{{end}} {{define
"header"}} "header"}}
<a href="./admin">Administration</a> <a class="whitespace-pre" href="./admin">Admin - General</a>
{{end}} {{define "content"}} {{end}} {{define "content"}}
<div class="w-full flex flex-col md:flex-row gap-4"> <div class="w-full flex flex-col md:flex-row gap-4">
<div> <div>
@ -168,7 +168,6 @@
<td class="py-2 float-right"> <td class="py-2 float-right">
<a <a
href="./admin/logs" href="./admin/logs"
target="_blank"
class="inline-block w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2" class="inline-block w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
> >
<span class="w-full">View</span> <span class="w-full">View</span>

20
templates/svgs/add.svg Normal file
View File

@ -0,0 +1,20 @@
<svg
width="{{ or .Size 24 }}"
height="{{ or .Size 24 }}"
{{ if .Disabled }}
class="text-gray-200 dark:text-gray-600"
{{ else }}
class="hover:text-gray-800 dark:hover:text-gray-100"
{{ end }}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 8.25C12.4142 8.25 12.75 8.58579 12.75 9V11.25H15C15.4142 11.25 15.75 11.5858 15.75 12C15.75 12.4142 15.4142 12.75 15 12.75H12.75L12.75 15C12.75 15.4142 12.4142 15.75 12 15.75C11.5858 15.75 11.25 15.4142 11.25 15V12.75H9C8.58579 12.75 8.25 12.4142 8.25 12C8.25 11.5858 8.58579 11.25 9 11.25H11.25L11.25 9C11.25 8.58579 11.5858 8.25 12 8.25Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 972 B

View File

@ -0,0 +1,20 @@
<svg
width="{{ or .Size 24 }}"
height="{{ or .Size 24 }}"
{{ if .Disabled }}
class="text-gray-200 dark:text-gray-600"
{{ else }}
class="hover:text-gray-800 dark:hover:text-gray-100"
{{ end }}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.2788 2.15224C13.9085 2 13.439 2 12.5 2C11.561 2 11.0915 2 10.7212 2.15224C10.2274 2.35523 9.83509 2.74458 9.63056 3.23463C9.53719 3.45834 9.50065 3.7185 9.48635 4.09799C9.46534 4.65568 9.17716 5.17189 8.69017 5.45093C8.20318 5.72996 7.60864 5.71954 7.11149 5.45876C6.77318 5.2813 6.52789 5.18262 6.28599 5.15102C5.75609 5.08178 5.22018 5.22429 4.79616 5.5472C4.47814 5.78938 4.24339 6.1929 3.7739 6.99993C3.30441 7.80697 3.06967 8.21048 3.01735 8.60491C2.94758 9.1308 3.09118 9.66266 3.41655 10.0835C3.56506 10.2756 3.77377 10.437 4.0977 10.639C4.57391 10.936 4.88032 11.4419 4.88029 12C4.88026 12.5581 4.57386 13.0639 4.0977 13.3608C3.77372 13.5629 3.56497 13.7244 3.41645 13.9165C3.09108 14.3373 2.94749 14.8691 3.01725 15.395C3.06957 15.7894 3.30432 16.193 3.7738 17C4.24329 17.807 4.47804 18.2106 4.79606 18.4527C5.22008 18.7756 5.75599 18.9181 6.28589 18.8489C6.52778 18.8173 6.77305 18.7186 7.11133 18.5412C7.60852 18.2804 8.2031 18.27 8.69012 18.549C9.17714 18.8281 9.46533 19.3443 9.48635 19.9021C9.50065 20.2815 9.53719 20.5417 9.63056 20.7654C9.83509 21.2554 10.2274 21.6448 10.7212 21.8478C11.0915 22 11.561 22 12.5 22C13.439 22 13.9085 22 14.2788 21.8478C14.7726 21.6448 15.1649 21.2554 15.3694 20.7654C15.4628 20.5417 15.4994 20.2815 15.5137 19.902C15.5347 19.3443 15.8228 18.8281 16.3098 18.549C16.7968 18.2699 17.3914 18.2804 17.8886 18.5412C18.2269 18.7186 18.4721 18.8172 18.714 18.8488C19.2439 18.9181 19.7798 18.7756 20.2038 18.4527C20.5219 18.2105 20.7566 17.807 21.2261 16.9999C21.6956 16.1929 21.9303 15.7894 21.9827 15.395C22.0524 14.8691 21.9088 14.3372 21.5835 13.9164C21.4349 13.7243 21.2262 13.5628 20.9022 13.3608C20.4261 13.0639 20.1197 12.558 20.1197 11.9999C20.1197 11.4418 20.4261 10.9361 20.9022 10.6392C21.2263 10.4371 21.435 10.2757 21.5836 10.0835C21.9089 9.66273 22.0525 9.13087 21.9828 8.60497C21.9304 8.21055 21.6957 7.80703 21.2262 7C20.7567 6.19297 20.522 5.78945 20.2039 5.54727C19.7799 5.22436 19.244 5.08185 18.7141 5.15109C18.4722 5.18269 18.2269 5.28136 17.8887 5.4588C17.3915 5.71959 16.7969 5.73002 16.3099 5.45096C15.8229 5.17191 15.5347 4.65566 15.5136 4.09794C15.4993 3.71848 15.4628 3.45833 15.3694 3.23463C15.1649 2.74458 14.7726 2.35523 14.2788 2.15224ZM12.5 15C14.1695 15 15.5228 13.6569 15.5228 12C15.5228 10.3431 14.1695 9 12.5 9C10.8305 9 9.47716 10.3431 9.47716 12C9.47716 13.6569 10.8305 15 12.5 15Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB