chore: migrate admin general

This commit is contained in:
Evan Reichard 2025-09-25 15:49:51 -04:00
parent 10bbd908e6
commit 9a7e83ae5f
7 changed files with 230 additions and 54 deletions

6
.golangci.toml Normal file
View File

@ -0,0 +1,6 @@
#:schema https://golangci-lint.run/jsonschema/golangci.jsonschema.json
version = "2"
[[linters.exclusions.rules]]
linters = [ "errcheck" ]
source = "^\\s*defer\\s+"

View File

@ -162,13 +162,17 @@ func (api *API) registerWebAppRoutes(router *gin.Engine) {
// TODO // TODO
router.GET("/login", api.appGetLogin) router.GET("/login", api.appGetLogin)
router.GET("/register", api.appGetRegister) router.GET("/register", api.appGetRegister)
// DONE
router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
// TODO - WIP
router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs) router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport) router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport)
router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport) router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport)
router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers) router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers)
router.POST("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appUpdateAdminUsers) router.POST("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appUpdateAdminUsers)
router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
// Demo mode enabled configuration // Demo mode enabled configuration
if api.cfg.DemoMode { if api.cfg.DemoMode {

View File

@ -27,6 +27,8 @@ import (
"reichard.io/antholume/database" "reichard.io/antholume/database"
"reichard.io/antholume/metadata" "reichard.io/antholume/metadata"
"reichard.io/antholume/utils" "reichard.io/antholume/utils"
"reichard.io/antholume/web/models"
"reichard.io/antholume/web/pages"
) )
type adminAction string type adminAction string
@ -96,21 +98,31 @@ type importResult struct {
Error error Error error
} }
func (api *API) appPerformAdminAction(c *gin.Context) { func (api *API) appGetAdmin(c *gin.Context) {
templateVars, _ := api.getBaseTemplateVars("admin", c) api.renderPage(c, &pages.AdminGeneral{})
}
func (api *API) appPerformAdminAction(c *gin.Context) {
var rAdminAction requestAdminAction var rAdminAction requestAdminAction
if err := c.ShouldBind(&rAdminAction); err != nil { if err := c.ShouldBind(&rAdminAction); err != nil {
log.Error("Invalid Form Bind: ", err) log.Error("invalid or missing form values")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values") appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return return
} }
var allNotifications []*models.Notification
switch rAdminAction.Action { switch rAdminAction.Action {
case adminRestore:
api.processRestoreFile(rAdminAction, c)
return
case adminBackup:
api.processBackup(c, rAdminAction.BackupTypes)
return
case adminMetadataMatch: case adminMetadataMatch:
// TODO allNotifications = append(allNotifications, &models.Notification{
// 1. Documents xref most recent metadata table? Type: models.NotificationTypeError,
// 2. Select all / deselect? Content: "Metadata match not implemented",
})
case adminCacheTables: case adminCacheTables:
go func() { go func() {
err := api.db.CacheTempTables(c) err := api.db.CacheTempTables(c)
@ -118,50 +130,14 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
log.Error("Unable to cache temp tables: ", err) log.Error("Unable to cache temp tables: ", err)
} }
}() }()
case adminRestore:
api.processRestoreFile(rAdminAction, c)
return
case adminBackup:
// Vacuum
_, err := api.db.DB.ExecContext(c, "VACUUM;")
if err != nil {
log.Error("Unable to vacuum DB: ", err)
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
return
}
// Set Headers allNotifications = append(allNotifications, &models.Notification{
c.Header("Content-type", "application/octet-stream") Type: models.NotificationTypeSuccess,
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeBackup_%s.zip\"", time.Now().Format("20060102150405"))) Content: "Initiated table cache",
// Stream Backup ZIP Archive
c.Stream(func(w io.Writer) bool {
var directories []string
for _, item := range rAdminAction.BackupTypes {
switch item {
case backupCovers:
directories = append(directories, "covers")
case backupDocuments:
directories = append(directories, "documents")
}
}
err := api.createBackup(c, w, directories)
if err != nil {
log.Error("Backup Error: ", err)
}
return false
}) })
return
} }
c.HTML(http.StatusOK, "page/admin", templateVars) api.renderPage(c, &pages.AdminGeneral{}, allNotifications...)
}
func (api *API) appGetAdmin(c *gin.Context) {
templateVars, _ := api.getBaseTemplateVars("admin", c)
c.HTML(http.StatusOK, "page/admin", templateVars)
} }
func (api *API) appGetAdminLogs(c *gin.Context) { func (api *API) appGetAdminLogs(c *gin.Context) {
@ -534,6 +510,40 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
c.HTML(http.StatusOK, "page/admin-import-results", templateVars) c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
} }
func (api *API) processBackup(c *gin.Context, backupTypes []backupType) {
// Vacuum
_, err := api.db.DB.ExecContext(c, "VACUUM;")
if err != nil {
log.Error("Unable to vacuum DB: ", err)
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
return
}
// Set Headers
c.Header("Content-type", "application/octet-stream")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeBackup_%s.zip\"", time.Now().Format("20060102150405")))
// Stream Backup ZIP Archive
c.Stream(func(w io.Writer) bool {
var directories []string
for _, item := range backupTypes {
switch item {
case backupCovers:
directories = append(directories, "covers")
case backupDocuments:
directories = append(directories, "documents")
}
}
err := api.createBackup(c, w, directories)
if err != nil {
log.Error("Backup Error: ", err)
}
return false
})
}
func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) { func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
// Validate Type & Derive Extension on MIME // Validate Type & Derive Extension on MIME
uploadedFile, err := rAdminAction.RestoreFile.Open() uploadedFile, err := rAdminAction.RestoreFile.Open()
@ -790,7 +800,7 @@ func (api *API) createBackup(ctx context.Context, w io.Writer, directories []str
} }
} }
ar.Close() _ = ar.Close()
return nil return nil
} }

