mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-01-31 21:48:32 -06:00
Improved logs UI design, added stats
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/forceu/gokapi/internal/configuration/database/migration"
|
||||
"github.com/forceu/gokapi/internal/helper/systemd"
|
||||
"github.com/forceu/gokapi/internal/logging/serverStats"
|
||||
|
||||
"github.com/forceu/gokapi/internal/configuration"
|
||||
"github.com/forceu/gokapi/internal/configuration/cloudconfig"
|
||||
@@ -69,6 +70,7 @@ func main() {
|
||||
storage.CleanUp(true)
|
||||
logging.LogStartup()
|
||||
showDeprecationWarnings()
|
||||
serverStats.Init()
|
||||
go webserver.Start()
|
||||
|
||||
c := make(chan os.Signal)
|
||||
@@ -81,6 +83,7 @@ func main() {
|
||||
func shutdown() {
|
||||
fmt.Println("Shutting down...")
|
||||
webserver.Shutdown()
|
||||
serverStats.Shutdown()
|
||||
logging.LogShutdown()
|
||||
database.Close()
|
||||
}
|
||||
|
||||
@@ -353,3 +353,15 @@ func SaveFileRequest(request models.FileRequest) {
|
||||
func DeleteFileRequest(request models.FileRequest) {
|
||||
db.DeleteFileRequest(request)
|
||||
}
|
||||
|
||||
// Statistics
|
||||
|
||||
// GetStatTraffic returns the total traffic from statistics
|
||||
func GetStatTraffic() uint64 {
|
||||
return db.GetStatTraffic()
|
||||
}
|
||||
|
||||
// SaveStatTraffic stores the total traffic
|
||||
func SaveStatTraffic(totalTraffic uint64) {
|
||||
db.SaveStatTraffic(totalTraffic)
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@ type Database interface {
|
||||
SaveFileRequest(request models.FileRequest)
|
||||
// DeleteFileRequest deletes a file request with the given ID
|
||||
DeleteFileRequest(request models.FileRequest)
|
||||
|
||||
// GetStatTraffic returns the total traffic from statistics
|
||||
GetStatTraffic() uint64
|
||||
// SaveStatTraffic stores the total traffic
|
||||
SaveStatTraffic(totalTraffic uint64)
|
||||
}
|
||||
|
||||
// GetNew connects to the given database and initialises it
|
||||
|
||||
@@ -257,6 +257,16 @@ func (p DatabaseProvider) getKeyInt(id string) (int, bool) {
|
||||
helper.Check(err2)
|
||||
return resultInt, true
|
||||
}
|
||||
|
||||
func (p DatabaseProvider) getKeyUInt64(id string) (uint64, bool) {
|
||||
result, err := p.getKeyRaw(id)
|
||||
if result == nil {
|
||||
return 0, false
|
||||
}
|
||||
resultInt, err2 := redigo.Uint64(result, err)
|
||||
helper.Check(err2)
|
||||
return resultInt, true
|
||||
}
|
||||
func (p DatabaseProvider) getKeyBytes(id string) ([]byte, bool) {
|
||||
result, err := p.getKeyRaw(id)
|
||||
if result == nil {
|
||||
|
||||
16
internal/configuration/database/provider/redis/statistics.go
Normal file
16
internal/configuration/database/provider/redis/statistics.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package redis
|
||||
|
||||
const (
|
||||
prefixStatisticsTraffic = "stats:traffic"
|
||||
)
|
||||
|
||||
// GetStatTraffic returns the total traffic from statistics
|
||||
func (p DatabaseProvider) GetStatTraffic() uint64 {
|
||||
result, _ := p.getKeyUInt64(prefixStatisticsTraffic)
|
||||
return result
|
||||
}
|
||||
|
||||
// SaveStatTraffic stores the total traffic
|
||||
func (p DatabaseProvider) SaveStatTraffic(totalTraffic uint64) {
|
||||
p.setKey(prefixStatisticsTraffic, totalTraffic)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ type DatabaseProvider struct {
|
||||
}
|
||||
|
||||
// DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed
|
||||
const DatabaseSchemeVersion = 12
|
||||
const DatabaseSchemeVersion = 13
|
||||
|
||||
// New returns an instance
|
||||
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
|
||||
@@ -46,23 +46,23 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
|
||||
err := p.rawSqlite("ALTER TABLE FileMetaData DROP COLUMN ExpireAtString;")
|
||||
helper.Check(err)
|
||||
}
|
||||
// < v2.2.0
|
||||
// < v2.2.0-rc1
|
||||
if currentDbVersion < 12 {
|
||||
|
||||
err := p.rawSqlite(`ALTER TABLE FileMetaData ADD COLUMN "UploadRequestId" TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE ApiKeys ADD COLUMN "UploadRequestId" TEXT NOT NULL DEFAULT '';
|
||||
CREATE TABLE "UploadRequests" (
|
||||
"id" TEXT NOT NULL UNIQUE,
|
||||
"name" TEXT NOT NULL,
|
||||
"userid" INTEGER NOT NULL,
|
||||
"expiry" INTEGER NOT NULL,
|
||||
"maxFiles" INTEGER NOT NULL,
|
||||
"maxSize" INTEGER NOT NULL,
|
||||
"creation" INTEGER NOT NULL,
|
||||
"apiKey" TEXT NOT NULL UNIQUE,
|
||||
"note" TEXT NOT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);`)
|
||||
ALTER TABLE ApiKeys ADD COLUMN "UploadRequestId" TEXT NOT NULL DEFAULT '';
|
||||
CREATE TABLE "UploadRequests" (
|
||||
"id" TEXT NOT NULL UNIQUE,
|
||||
"name" TEXT NOT NULL,
|
||||
"userid" INTEGER NOT NULL,
|
||||
"expiry" INTEGER NOT NULL,
|
||||
"maxFiles" INTEGER NOT NULL,
|
||||
"maxSize" INTEGER NOT NULL,
|
||||
"creation" INTEGER NOT NULL,
|
||||
"apiKey" TEXT NOT NULL UNIQUE,
|
||||
"note" TEXT NOT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);`)
|
||||
helper.Check(err)
|
||||
grantUploadPerm := environment.New().PermRequestGrantedByDefault
|
||||
for _, user := range p.GetAllUsers() {
|
||||
@@ -77,6 +77,16 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// < v2.2.0-rc2
|
||||
if currentDbVersion < 13 {
|
||||
err := p.rawSqlite(`CREATE TABLE "Statistics" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"type" INTEGER NOT NULL UNIQUE,
|
||||
"value" INTEGER,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);`)
|
||||
helper.Check(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetDbVersion gets the version number of the database
|
||||
@@ -223,7 +233,13 @@ func (p DatabaseProvider) createNewDatabase() error {
|
||||
"apiKey" TEXT NOT NULL UNIQUE,
|
||||
"note" TEXT NOT NULL,
|
||||
PRIMARY KEY("id")
|
||||
);`
|
||||
);
|
||||
CREATE TABLE "Statistics" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"type" INTEGER NOT NULL UNIQUE,
|
||||
"value" INTEGER,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);`
|
||||
err := p.rawSqlite(sqlStmt)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,9 +3,10 @@ package sqlite
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/forceu/gokapi/internal/helper"
|
||||
"github.com/forceu/gokapi/internal/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type schemaSessions struct {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/forceu/gokapi/internal/helper"
|
||||
)
|
||||
|
||||
const statIdTraffic = "1"
|
||||
|
||||
// GetStatTraffic returns the total traffic from statistics
|
||||
func (p DatabaseProvider) GetStatTraffic() uint64 {
|
||||
var result uint64
|
||||
row := p.sqliteDb.QueryRow("SELECT value FROM Statistics WHERE type = ?", statIdTraffic)
|
||||
err := row.Scan(&result)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0
|
||||
}
|
||||
helper.Check(err)
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SaveStatTraffic stores the total traffic
|
||||
func (p DatabaseProvider) SaveStatTraffic(totalTraffic uint64) {
|
||||
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO Statistics (type, value) VALUES (?, ?)", statIdTraffic, totalTraffic)
|
||||
helper.Check(err)
|
||||
}
|
||||
60
internal/logging/serverStats/ServerStats.go
Normal file
60
internal/logging/serverStats/ServerStats.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package serverStats
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/forceu/gokapi/internal/configuration/database"
|
||||
"github.com/forceu/gokapi/internal/models"
|
||||
)
|
||||
|
||||
const trafficSaveInterval = 5 * time.Minute
|
||||
|
||||
var startTime time.Time
|
||||
var currentTraffic trafficInfo
|
||||
|
||||
type trafficInfo struct {
|
||||
Total uint64
|
||||
Mutex sync.RWMutex
|
||||
LastUpdate time.Time
|
||||
}
|
||||
|
||||
func Init() {
|
||||
startTime = time.Now()
|
||||
}
|
||||
|
||||
func Shutdown() {
|
||||
saveTraffic()
|
||||
}
|
||||
|
||||
func saveTraffic() {
|
||||
currentTraffic.Mutex.RLock()
|
||||
database.SaveStatTraffic(currentTraffic.Total)
|
||||
currentTraffic.Mutex.RUnlock()
|
||||
}
|
||||
|
||||
func GetUptime() int64 {
|
||||
return time.Since(startTime).Milliseconds() / 1000
|
||||
}
|
||||
|
||||
func GetTotalFiles() int {
|
||||
return len(database.GetAllMetadata())
|
||||
}
|
||||
|
||||
func GetCurrentTraffic() uint64 {
|
||||
currentTraffic.Mutex.RLock()
|
||||
defer currentTraffic.Mutex.RUnlock()
|
||||
return currentTraffic.Total
|
||||
}
|
||||
|
||||
func AddTraffic(file models.File) {
|
||||
currentTraffic.Mutex.Lock()
|
||||
currentTraffic.Total = currentTraffic.Total + uint64(file.SizeBytes)
|
||||
requireSave := time.Since(currentTraffic.LastUpdate) > trafficSaveInterval
|
||||
currentTraffic.LastUpdate = time.Now()
|
||||
currentTraffic.Mutex.Unlock()
|
||||
|
||||
if requireSave {
|
||||
saveTraffic()
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/forceu/gokapi/internal/encryption"
|
||||
"github.com/forceu/gokapi/internal/helper"
|
||||
"github.com/forceu/gokapi/internal/logging"
|
||||
"github.com/forceu/gokapi/internal/logging/serverStats"
|
||||
"github.com/forceu/gokapi/internal/models"
|
||||
"github.com/forceu/gokapi/internal/storage"
|
||||
"github.com/forceu/gokapi/internal/storage/filerequest"
|
||||
@@ -739,8 +740,17 @@ type AdminView struct {
|
||||
MinLengthPassword int
|
||||
FileRequestMaxFiles int
|
||||
FileRequestMaxSize int
|
||||
CpuLoad int
|
||||
MemoryUsage int
|
||||
MemoryTotal int
|
||||
DiskUsage int
|
||||
TotalFiles int
|
||||
DataServed int64
|
||||
Uptime int64
|
||||
TimeNow int64
|
||||
CustomContent customStatic
|
||||
TotalTraffic uint64
|
||||
|
||||
CustomContent customStatic
|
||||
}
|
||||
|
||||
// getUserMap needs to return the map with pointers; otherwise template cannot call
|
||||
@@ -807,6 +817,9 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
|
||||
apiKeyList = sortApiKeys(apiKeyList)
|
||||
case ViewLogs:
|
||||
u.Logs, _ = logging.GetAll()
|
||||
u.TotalFiles = serverStats.GetTotalFiles()
|
||||
u.Uptime = serverStats.GetUptime()
|
||||
u.TotalTraffic = serverStats.GetCurrentTraffic()
|
||||
case ViewUsers:
|
||||
uploadCounts := storage.GetUploadCounts()
|
||||
u.Users = make([]userInfo, 0)
|
||||
|
||||
@@ -57,6 +57,11 @@ a:hover {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
|
||||
.text-muted {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
background: rgb(47, 52, 58) !important;
|
||||
color: #ffffff;
|
||||
@@ -647,3 +652,4 @@ tr.no-bottom-border td {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,8 +11,13 @@ function filterLogs(tag) {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
|
||||
function deleteLogs(cutoff) {
|
||||
if (cutoff == "none") {
|
||||
function deleteLogs() {
|
||||
const delSelector = document.getElementById("deleteLogsSel");
|
||||
if (!delSelector) {
|
||||
return;
|
||||
}
|
||||
const cutoff = delSelector.value;
|
||||
if (cutoff == "none" || cutoff == "") {
|
||||
return;
|
||||
}
|
||||
if (!confirm("Do you want to delete the selected logs?")) {
|
||||
@@ -36,6 +41,7 @@ function deleteLogs(cutoff) {
|
||||
case "30":
|
||||
timestamp = timestamp - 30 * 24 * 60 * 60;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
apiLogsDelete(timestamp)
|
||||
.then(data => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,54 +1,181 @@
|
||||
{{ define "logs" }}{{ template "header" . }}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div id="container" class="card" style="width: 80%">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Log File</h3>
|
||||
<br>
|
||||
|
||||
<textarea class="form-control" id="logviewer" rows="20" readonly>
|
||||
{{ .Logs }}</textarea>
|
||||
|
||||
<br>
|
||||
<div class="d-flex gap-3">
|
||||
<select id="logFilter" class="form-select" onchange="filterLogs(this.value)">
|
||||
<option value="all">Show all logs</option>
|
||||
<option value="warning">Show [warning]</option>
|
||||
<option value="auth">Show [auth]</option>
|
||||
<option value="download">Show [download]</option>
|
||||
<option value="upload">Show [upload]</option>
|
||||
<option value="edit">Show [edit]</option>
|
||||
<option value="info">Show [info]</option>
|
||||
</select>
|
||||
|
||||
<style>
|
||||
.stat-card {
|
||||
background: #1a1d20;
|
||||
border: 1px solid #2d3238;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.3) !important;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
.stat-icon {
|
||||
opacity: 0.6;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.progress-stat {
|
||||
height: 4px;
|
||||
background-color: #2b3035;
|
||||
}
|
||||
|
||||
.log-dark-input {
|
||||
background-color: #2b3035 !important;
|
||||
border-color: #444b52 !important;
|
||||
color: #e9ecef !important;
|
||||
}
|
||||
.log-dark-input:focus {
|
||||
background-color: #32383e !important;
|
||||
border-color: #0d6efd !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
.input-group-text-dark {
|
||||
background-color: #1a1d20 !important;
|
||||
border-color: #444b52 !important;
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
#logviewer::-webkit-scrollbar { width: 8px; }
|
||||
#logviewer::-webkit-scrollbar-track { background: #000; }
|
||||
#logviewer::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
||||
</style>
|
||||
|
||||
<div class="container-fluid py-4" style="max-width: 1200px;">
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg-2">
|
||||
<div class="card stat-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="text-muted small text-uppercase fw-bold mb-1">Uptime</div>
|
||||
<div class="h5 mb-0 text-light">24d 12h</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-2">
|
||||
<div class="card stat-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="text-muted small text-uppercase fw-bold mb-1">CPU Load</div>
|
||||
<div class="h5 mb-2 text-light">12%</div>
|
||||
<div class="progress progress-stat">
|
||||
<div class="progress-bar bg-info" style="width: 12%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-2">
|
||||
<div class="card stat-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="text-muted small text-uppercase fw-bold mb-1">Memory</div>
|
||||
<div class="h5 mb-2 text-light text-nowrap">142 / 512<span class="small opacity-50">MB</span></div>
|
||||
<div class="progress progress-stat">
|
||||
<div class="progress-bar bg-success" style="width: 27%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-2">
|
||||
<div class="card stat-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="text-muted small text-uppercase fw-bold mb-1">Disk Space</div>
|
||||
<div class="h5 mb-2 text-light">68%</div>
|
||||
<div class="progress progress-stat">
|
||||
<div class="progress-bar bg-warning" style="width: 68%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-2">
|
||||
<div class="card stat-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="text-muted small text-uppercase fw-bold mb-1">Data Served</div>
|
||||
<div class="h5 mb-0 text-light">1.24 <span class="small opacity-50">TB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-lg-2">
|
||||
<div class="card stat-card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3">
|
||||
<div class="text-muted small text-uppercase fw-bold mb-1">Active Files</div>
|
||||
<div class="h5 mb-0 text-light">{{ .TotalFiles }} <span class="small opacity-50">Total</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-lg bg-dark overflow-hidden">
|
||||
<div class="card-header bg-dark border-bottom border-secondary py-3 d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0 fw-bold text-light me-3">System Logs</h6>
|
||||
</div>
|
||||
<div class="text-muted small font-monospace">gokapi_v1.x.x</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0 bg-black">
|
||||
<textarea
|
||||
class="form-control bg-black text-info font-monospace border-0 p-4"
|
||||
id="logviewer"
|
||||
rows="20"
|
||||
readonly
|
||||
style="resize: none; font-size: 0.85rem; line-height: 1.5; outline: none; border-radius: 0;"
|
||||
>{{ .Logs }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-dark border-top border-secondary p-3">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text input-group-text-dark">Filter</span>
|
||||
<select id="logFilter" class="form-select log-dark-input" onchange="filterLogs(this.value)">
|
||||
<option value="all">All Events</option>
|
||||
<option value="warning">Warnings</option>
|
||||
<option value="auth">Security</option>
|
||||
<option value="download">Downloads</option>
|
||||
<option value="upload">Uploads</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2"></div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex gap-2 justify-content-md-end">
|
||||
{{ if or (eq .ActiveUser.UserLevel 0) (eq .ActiveUser.UserLevel 1) }}
|
||||
<select id="deleteLogs" class="form-select" onchange="deleteLogs(this.value)">
|
||||
<option value="none">Delete Logs...</option>
|
||||
<option value="2">Older than 2 days</option>
|
||||
<option value="7">Older than 7 days</option>
|
||||
<option value="14">Older than 14 days</option>
|
||||
<option value="30">Older than 30 days</option>
|
||||
<option value="all">Delete all logs</option>
|
||||
</select>
|
||||
<select id="deleteLogsSel" class="form-select form-select-sm log-dark-input w-auto">
|
||||
<option value="none">Delete Logs...</option>
|
||||
<option value="30">Older than 30 days</option>
|
||||
<option value="14">Older than 14 days</option>
|
||||
<option value="7">Older than 7 days</option>
|
||||
<option value="2">Older than 2 days</option>
|
||||
<option value="all">Delete all logs</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-light px-3" onclick="deleteLogs()">Execute</button>
|
||||
{{ else }}
|
||||
<select id="deleteLogs" disabled class="form-select disabled" onchange="">
|
||||
<select id="deleteLogsSel" disabled class="form-select form-select-sm log-dark-input w-auto disabled">
|
||||
<option value="none">Only administrators can delete logs</option>
|
||||
</select>
|
||||
{{ end }}
|
||||
<button class="btn btn-sm btn-outline-light px-3" disabled onclick="">Execute</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./js/min/admin.min.{{ template "js_admin_version"}}.js"></script>
|
||||
<script>
|
||||
let textarea = document.getElementById('logviewer');
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
var logContent = textarea.value;
|
||||
</script>
|
||||
<script>
|
||||
const textarea = document.getElementById('logviewer');
|
||||
var logContent = textarea.value;
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
</script>
|
||||
|
||||
{{ template "pagename" "LogOverview"}}
|
||||
{{ template "customjs" .}}
|
||||
{{ template "footer" true}}
|
||||
|
||||
Reference in New Issue
Block a user