mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Add event log in Library > Errors
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
@@ -292,6 +292,14 @@
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/library/errors" @click="" class="nav-errors">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate key="Errors">Errors</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</v-list-group>
|
||||
|
||||
<template v-if="!config.disableSettings">
|
||||
|
||||
107
frontend/src/pages/library/errors.vue
Normal file
107
frontend/src/pages/library/errors.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="p-page p-page-errors" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
|
||||
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||
<v-toolbar flat color="secondary">
|
||||
<v-toolbar-title>
|
||||
<translate>Event Log</translate>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon @click.stop="reload" class="action-reload">
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list two-line>
|
||||
<v-list-tile
|
||||
v-for="(err, index) in errors" :key="index"
|
||||
avatar
|
||||
@click=""
|
||||
>
|
||||
<v-list-tile-avatar>
|
||||
<v-icon>{{ err.Level }}</v-icon>
|
||||
</v-list-tile-avatar>
|
||||
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>{{ err.Message }}</v-list-tile-title>
|
||||
<v-list-tile-sub-title>{{ formatTime(err.Time) }}</v-list-tile-sub-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {DateTime} from "luxon";
|
||||
import Api from "common/api";
|
||||
|
||||
export default {
|
||||
name: 'p-page-errors',
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
dirty: false,
|
||||
results: [],
|
||||
scrollDisabled: false,
|
||||
pageSize: 100,
|
||||
offset: 0,
|
||||
page: 0,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.page = 0;
|
||||
this.offset = 0;
|
||||
this.scrollDisabled = false;
|
||||
this.loadMore();
|
||||
},
|
||||
loadMore() {
|
||||
if (this.loading || this.scrollDisabled) return;
|
||||
|
||||
this.loading = true;
|
||||
this.scrollDisabled = true;
|
||||
|
||||
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
|
||||
const offset = this.dirty ? 0 : this.offset;
|
||||
|
||||
const params = {
|
||||
count: count,
|
||||
offset: offset,
|
||||
};
|
||||
|
||||
Api.get("errors", {params}).then((resp) => {
|
||||
if (!resp.data) {
|
||||
resp.data = [];
|
||||
}
|
||||
|
||||
if (offset === 0) {
|
||||
this.errors = resp.data;
|
||||
} else {
|
||||
this.errors = this.errors.concat(resp.data);
|
||||
}
|
||||
|
||||
this.scrollDisabled = (resp.data.length < count);
|
||||
|
||||
if (!this.scrollDisabled) {
|
||||
this.offset = offset + count;
|
||||
this.page++;
|
||||
}
|
||||
}).finally(() => this.loading = false)
|
||||
},
|
||||
level(s) {
|
||||
return s.substr(0, 4).toUpperCase();
|
||||
},
|
||||
formatTime(s) {
|
||||
return DateTime.fromISO(s).toFormat("yyyy-LL-dd HH:mm:ss");
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadMore();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -33,6 +33,7 @@ import Albums from "pages/albums.vue";
|
||||
import AlbumPhotos from "pages/album/photos.vue";
|
||||
import Places from "pages/places.vue";
|
||||
import Files from "pages/library/files.vue";
|
||||
import Errors from "pages/library/errors.vue";
|
||||
import Labels from "pages/labels.vue";
|
||||
import People from "pages/people.vue";
|
||||
import Library from "pages/library.vue";
|
||||
@@ -200,6 +201,12 @@ export default [
|
||||
meta: {title: "Hidden Files", auth: true},
|
||||
props: {staticFilter: {hidden: true}},
|
||||
},
|
||||
{
|
||||
name: "errors",
|
||||
path: "/library/errors",
|
||||
component: Errors,
|
||||
meta: {title: c.name, auth: true},
|
||||
},
|
||||
{
|
||||
name: "labels",
|
||||
path: "/labels",
|
||||
|
||||
@@ -2,9 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -28,3 +31,28 @@ var (
|
||||
ErrNotFound = gin.H{"code": http.StatusNotFound, "error": "Not found"}
|
||||
ErrInvalidPassword = gin.H{"code": http.StatusBadRequest, "error": "Invalid password, please try again"}
|
||||
)
|
||||
|
||||
func GetErrors(router *gin.RouterGroup) {
|
||||
router.GET("/errors", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceLogs, acl.ActionSearch)
|
||||
|
||||
if s.Invalid() {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
limit := txt.Int(c.Query("count"))
|
||||
offset := txt.Int(c.Query("offset"))
|
||||
|
||||
if resp, err := query.Errors(limit, offset); err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
} else {
|
||||
c.Header("X-Count", strconv.Itoa(len(resp)))
|
||||
c.Header("X-Limit", strconv.Itoa(limit))
|
||||
c.Header("X-Offset", strconv.Itoa(offset))
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
|
||||
// Error represents an error message log.
|
||||
type Error struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
ErrorTime time.Time `sql:"index"`
|
||||
ErrorLevel string `gorm:"type:varbinary(32)"`
|
||||
ErrorMessage string `gorm:"type:varbinary(2048)"`
|
||||
ID uint `gorm:"primary_key" json:"ID" yaml:"ID"`
|
||||
ErrorTime time.Time `sql:"index" json:"Time" yaml:"Time"`
|
||||
ErrorLevel string `gorm:"type:varbinary(32)" json:"Level" yaml:"Level"`
|
||||
ErrorMessage string `gorm:"type:varbinary(2048)" json:"Message" yaml:"Message"`
|
||||
}
|
||||
|
||||
type Errors []Error
|
||||
|
||||
// SaveErrorMessages subscribes to error logs and stored them in the errors table.
|
||||
func SaveErrorMessages() {
|
||||
s := event.Subscribe("log.*")
|
||||
|
||||
14
internal/query/errors.go
Normal file
14
internal/query/errors.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
// Errors returns the error log.
|
||||
func Errors(limit, offset int) (results entity.Errors, err error) {
|
||||
stmt := Db()
|
||||
|
||||
err = stmt.Order("error_time DESC").Limit(limit).Offset(offset).Find(&results).Error
|
||||
|
||||
return results, err
|
||||
}
|
||||
@@ -111,6 +111,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||
api.GetSettings(v1)
|
||||
api.SaveSettings(v1)
|
||||
api.ChangePassword(v1)
|
||||
api.GetErrors(v1)
|
||||
|
||||
api.GetSvg(v1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user