mirror of
https://github.com/simon987/sist2.git
synced 2025-04-10 14:06:45 +00:00
Add log cleanup features
This commit is contained in:
parent
f0fd708082
commit
5522bcfa9b
@ -112,6 +112,16 @@ class Sist2AdminApi {
|
||||
getSist2AdminInfo() {
|
||||
return axios.get(`${this.baseUrl}/api/`);
|
||||
}
|
||||
|
||||
getLogsToDelete(jobName, n) {
|
||||
return axios.get(`${this.baseUrl}/api/job/${jobName}/logs_to_delete`, {
|
||||
params: {n: n}
|
||||
});
|
||||
}
|
||||
|
||||
deleteTaskLogs(taskId) {
|
||||
return axios.post(`${this.baseUrl}/api/task/${taskId}/delete_logs`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Sist2AdminApi()
|
@ -1,57 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)">
|
||||
{{ $t("jobOptions.desktopNotifications") }}
|
||||
</b-form-checkbox>
|
||||
<div>
|
||||
<b-form-checkbox :checked="desktopNotificationsEnabled" @change="updateNotifications($event)">
|
||||
{{ $t("jobOptions.desktopNotifications") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox v-model="job.schedule_enabled" @change="update()">
|
||||
{{ $t("jobOptions.scheduleEnabled") }}
|
||||
</b-form-checkbox>
|
||||
<b-form-checkbox v-model="job.schedule_enabled" @change="update()">
|
||||
{{ $t("jobOptions.scheduleEnabled") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<label>{{ $t("jobOptions.cron") }}</label>
|
||||
<b-form-input class="text-monospace" :state="cronValid" v-model="job.cron_expression" :disabled="!job.schedule_enabled" @change="update()"></b-form-input>
|
||||
</div>
|
||||
<label>{{ $t("jobOptions.cron") }}</label>
|
||||
<b-form-input class="text-monospace" :state="cronValid" v-model="job.cron_expression"
|
||||
:disabled="!job.schedule_enabled" @change="update()"></b-form-input>
|
||||
|
||||
<label>{{ $t("jobOptions.keepNLogs") }}</label>
|
||||
<b-input-group>
|
||||
<b-form-input type="number" v-model="job.keep_last_n_logs" @change="update()"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="danger" @click="onDeleteNowClick()">{{ $t("jobOptions.deleteNow") }}</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Sist2AdminApi from "@/Sist2AdminApi";
|
||||
|
||||
export default {
|
||||
name: "JobOptions",
|
||||
props: ["job"],
|
||||
data() {
|
||||
return {
|
||||
cronValid: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
desktopNotificationsEnabled() {
|
||||
return this.$store.state.jobDesktopNotificationMap[this.job.name];
|
||||
}
|
||||
},
|
||||
name: "JobOptions",
|
||||
props: ["job"],
|
||||
data() {
|
||||
return {
|
||||
cronValid: undefined,
|
||||
logsToDelete: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
desktopNotificationsEnabled() {
|
||||
return this.$store.state.jobDesktopNotificationMap[this.job.name];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.cronValid = this.checkCron(this.job.cron_expression)
|
||||
this.cronValid = this.checkCron(this.job.cron_expression)
|
||||
},
|
||||
methods: {
|
||||
checkCron(expression) {
|
||||
return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression);
|
||||
},
|
||||
updateNotifications(value) {
|
||||
this.$store.dispatch("setJobDesktopNotification", {
|
||||
job: this.job.name,
|
||||
enabled: value
|
||||
});
|
||||
},
|
||||
update() {
|
||||
if (this.job.schedule_enabled) {
|
||||
this.cronValid = this.checkCron(this.job.cron_expression);
|
||||
} else {
|
||||
this.cronValid = undefined;
|
||||
}
|
||||
checkCron(expression) {
|
||||
return /((((\d+,)+\d+|(\d+([/-])\d+)|\d+|\*) ?){5,7})/.test(expression);
|
||||
},
|
||||
updateNotifications(value) {
|
||||
this.$store.dispatch("setJobDesktopNotification", {
|
||||
job: this.job.name,
|
||||
enabled: value
|
||||
});
|
||||
},
|
||||
update() {
|
||||
if (this.job.schedule_enabled) {
|
||||
this.cronValid = this.checkCron(this.job.cron_expression);
|
||||
} else {
|
||||
this.cronValid = undefined;
|
||||
}
|
||||
|
||||
if (this.cronValid !== false) {
|
||||
this.$emit("change", this.job);
|
||||
}
|
||||
if (this.cronValid !== false) {
|
||||
this.$emit("change", this.job);
|
||||
}
|
||||
},
|
||||
onDeleteNowClick() {
|
||||
Sist2AdminApi.getLogsToDelete(this.job.name, this.job.keep_last_n_logs).then(resp => {
|
||||
const toDelete = resp.data;
|
||||
const message = `Delete ${toDelete.length} log files?`;
|
||||
|
||||
this.$bvModal.msgBoxConfirm(message, {
|
||||
title: this.$t("confirmation"),
|
||||
size: "sm",
|
||||
buttonSize: "sm",
|
||||
okVariant: "danger",
|
||||
okTitle: this.$t("delete"),
|
||||
cancelTitle: this.$t("cancel"),
|
||||
footerClass: "p-2",
|
||||
hideHeaderClose: false,
|
||||
centered: true
|
||||
}).then(value => {
|
||||
if (value) {
|
||||
toDelete.forEach(row => {
|
||||
Sist2AdminApi.deleteTaskLogs(row["id"]);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -5,10 +5,13 @@ export default {
|
||||
go: "Go",
|
||||
online: "online",
|
||||
offline: "offline",
|
||||
view: "View",
|
||||
delete: "Delete",
|
||||
runNow: "Index now",
|
||||
create: "Create",
|
||||
cancel: "Cancel",
|
||||
test: "Test",
|
||||
confirmation: "Confirmation",
|
||||
|
||||
jobTitle: "job configuration",
|
||||
tasks: "Tasks",
|
||||
@ -99,6 +102,8 @@ export default {
|
||||
jobOptions: {
|
||||
title: "Job options",
|
||||
cron: "Job schedule",
|
||||
keepNLogs: "Keep last N log files. Set to -1 to keep all logs.",
|
||||
deleteNow: "Delete now",
|
||||
scheduleEnabled: "Enable scheduled re-scan",
|
||||
noJobAvailable: "No jobs available.",
|
||||
desktopNotifications: "Desktop notifications"
|
||||
|
@ -1,38 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
<b-card v-if="tasks.length > 0">
|
||||
<h2>{{ $t("runningTasks") }}</h2>
|
||||
<b-list-group>
|
||||
<TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
<b-card v-if="tasks.length > 0">
|
||||
<h2>{{ $t("runningTasks") }}</h2>
|
||||
<b-list-group>
|
||||
<TaskListItem v-for="task in tasks" :key="task.id" :task="task"></TaskListItem>
|
||||
</b-list-group>
|
||||
</b-card>
|
||||
|
||||
<b-card class="mt-4">
|
||||
<b-card class="mt-4">
|
||||
|
||||
<b-card-title>{{ $t("taskHistory") }}</b-card-title>
|
||||
<b-card-title>{{ $t("taskHistory") }}</b-card-title>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<b-table
|
||||
id="task-history"
|
||||
:items="historyItems"
|
||||
:fields="historyFields"
|
||||
:current-page="historyCurrentPage"
|
||||
:tbody-tr-class="rowClass"
|
||||
:per-page="10"
|
||||
>
|
||||
<template #cell(logs)="data">
|
||||
<router-link :to="`/log/${data.item.logs}`">{{ $t("logs") }}</router-link>
|
||||
</template>
|
||||
<b-table
|
||||
id="task-history"
|
||||
:items="historyItems"
|
||||
:fields="historyFields"
|
||||
:current-page="historyCurrentPage"
|
||||
:tbody-tr-class="rowClass"
|
||||
:per-page="10"
|
||||
>
|
||||
<template #cell(logs)="data">
|
||||
<template v-if="data.item._row.has_logs">
|
||||
<b-button variant="link" size="sm" :to="`/log/${data.item.id}`">
|
||||
{{ $t("view") }}
|
||||
</b-button>
|
||||
/
|
||||
<b-button variant="link" size="sm" @click="deleteLogs(data.item.id)">
|
||||
{{ $t("delete") }}
|
||||
</b-button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
</b-table>
|
||||
<template #cell(delete)="data">
|
||||
</template>
|
||||
|
||||
<b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length"
|
||||
:per-page="10"></b-pagination>
|
||||
</b-table>
|
||||
|
||||
</b-card>
|
||||
</div>
|
||||
<b-pagination limit="20" v-model="historyCurrentPage" :total-rows="historyItems.length"
|
||||
:per-page="10"></b-pagination>
|
||||
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -45,106 +56,116 @@ const HOUR = 3600;
|
||||
const MINUTE = 60;
|
||||
|
||||
function humanDuration(sec_num) {
|
||||
sec_num = sec_num / 1000;
|
||||
const days = Math.floor(sec_num / DAY);
|
||||
sec_num -= days * DAY;
|
||||
const hours = Math.floor(sec_num / HOUR);
|
||||
sec_num -= hours * HOUR;
|
||||
const minutes = Math.floor(sec_num / MINUTE);
|
||||
sec_num -= minutes * MINUTE;
|
||||
const seconds = Math.floor(sec_num);
|
||||
sec_num = sec_num / 1000;
|
||||
const days = Math.floor(sec_num / DAY);
|
||||
sec_num -= days * DAY;
|
||||
const hours = Math.floor(sec_num / HOUR);
|
||||
sec_num -= hours * HOUR;
|
||||
const minutes = Math.floor(sec_num / MINUTE);
|
||||
sec_num -= minutes * MINUTE;
|
||||
const seconds = Math.floor(sec_num);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days} days ${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
if (days > 0) {
|
||||
return `${days} days ${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
if (seconds > 0) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
if (seconds > 0) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
return "<0s";
|
||||
return "<0s";
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Tasks',
|
||||
components: {TaskListItem},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
tasks: [],
|
||||
taskHistory: [],
|
||||
timerId: null,
|
||||
historyFields: [
|
||||
{key: "name", label: this.$t("taskName")},
|
||||
{key: "time", label: this.$t("taskStarted")},
|
||||
{key: "duration", label: this.$t("taskDuration")},
|
||||
{key: "status", label: this.$t("taskStatus")},
|
||||
{key: "logs", label: this.$t("logs")},
|
||||
],
|
||||
historyCurrentPage: 1,
|
||||
historyItems: []
|
||||
}
|
||||
},
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true;
|
||||
this.update().then(() => this.loading = false);
|
||||
name: 'Tasks',
|
||||
components: {TaskListItem},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
tasks: [],
|
||||
taskHistory: [],
|
||||
timerId: null,
|
||||
historyFields: [
|
||||
{key: "name", label: this.$t("taskName")},
|
||||
{key: "time", label: this.$t("taskStarted")},
|
||||
{key: "duration", label: this.$t("taskDuration")},
|
||||
{key: "status", label: this.$t("taskStatus")},
|
||||
{key: "logs", label: this.$t("logs")},
|
||||
],
|
||||
historyCurrentPage: 1,
|
||||
historyItems: []
|
||||
}
|
||||
},
|
||||
props: {
|
||||
msg: String
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true;
|
||||
this.update().then(() => this.loading = false);
|
||||
|
||||
this.timerId = window.setInterval(this.update, 1000);
|
||||
this.updateHistory();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.timerId) {
|
||||
window.clearInterval(this.timerId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClass(row) {
|
||||
if (row.status === "failed") {
|
||||
return "table-danger";
|
||||
}
|
||||
return null;
|
||||
this.timerId = window.setInterval(this.update, 1000);
|
||||
this.updateHistory();
|
||||
},
|
||||
updateHistory() {
|
||||
Sist2AdminApi.getTaskHistory().then(resp => {
|
||||
this.historyItems = resp.data.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
duration: this.taskDuration(row),
|
||||
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
|
||||
logs: row.id,
|
||||
status: row.return_code === 0 ? "ok" : "failed"
|
||||
}));
|
||||
});
|
||||
destroyed() {
|
||||
if (this.timerId) {
|
||||
window.clearInterval(this.timerId);
|
||||
}
|
||||
},
|
||||
update() {
|
||||
return Sist2AdminApi.getTasks().then(resp => {
|
||||
this.tasks = resp.data;
|
||||
})
|
||||
},
|
||||
taskDuration(task) {
|
||||
const start = moment.utc(task.started);
|
||||
const end = moment.utc(task.ended);
|
||||
methods: {
|
||||
rowClass(row) {
|
||||
if (row.status === "failed") {
|
||||
return "table-danger";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
updateHistory() {
|
||||
Sist2AdminApi.getTaskHistory().then(resp => {
|
||||
this.historyItems = resp.data.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
duration: this.taskDuration(row),
|
||||
time: moment.utc(row.started).local().format("dd, MMM Do YYYY, HH:mm:ss"),
|
||||
logs: null,
|
||||
status: row.return_code === 0 ? "ok" : "failed",
|
||||
_row: row
|
||||
}));
|
||||
});
|
||||
},
|
||||
update() {
|
||||
return Sist2AdminApi.getTasks().then(resp => {
|
||||
this.tasks = resp.data;
|
||||
})
|
||||
},
|
||||
taskDuration(task) {
|
||||
const start = moment.utc(task.started);
|
||||
const end = moment.utc(task.ended);
|
||||
|
||||
return humanDuration(end.diff(start))
|
||||
return humanDuration(end.diff(start))
|
||||
},
|
||||
deleteLogs(taskId) {
|
||||
Sist2AdminApi.deleteTaskLogs(taskId).then(() => {
|
||||
this.updateHistory();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#task-history {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -21,7 +21,8 @@ from config import LOG_FOLDER, logger, WEBSERVER_PORT, DATA_FOLDER, SIST2_BINARY
|
||||
from jobs import Sist2Job, Sist2ScanTask, TaskQueue, Sist2IndexTask, JobStatus
|
||||
from notifications import Subscribe, Notifications
|
||||
from sist2 import Sist2
|
||||
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION
|
||||
from state import migrate_v1_to_v2, RUNNING_FRONTENDS, TESSERACT_LANGS, DB_SCHEMA_VERSION, migrate_v3_to_v4, \
|
||||
get_log_files_to_remove, delete_log_file
|
||||
from web import Sist2Frontend
|
||||
|
||||
sist2 = Sist2(SIST2_BINARY, DATA_FOLDER)
|
||||
@ -80,7 +81,6 @@ async def get_jobs():
|
||||
|
||||
@app.put("/api/job/{name:str}")
|
||||
async def update_job(name: str, new_job: Sist2Job):
|
||||
|
||||
new_job.last_modified = datetime.utcnow()
|
||||
job = db["jobs"][name]
|
||||
if not job:
|
||||
@ -133,6 +133,16 @@ async def kill_job(task_id: str):
|
||||
return task_queue.kill_task(task_id)
|
||||
|
||||
|
||||
@app.post("/api/task/{task_id:str}/delete_logs")
|
||||
async def delete_task_logs(task_id: str):
|
||||
if not db["task_done"][task_id]:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
delete_log_file(db, task_id)
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
def _run_job(job: Sist2Job):
|
||||
job.last_modified = datetime.utcnow()
|
||||
if job.status == JobStatus("created"):
|
||||
@ -157,6 +167,11 @@ async def run_job(name: str):
|
||||
return "ok"
|
||||
|
||||
|
||||
@app.get("/api/job/{name:str}/logs_to_delete")
|
||||
async def task_history(n: int, name: str):
|
||||
return get_log_files_to_remove(db, name, n)
|
||||
|
||||
|
||||
@app.delete("/api/job/{name:str}")
|
||||
async def delete_job(name: str):
|
||||
job = db["jobs"][name]
|
||||
@ -320,7 +335,6 @@ async def ws_tail_log(websocket: WebSocket):
|
||||
async with Subscribe(notifications) as ob:
|
||||
async for notification in ob.notifications():
|
||||
await websocket.send_json(notification)
|
||||
print(notification)
|
||||
|
||||
except ConnectionClosed:
|
||||
return
|
||||
@ -380,6 +394,9 @@ if __name__ == '__main__':
|
||||
if db["sist2_admin"]["info"]["version"] == "2":
|
||||
logger.error("Cannot migrate database from v2 to v3. Delete state.db to proceed.")
|
||||
exit(-1)
|
||||
if db["sist2_admin"]["info"]["version"] == "3":
|
||||
logger.info("Migrating to v4 database schema")
|
||||
migrate_v3_to_v4(db)
|
||||
|
||||
start_frontends()
|
||||
cron.initialize(db, _run_job)
|
||||
|
@ -16,7 +16,7 @@ from pydantic import BaseModel
|
||||
from config import logger, LOG_FOLDER
|
||||
from notifications import Notifications
|
||||
from sist2 import ScanOptions, IndexOptions, Sist2
|
||||
from state import RUNNING_FRONTENDS
|
||||
from state import RUNNING_FRONTENDS, get_log_files_to_remove, delete_log_file
|
||||
from web import Sist2Frontend
|
||||
|
||||
|
||||
@ -35,6 +35,8 @@ class Sist2Job(BaseModel):
|
||||
cron_expression: str
|
||||
schedule_enabled: bool = False
|
||||
|
||||
keep_last_n_logs: int = -1
|
||||
|
||||
previous_index: str = None
|
||||
index_path: str = None
|
||||
previous_index_path: str = None
|
||||
@ -301,8 +303,14 @@ class TaskQueue:
|
||||
"ended": task.ended,
|
||||
"started": task.started,
|
||||
"name": task.display_name,
|
||||
"return_code": task_result
|
||||
"return_code": task_result,
|
||||
"has_logs": 1
|
||||
}
|
||||
|
||||
logs_to_delete = get_log_files_to_remove(self._db, task.job.name, task.job.keep_last_n_logs)
|
||||
for row in logs_to_delete:
|
||||
delete_log_file(self._db, row["id"])
|
||||
|
||||
if isinstance(task, Sist2IndexTask):
|
||||
self._notifications.notify({
|
||||
"message": "notifications.indexCompleted",
|
||||
|
@ -1,16 +1,19 @@
|
||||
from typing import Dict
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from hexlib.db import Table, PersistentState
|
||||
import pickle
|
||||
|
||||
from tesseract import get_tesseract_langs
|
||||
import sqlite3
|
||||
from config import LOG_FOLDER
|
||||
|
||||
RUNNING_FRONTENDS: Dict[str, int] = {}
|
||||
|
||||
TESSERACT_LANGS = get_tesseract_langs()
|
||||
|
||||
DB_SCHEMA_VERSION = "3"
|
||||
DB_SCHEMA_VERSION = "4"
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@ -50,8 +53,35 @@ class PickleTable(Table):
|
||||
yield dict((k, _deserialize(v)) for k, v in row.items())
|
||||
|
||||
|
||||
def migrate_v1_to_v2(db: PersistentState):
|
||||
def get_log_files_to_remove(db: PersistentState, job_name: str, n: int):
|
||||
if n < 0:
|
||||
return []
|
||||
|
||||
counter = 0
|
||||
to_remove = []
|
||||
|
||||
for row in db["task_done"].sql("WHERE has_logs=1 ORDER BY started DESC"):
|
||||
if row["name"].endswith(f"[{job_name}]"):
|
||||
counter += 1
|
||||
|
||||
if counter > n:
|
||||
to_remove.append(row)
|
||||
|
||||
return to_remove
|
||||
|
||||
|
||||
def delete_log_file(db: PersistentState, task_id: str):
|
||||
db["task_done"][task_id] = {
|
||||
"has_logs": 0
|
||||
}
|
||||
|
||||
try:
|
||||
os.remove(os.path.join(LOG_FOLDER, f"sist2-{task_id}.log"))
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def migrate_v1_to_v2(db: PersistentState):
|
||||
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v2.bak")
|
||||
|
||||
# Frontends
|
||||
@ -77,3 +107,16 @@ def migrate_v1_to_v2(db: PersistentState):
|
||||
db["sist2_admin"]["info"] = {
|
||||
"version": "2"
|
||||
}
|
||||
|
||||
|
||||
def migrate_v3_to_v4(db: PersistentState):
|
||||
shutil.copy(db.dbfile, db.dbfile + "-before-migrate-v4.bak")
|
||||
|
||||
conn = sqlite3.connect(db.dbfile)
|
||||
conn.execute("ALTER TABLE task_done ADD COLUMN has_logs INTEGER DEFAULT 1")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
db["sist2_admin"]["info"] = {
|
||||
"version": "4"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user