File diff suppressed because one or more lines are too long

View File

@ -12,14 +12,17 @@ func Notifications(notifications []*models.Notification) g.Node {
return nil return nil
} }
return h.Div( return h.Div(
h.Class("fixed flex flex-col gap-2 bottom-0 right-0 p-2 sm:p-4 text-white dark:text-black"), h.Class("fixed flex flex-col gap-2 bottom-0 right-0 text-white dark:text-black"),
g.Group(sliceutils.Map(notifications, notificationNode)), g.Group(sliceutils.Map(notifications, notificationNode)),
) )
} }
func notificationNode(n *models.Notification) g.Node { func notificationNode(n *models.Notification) g.Node {
return h.Div( return h.Div(
h.Class("bg-gray-600 dark:bg-gray-400 px-4 py-2 rounded-lg shadow-lg w-64 animate-notification"), h.Class("p-2 sm:p-4 animate-notification"),
h.P(g.Text(n.Content)), h.Div(
h.Class("bg-gray-600 dark:bg-gray-400 px-4 py-2 rounded-lg shadow-lg w-64"),
g.Text(n.Content),
),
) )
} }

153
web/pages/admin.go Normal file
View File

@ -0,0 +1,153 @@
package pages
import (
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
"reichard.io/antholume/web/components/ui"
"reichard.io/antholume/web/models"
"reichard.io/antholume/web/pages/layout"
)
var _ Page = (*AdminGeneral)(nil)
type AdminGeneral struct{}
func (p *AdminGeneral) Generate(ctx models.PageContext) (g.Node, error) {
return layout.Layout(
ctx.WithRoute(models.AdminGeneralPage),
h.Div(
h.Class("w-full flex flex-col gap-4 grow"),
backupAndRestoreSection(),
tasksSection(),
),
)
}
func backupAndRestoreSection() g.Node {
return h.Div(
h.Class("flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"),
h.P(
h.Class("text-lg font-semibold mb-2"),
g.Text("Backup & Restore"),
),
h.Div(
h.Class("flex flex-col gap-4"),
backupForm(),
restoreForm(),
),
)
}
func backupForm() g.Node {
return h.Form(
h.Class("flex justify-between"),
h.Action("./admin"),
h.Method("POST"),
h.Input(
h.Type("text"),
h.Name("action"),
h.Value("BACKUP"),
h.Class("hidden"),
),
h.Div(
h.Class("flex gap-8"),
h.Div(
h.Class("flex gap-2 items-center"),
h.Input(
h.Type("checkbox"),
h.ID("backup_covers"),
h.Name("backup_types"),
h.Value("COVERS"),
),
h.Label(
h.For("backup_covers"),
g.Text("Covers"),
),
),
h.Div(
h.Class("flex gap-2 items-center"),
h.Input(
h.Type("checkbox"),
h.ID("backup_documents"),
h.Name("backup_types"),
h.Value("DOCUMENTS"),
),
h.Label(
h.For("backup_documents"),
g.Text("Documents"),
),
),
),
h.Div(
h.Class("h-10 w-40"),
ui.FormButton(g.Text("Backup"), "", ui.ButtonConfig{Variant: ui.ButtonVariantSecondary}),
),
)
}
func restoreForm() g.Node {
return h.Form(
h.Class("flex justify-between"),
h.Action("./admin"),
h.Method("POST"),
g.Attr("enctype", "multipart/form-data"),
h.Input(
h.Type("text"),
h.Name("action"),
h.Value("RESTORE"),
h.Class("hidden"),
),
h.Div(
h.Class("flex items-center"),
h.Input(
h.Type("file"),
h.Accept(".zip"),
h.Name("restore_file"),
h.Class("w-full"),
),
),
h.Div(
h.Class("h-10 w-40"),
ui.FormButton(g.Text("Restore"), "", ui.ButtonConfig{Variant: ui.ButtonVariantSecondary}),
),
)
}
func tasksSection() g.Node {
return h.Div(
h.Class("flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"),
h.P(
h.Class("text-lg font-semibold mb-4"),
g.Text("Tasks"),
),
h.Div(
h.Class("grid grid-cols-[1fr_auto] gap-x-4 gap-y-3 items-center"),
g.Group(taskItem("Metadata Matching", "METADATA_MATCH")),
g.Group(taskItem("Cache Tables", "CACHE_TABLES")),
),
)
}
func taskItem(name, action string) []g.Node {
return []g.Node{
h.P(
h.Class("text-black dark:text-white"),
g.Text(name),
),
h.Form(
h.Action("./admin"),
h.Method("POST"),
h.Input(
h.Type("text"),
h.Name("action"),
h.Value(action),
h.Class("hidden"),
),
h.Div(
h.Class("h-10 w-40"),
ui.FormButton(g.Text("Run"), "", ui.ButtonConfig{Variant: ui.ButtonVariantSecondary}),
),
),
}
}

View File

@ -18,7 +18,7 @@ func Navigation(ctx models.PageContext) g.Node {
return h.Div( return h.Div(
g.Attr("class", "flex items-center justify-between w-full h-16"), g.Attr("class", "flex items-center justify-between w-full h-16"),
Sidebar(ctx), Sidebar(ctx),
h.H1(g.Attr("class", "text-xl font-bold px-6 lg:ml-44"), g.Text(ctx.Route.Title())), h.H1(g.Attr("class", "text-xl font-bold whitespace-nowrap px-6 lg:ml-44"), g.Text(ctx.Route.Title())),
Dropdown(ctx.UserInfo.Username), Dropdown(ctx.UserInfo.Username),
) )
} }