-
+
+
{{ $t('App.Notifications.Remind') }}
-
-
- 1H
-
-
- 1D
-
-
- 7D
-
-
-
-
- {{ $t('App.Notifications.NextReboot') }}
-
-
- {{ $t('App.Notifications.Never') }}
-
-
+
+ {{ reminder.text }}
+
+
@@ -101,6 +83,13 @@ import BaseMixin from '@/components/mixins/base'
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
import { mdiClose, mdiLinkVariant, mdiBellOffOutline } from '@mdi/js'
import { GuiNotificationStateEntry } from '@/store/gui/notifications/types'
+import { TranslateResult } from 'vue-i18n'
+import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
+
+interface ReminderOption {
+ text: string | TranslateResult
+ clickFunction: Function
+}
@Component({
components: {},
@@ -110,7 +99,8 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
mdiLinkVariant = mdiLinkVariant
mdiBellOffOutline = mdiBellOffOutline
- private expand = false
+ expand = false
+ showMaintenanceDetails = false
@Prop({ required: true })
declare readonly entry: GuiNotificationStateEntry
@@ -139,6 +129,49 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
return this.entry.id.slice(0, posFirstSlash)
}
+ get maintenanceEntry() {
+ if (this.entryType !== 'maintenance') return null
+
+ const id = this.entry.id.replace('maintenance/', '')
+ const entries = this.$store.getters['gui/maintenance/getEntries']
+
+ return entries.find((entry: GuiMaintenanceStateEntry) => entry.id === id)
+ }
+
+ get reminderTimes() {
+ let output: ReminderOption[] = [
+ {
+ text: this.$t('App.Notifications.NextReboot'),
+ clickFunction: () => this.dismiss('reboot', null),
+ },
+ { text: this.$t('App.Notifications.Never'), clickFunction: () => this.close() },
+ ]
+
+ if (['announcement', 'maintenance'].includes(this.entryType)) {
+ output = []
+ output.push({
+ text: this.$t('App.Notifications.OneHourShort'),
+ clickFunction: () => this.dismiss('time', 60 * 60),
+ })
+ output.push({
+ text: this.$t('App.Notifications.OneDayShort'),
+ clickFunction: () => this.dismiss('time', 60 * 60 * 24),
+ })
+ output.push({
+ text: this.$t('App.Notifications.OneWeekShort'),
+ clickFunction: () => this.dismiss('time', 60 * 60 * 24 * 7),
+ })
+ }
+
+ return output
+ }
+
+ xButtonAction() {
+ if (this.entryType === 'announcement') return this.close()
+
+ this.dismiss('reboot', null)
+ }
+
close() {
this.$store.dispatch('gui/notifications/close', { id: this.entry.id })
}
@@ -157,10 +190,12 @@ export default class NotificationMenuEntry extends Mixins(BaseMixin) {
diff --git a/src/components/panels/HistoryStatisticsPanel.vue b/src/components/panels/HistoryStatisticsPanel.vue
index a282fe979..628bb12c6 100644
--- a/src/components/panels/HistoryStatisticsPanel.vue
+++ b/src/components/panels/HistoryStatisticsPanel.vue
@@ -12,19 +12,19 @@
{{ $t('History.SelectedPrinttime') }} |
- {{ formatPrintTime(selectedPrintTime) }} |
+ {{ formatPrintTime(selectedPrintTime, false) }} |
{{ $t('History.LongestPrinttime') }} |
- {{ formatPrintTime(selectedLongestPrintTime) }} |
+ {{ formatPrintTime(selectedLongestPrintTime, false) }} |
{{ $t('History.AvgPrinttime') }} |
- {{ formatPrintTime(selectedAvgPrintTime) }} |
+ {{ formatPrintTime(selectedAvgPrintTime, false) }} |
{{ $t('History.SelectedFilamentUsed') }} |
- {{ Math.round(selectedFilamentUsed / 100) / 10 }} m |
+ {{ selectedFilamentUsedFormat }} |
{{ $t('History.SelectedJobs') }} |
@@ -34,19 +34,19 @@
{{ $t('History.TotalPrinttime') }} |
- {{ formatPrintTime(totalPrintTime) }} |
+ {{ formatPrintTime(totalPrintTime, false) }} |
{{ $t('History.LongestPrinttime') }} |
- {{ formatPrintTime(longestPrintTime) }} |
+ {{ formatPrintTime(longestPrintTime, false) }} |
{{ $t('History.AvgPrinttime') }} |
- {{ formatPrintTime(avgPrintTime) }} |
+ {{ formatPrintTime(avgPrintTime, false) }} |
{{ $t('History.TotalFilamentUsed') }} |
- {{ Math.round(totalFilamentUsed / 100) / 10 }} m |
+ {{ totalFilamentUsedFormat }} |
{{ $t('History.TotalJobs') }} |
@@ -57,17 +57,12 @@
-
-
+
+
-
- {{ $t('History.Chart') }}
-
-
- {{ $t('History.Table') }}
-
+ {{ $t('History.Chart') }}
+ {{ $t('History.Table') }}
@@ -88,16 +83,12 @@
-
-
+
+
-
- {{ $t('History.FilamentUsage') }}
-
-
- {{ $t('History.PrinttimeAvg') }}
-
+ {{ $t('History.FilamentUsage') }}
+ {{ $t('History.PrinttimeAvg') }}
@@ -115,15 +106,17 @@ import HistoryPrinttimeAvg from '@/components/charts/HistoryPrinttimeAvg.vue'
import HistoryAllPrintStatusChart from '@/components/charts/HistoryAllPrintStatusChart.vue'
import { ServerHistoryStateJob } from '@/store/server/history/types'
import { mdiChartAreaspline, mdiDatabaseArrowDownOutline } from '@mdi/js'
+import { formatPrintTime } from '@/plugins/helpers'
@Component({
components: { Panel, HistoryFilamentUsage, HistoryPrinttimeAvg, HistoryAllPrintStatusChart },
})
export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
mdiChartAreaspline = mdiChartAreaspline
mdiDatabaseArrowDownOutline = mdiDatabaseArrowDownOutline
+ formatPrintTime = formatPrintTime
get selectedJobs() {
- return this.$store.state.gui.view.history.selectedJobs ?? []
+ return this.$store.getters['server/history/getSelectedJobs']
}
get existsSelectedJobs() {
@@ -131,9 +124,7 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
}
get totalPrintTime() {
- return 'total_print_time' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.total_print_time
- : 0
+ return this.$store.state.server.history.job_totals?.total_print_time ?? 0
}
get selectedPrintTime() {
@@ -147,9 +138,7 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
}
get longestPrintTime() {
- return 'longest_print' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.longest_print
- : 0
+ return this.$store.state.server.history.job_totals?.longest_print ?? 0
}
get selectedLongestPrintTime() {
@@ -177,9 +166,13 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
}
get totalFilamentUsed() {
- return 'total_filament_used' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.total_filament_used
- : 0
+ return this.$store.state.server.history.job_totals?.total_filament_used ?? 0
+ }
+
+ get totalFilamentUsedFormat() {
+ const value = Math.round(this.totalFilamentUsed / 100) / 10
+
+ return `${value} m`
}
get selectedFilamentUsed() {
@@ -192,10 +185,14 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
return filamentUsed
}
+ get selectedFilamentUsedFormat() {
+ const value = Math.round(this.selectedFilamentUsed / 100) / 10
+
+ return `${value} m`
+ }
+
get totalJobsCount() {
- return 'total_jobs' in this.$store.state.server.history.job_totals
- ? this.$store.state.server.history.job_totals.total_jobs
- : 0
+ return this.$store.state.server.history.job_totals?.total_jobs ?? 0
}
get toggleChart() {
@@ -223,25 +220,5 @@ export default class HistoryStatisticsPanel extends Mixins(BaseMixin) {
this.$socket.emit('server.history.list', { start: 0, limit: 50 }, { action: 'server/history/getHistory' })
}
-
- formatPrintTime(totalSeconds: number) {
- if (totalSeconds) {
- let output = ''
-
- const hours = Math.floor(totalSeconds / 3600)
- totalSeconds %= 3600
- if (hours) output += ' ' + hours + 'h'
-
- const minutes = Math.floor(totalSeconds / 60)
- if (minutes) output += ' ' + minutes + 'm'
-
- const seconds = totalSeconds % 60
- if (seconds) output += ' ' + seconds.toFixed(0) + 's'
-
- return output
- }
-
- return '--'
- }
}
diff --git a/src/locales/en.json b/src/locales/en.json
index a9999d938..3e8e1f1a2 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -16,6 +16,8 @@
"KlipperRuntimeWarning": "Klipper runtime warning",
"KlipperWarning": "Klipper warning"
},
+ "MaintenanceReminder": "Maintenance Reminder",
+ "MaintenanceReminderText": "Maintenance \"{name}\" is due.",
"MoonrakerWarnings": {
"MoonrakerComponent": "Moonraker: {component}",
"MoonrakerFailedComponentDescription": "An error was detected while loading the moonraker component '{component}'. Please check the log file and fix the issue.",
@@ -29,7 +31,11 @@
"NextReboot": "next reboot",
"NoNotification": "No Notification available",
"Notifications": "Notifications",
- "Remind": "Remind:"
+ "OneDayShort": "1D",
+ "OneHourShort": "1H",
+ "OneWeekShort": "1W",
+ "Remind": "Remind:",
+ "ShowDetails": "show details"
},
"NumberInput": {
"GreaterOrEqualError": "Must be greater or equal than {min}!",
@@ -345,6 +351,8 @@
"Wireframe": "Wireframe"
},
"History": {
+ "AddANote": "Add a note",
+ "AddMaintenance": "Add Maintenance",
"AddNote": "Add note",
"AddToQueueSuccessful": "File {filename} added to Queue.",
"AllJobs": "All",
@@ -352,16 +360,26 @@
"Cancel": "Cancel",
"Chart": "Chart",
"CreateNote": "Create Note",
+ "DateBasedReminder": "Date",
+ "DateBasedReminderDescription": "This reminder is based on the date.",
+ "Days": "days",
"Delete": "Delete",
"DeleteSelectedQuestion": "Do you really want to delete {count} selected jobs?",
"DeleteSingleJobQuestion": "Do you really want to delete the job?",
"Details": "Details",
+ "EditMaintenance": "Edit Maintenance",
"EditNote": "Edit Note",
"Empty": "empty",
"EndTime": "End Time",
+ "EntryCreatedAt": "Created at {date}.",
+ "EntryNextPerform": "Next perform:",
+ "EntryPerformedAt": "Performed at {date}.",
+ "EntrySince": "Used since:",
"EstimatedFilament": "Estimated Filament",
"EstimatedFilamentWeight": "Estimated Filament Weight",
"EstimatedTime": "Estimated Time",
+ "FilamentBasedReminder": "Filament",
+ "FilamentBasedReminderDescription": "This reminder is based on the filament usage.",
"FilamentCalc": "Filament Calc",
"FilamentUsage": "Filament usage",
"FilamentUsed": "Filament Used",
@@ -372,18 +390,35 @@
"FirstLayerHeight": "First Layer Height",
"HistoryFilamentUsage": "Filament",
"HistoryPrinttimeAVG": "Prints",
+ "Hours": "hours",
+ "InvalidNameEmpty": "Invalid name. Name must not be empty!",
"JobDetails": "Job Details",
"Jobs": "Jobs",
"LastModified": "Last Modified",
"LayerHeight": "Layer Height",
"LoadCompleteHistory": "Load complete history",
"LongestPrinttime": "Longest Print Time",
+ "Maintenance": "Maintenance",
+ "MaintenanceEntries": "Maintenance Entries",
+ "Meter": "meter",
+ "Name": "Name",
+ "NoReminder": "No reminder",
"Note": "Note",
"ObjectHeight": "Object Height",
+ "OneTime": "One-Time",
+ "Perform": "perform",
+ "Performed": "performed",
+ "PerformedAndReschedule": "performed and reschedule",
+ "PerformMaintenance": "Perform Maintenance",
"PrintDuration": "Print Time",
"PrintHistory": "Print History",
+ "PrintJobs": "Print Jobs",
"PrintTime": "Print Time",
"PrinttimeAvg": "Print Time - Ø",
+ "PrinttimeBasedReminder": "Print Time",
+ "PrinttimeBasedReminderDescription": "This reminder is based on the print time.",
+ "Reminder": "Reminder",
+ "Repeat": "Repeat",
"Reprint": "Reprint",
"Save": "save",
"Search": "search",
@@ -950,7 +985,8 @@
"DbConsoleHistory": "Console History",
"DbHistoryJobs": "History Jobs",
"DbHistoryTotals": "History Totals",
- "DBNavigation": "Navigation",
+ "DbMaintenance": "Maintenance",
+ "DbNavigation": "Navigation",
"DbTimelapseSettings": "Timelapse Settings",
"DbView": "View Settings",
"EstimateValues": {
diff --git a/src/pages/History.vue b/src/pages/History.vue
index 84ef0fd35..4a304f4c4 100644
--- a/src/pages/History.vue
+++ b/src/pages/History.vue
@@ -17,11 +17,9 @@ import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import HistoryListPanel from '@/components/panels/HistoryListPanel.vue'
import HistoryStatisticsPanel from '@/components/panels/HistoryStatisticsPanel.vue'
+
@Component({
- components: {
- HistoryStatisticsPanel,
- HistoryListPanel,
- },
+ components: { HistoryListPanel, HistoryStatisticsPanel },
})
export default class PageHistory extends Mixins(BaseMixin) {}
diff --git a/src/plugins/helpers.ts b/src/plugins/helpers.ts
index 8cbd54a94..719408ce8 100644
--- a/src/plugins/helpers.ts
+++ b/src/plugins/helpers.ts
@@ -112,30 +112,30 @@ export const formatFrequency = (frequency: number): string => {
return Math.max(frequency, 0.1).toFixed() + units[i]
}
-export const formatPrintTime = (totalSeconds: number): string => {
- if (totalSeconds) {
- let output = ''
+export const formatPrintTime = (totalSeconds: number, boolDays = true): string => {
+ if (!totalSeconds) return '--'
+ const output: string[] = []
+
+ if (boolDays) {
const days = Math.floor(totalSeconds / (3600 * 24))
if (days) {
totalSeconds %= 3600 * 24
- output += days + 'd'
+ output.push(`${days}d`)
}
+ }
- const hours = Math.floor(totalSeconds / 3600)
- totalSeconds %= 3600
- if (hours) output += ' ' + hours + 'h'
-
- const minutes = Math.floor(totalSeconds / 60)
- if (minutes) output += ' ' + minutes + 'm'
+ const hours = Math.floor(totalSeconds / 3600)
+ totalSeconds %= 3600
+ if (hours) output.push(`${hours}h`)
- const seconds = totalSeconds % 60
- if (seconds) output += ' ' + seconds.toFixed(0) + 's'
+ const minutes = Math.floor(totalSeconds / 60)
+ if (minutes) output.push(`${minutes}m`)
- return output
- }
+ const seconds = totalSeconds % 60
+ if (seconds) output.push(`${seconds.toFixed(0)}s`)
- return '--'
+ return output.join(' ')
}
export const sortFiles = (items: FileStateFile[] | null, sortBy: string[], sortDesc: boolean[]): FileStateFile[] => {
diff --git a/src/store/gui/actions.ts b/src/store/gui/actions.ts
index 1902d6e8a..ab362ee99 100644
--- a/src/store/gui/actions.ts
+++ b/src/store/gui/actions.ts
@@ -246,7 +246,7 @@ export const actions: ActionTree = {
}
for (const key of payload) {
- if (['webcams', 'timelapse'].includes(key)) {
+ if (['maintenance', 'timelapse', 'webcams'].includes(key)) {
const url = baseUrl + '?namespace=' + key
const response = await fetch(url)
diff --git a/src/store/gui/index.ts b/src/store/gui/index.ts
index 11c6cae55..44ac886f9 100644
--- a/src/store/gui/index.ts
+++ b/src/store/gui/index.ts
@@ -14,6 +14,7 @@ import { navigation } from '@/store/gui/navigation'
import { notifications } from '@/store/gui/notifications'
import { presets } from '@/store/gui/presets'
import { remoteprinters } from '@/store/gui/remoteprinters'
+import { maintenance } from '@/store/gui/maintenance'
import { webcams } from '@/store/gui/webcams'
import { heightmap } from '@/store/gui/heightmap'
@@ -243,6 +244,8 @@ export const getDefaultState = (): GuiState => {
'object_height',
],
selectedJobs: [],
+ showMaintenanceEntries: true,
+ showPrintJobs: true,
},
jobqueue: {
countPerPage: 10,
@@ -294,6 +297,7 @@ export const gui: Module = {
console,
gcodehistory,
macros,
+ maintenance,
miscellaneous,
navigation,
notifications,
diff --git a/src/store/gui/maintenance/actions.ts b/src/store/gui/maintenance/actions.ts
new file mode 100644
index 000000000..3205bcba2
--- /dev/null
+++ b/src/store/gui/maintenance/actions.ts
@@ -0,0 +1,172 @@
+import Vue from 'vue'
+import { ActionTree } from 'vuex'
+import { GuiMaintenanceState, MaintenanceJson } from '@/store/gui/maintenance/types'
+import { RootState } from '@/store/types'
+import { v4 as uuidv4 } from 'uuid'
+import { themeDir } from '@/store/variables'
+
+export const actions: ActionTree = {
+ reset({ commit }) {
+ commit('reset')
+ },
+
+ init() {
+ Vue.$socket.emit(
+ 'server.database.get_item',
+ { namespace: 'maintenance' },
+ { action: 'gui/maintenance/initStore' }
+ )
+ },
+
+ async initDb({ dispatch, rootGetters }) {
+ const baseUrl = rootGetters['socket/getUrl']
+ const url = `${baseUrl}/server/files/config/${themeDir}/maintenance.json?time=${Date.now()}`
+
+ const defaults: MaintenanceJson = await fetch(url)
+ .then((response) => {
+ if (response.status !== 200) return { entries: [] }
+
+ return response.json()
+ })
+ .catch((e) => {
+ window.console.error('maintenance.json cannot be parsed', e)
+ return { entries: [] }
+ })
+
+ // stop, when no entries are available/found
+ const entries = defaults.entries ?? []
+ if (entries?.length === 0) {
+ return
+ }
+
+ const totals = await fetch(`${baseUrl}/server/history/totals`)
+ .then((response) => {
+ if (response.status !== 200) return {}
+
+ return response.json()
+ })
+ .then((response: any) => response.result?.job_totals ?? {})
+ .catch((e) => {
+ window.console.debug('History totals could not be loaded', e)
+ })
+
+ const total_filament = totals.total_filament_used ?? 0
+ const total_print_time = totals.total_print_time ?? 0
+ const date = new Date().getTime() / 1000
+
+ entries.forEach((entry) => {
+ dispatch('store', {
+ entry: {
+ name: entry.name,
+ note: entry.note ?? '',
+ start_time: date,
+ end_time: null,
+ start_filament: total_filament,
+ end_filament: null,
+ start_printtime: total_print_time,
+ end_printtime: null,
+ last_entry: null,
+
+ reminder: {
+ type: entry.reminder?.type ?? null,
+
+ filament: {
+ bool: entry.reminder?.filament?.bool ?? false,
+ value: entry.reminder?.filament?.value ?? null,
+ },
+
+ printtime: {
+ bool: entry.reminder?.printtime?.bool ?? false,
+ value: entry.reminder?.printtime?.value ?? null,
+ },
+
+ date: {
+ bool: entry.reminder?.date?.bool ?? false,
+ value: entry.reminder?.date?.value ?? null,
+ },
+ },
+ },
+ })
+ })
+ },
+
+ async initStore({ commit, dispatch }, payload) {
+ await commit('reset')
+ await commit('initStore', payload)
+ await dispatch('socket/removeInitModule', 'gui/maintenance/init', { root: true })
+ },
+
+ upload(_, payload) {
+ Vue.$socket.emit('server.database.post_item', {
+ namespace: 'maintenance',
+ key: payload.id,
+ value: payload.value,
+ })
+ },
+
+ store({ commit, dispatch, state }, payload) {
+ const id = uuidv4()
+
+ commit('store', { id, values: payload.entry })
+ dispatch('upload', {
+ id,
+ value: state.entries[id],
+ })
+ },
+
+ update({ commit, dispatch }, payload) {
+ const id = payload.id
+ delete payload.id
+
+ commit('update', {
+ id: id,
+ entry: payload,
+ })
+ dispatch('upload', {
+ id: id,
+ value: payload,
+ })
+ },
+
+ delete({ commit }, payload) {
+ commit('delete', payload)
+ Vue.$socket.emit('server.database.delete_item', { namespace: 'maintenance', key: payload })
+ },
+
+ perform({ dispatch, state, rootState }, payload: { id: string; note: string }) {
+ const entry = state.entries[payload.id]
+ if (!entry) return
+
+ const totalFilament = rootState.server?.history?.job_totals?.total_filament_used ?? 0
+ const totalPrintTime = rootState.server?.history?.job_totals?.total_print_time ?? 0
+
+ entry.id = payload.id
+ entry.end_time = Date.now() / 1000
+ entry.end_filament = totalFilament
+ entry.end_printtime = totalPrintTime
+ entry.perform_note = payload.note.trim() || null
+
+ dispatch('update', entry)
+
+ if (entry.reminder.type === 'repeat') {
+ const date = new Date()
+
+ dispatch('store', {
+ entry: {
+ name: entry.name,
+ note: entry.note,
+ // divided by 1000 to get seconds, because history entries are also in seconds
+ start_time: date.getTime() / 1000,
+ end_time: null,
+ start_filament: totalFilament,
+ end_filament: null,
+ start_printtime: totalPrintTime,
+ end_printtime: null,
+ last_entry: payload.id,
+
+ reminder: { ...entry.reminder },
+ },
+ })
+ }
+ },
+}
diff --git a/src/store/gui/maintenance/getters.ts b/src/store/gui/maintenance/getters.ts
new file mode 100644
index 000000000..17f64b6c2
--- /dev/null
+++ b/src/store/gui/maintenance/getters.ts
@@ -0,0 +1,47 @@
+import { GetterTree } from 'vuex'
+import { GuiMaintenanceState, GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
+
+// eslint-disable-next-line
+export const getters: GetterTree = {
+ getEntries: (state) => {
+ const entries: GuiMaintenanceStateEntry[] = []
+
+ Object.keys(state.entries).forEach((id: string) => {
+ entries.push({ ...state.entries[id], id })
+ })
+
+ return entries
+ },
+
+ getOverdueEntries: (state, getters, rootState) => {
+ const currentTotalPrintTime = rootState.server.history.job_totals.total_print_time ?? 0
+ const currentTotalFilamentUsed = rootState.server.history.job_totals.total_filament_used ?? 0
+ const currentDate = new Date().getTime() / 1000
+
+ const entries: GuiMaintenanceStateEntry[] = getters['getEntries'] ?? []
+
+ return entries.filter((entry) => {
+ if (entry.reminder.type === null || entry.end_time !== null) return false
+
+ if (entry.reminder.filament.bool) {
+ const end = entry.start_filament + (entry.reminder.filament.value ?? 0)
+
+ if (end <= currentTotalFilamentUsed) return true
+ }
+
+ if (entry.reminder.printtime.bool) {
+ const end = entry.start_printtime + (entry.reminder.printtime.value ?? 0)
+
+ if (end <= currentTotalPrintTime) return true
+ }
+
+ if (entry.reminder.date.bool) {
+ const end = entry.start_time + (entry.reminder.date.value ?? 0) * 24 * 60 * 60
+
+ if (end <= currentDate) return true
+ }
+
+ return false
+ })
+ },
+}
diff --git a/src/store/gui/maintenance/index.ts b/src/store/gui/maintenance/index.ts
new file mode 100644
index 000000000..fdfcc39b3
--- /dev/null
+++ b/src/store/gui/maintenance/index.ts
@@ -0,0 +1,23 @@
+import { Module } from 'vuex'
+import { GuiMaintenanceState } from '@/store/gui/maintenance/types'
+import { actions } from '@/store/gui/maintenance/actions'
+import { mutations } from '@/store/gui/maintenance/mutations'
+import { getters } from '@/store/gui/maintenance/getters'
+
+export const getDefaultState = (): GuiMaintenanceState => {
+ return {
+ entries: {},
+ }
+}
+
+// initial state
+const state = getDefaultState()
+
+// eslint-disable-next-line
+export const maintenance: Module = {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+}
diff --git a/src/store/gui/maintenance/mutations.ts b/src/store/gui/maintenance/mutations.ts
new file mode 100644
index 000000000..7f9dee174
--- /dev/null
+++ b/src/store/gui/maintenance/mutations.ts
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import { MutationTree } from 'vuex'
+import { GuiMaintenanceState } from '@/store/gui/maintenance/types'
+import { getDefaultState } from './index'
+
+export const mutations: MutationTree = {
+ reset(state) {
+ Object.assign(state, getDefaultState())
+ },
+
+ initStore(state, payload) {
+ Vue.set(state, 'entries', payload.value)
+ },
+
+ store(state, payload) {
+ Vue.set(state.entries, payload.id, payload.values)
+ },
+
+ update(state, payload) {
+ if (!(payload.id in state.entries)) return
+
+ const entry = { ...state.entries[payload.id] }
+ Object.assign(entry, payload.entry)
+ Vue.set(state.entries, payload.id, entry)
+ },
+
+ delete(state, payload) {
+ if (payload in state.entries) {
+ Vue.delete(state.entries, payload)
+ }
+ },
+}
diff --git a/src/store/gui/maintenance/types.ts b/src/store/gui/maintenance/types.ts
new file mode 100644
index 000000000..cb63e8253
--- /dev/null
+++ b/src/store/gui/maintenance/types.ts
@@ -0,0 +1,67 @@
+export interface GuiMaintenanceState {
+ entries: {
+ [key: string]: GuiMaintenanceStateEntry
+ }
+}
+
+export interface GuiMaintenanceStateEntry {
+ id?: string
+ name: string
+ note: string
+ perform_note: string | null
+ start_time: number
+ end_time: number | null
+ start_filament: number
+ end_filament: number | null
+ start_printtime: number
+ end_printtime: number | null
+ last_entry: string | null
+
+ reminder: {
+ type: null | 'one-time' | 'repeat'
+
+ filament: {
+ bool: boolean
+ value: number | null
+ }
+
+ printtime: {
+ bool: boolean
+ value: number | null
+ }
+
+ date: {
+ bool: boolean
+ value: number | null
+ }
+ }
+}
+
+export interface HistoryListRowMaintenance extends GuiMaintenanceStateEntry {
+ type: 'maintenance'
+ select_id: string
+}
+
+export interface MaintenanceJson {
+ entries: MaintenanceJsonEntry[]
+}
+
+interface MaintenanceJsonEntry {
+ name: string
+ note?: string
+ reminder?: {
+ type: null | 'one-time' | 'repeat'
+ filament?: {
+ bool: boolean
+ value: number | null
+ }
+ printtime?: {
+ bool: boolean
+ value: number | null
+ }
+ date?: {
+ bool: boolean
+ value: number | null
+ }
+ }
+}
diff --git a/src/store/gui/notifications/getters.ts b/src/store/gui/notifications/getters.ts
index a8ddaa934..7364dd0a5 100644
--- a/src/store/gui/notifications/getters.ts
+++ b/src/store/gui/notifications/getters.ts
@@ -8,6 +8,7 @@ import { PrinterStateKlipperConfigWarning } from '@/store/printer/types'
import { detect } from 'detect-browser'
import semver from 'semver'
import { minBrowserVersions } from '@/store/variables'
+import { GuiMaintenanceStateEntry } from '@/store/gui/maintenance/types'
export const getters: GetterTree = {
getNotifications: (state, getters) => {
@@ -34,6 +35,9 @@ export const getters: GetterTree = {
// klipper warnings
notifications = notifications.concat(getters['getNotificationsKlipperWarnings'])
+ // user-created reminders
+ notifications = notifications.concat(getters['getNotificationsOverdueMaintenance'])
+
// browser warnings
notifications = notifications.concat(getters['getNotificationsBrowserWarnings'])
@@ -363,6 +367,37 @@ export const getters: GetterTree = {
return notifications
},
+ getNotificationsOverdueMaintenance: (state, getters, rootState, rootGetters) => {
+ const notifications: GuiNotificationStateEntry[] = []
+ let entries: GuiMaintenanceStateEntry[] = rootGetters['gui/maintenance/getOverdueEntries']
+ if (entries.length == 0) return []
+
+ const date = rootState.server.system_boot_at ?? new Date()
+
+ // get all dismissed reminders and convert it to a string[]
+ const remindersDismisses = rootGetters['gui/notifications/getDismissByCategory']('maintenance').map(
+ (dismiss: GuiNotificationStateDismissEntry) => {
+ return dismiss.id
+ }
+ )
+
+ // filter all dismissed reminders
+ entries = entries.filter((entry) => !remindersDismisses.includes(entry.id))
+
+ entries.forEach((entry) => {
+ notifications.push({
+ id: `maintenance/${entry.id}`,
+ priority: 'high',
+ title: i18n.t('App.Notifications.MaintenanceReminder').toString(),
+ description: i18n.t('App.Notifications.MaintenanceReminderText', { name: entry.name }).toString(),
+ date,
+ dismissed: false,
+ })
+ })
+
+ return notifications
+ },
+
getDismiss: (state, getters, rootState) => {
const currentTime = new Date()
const systemBootAt = rootState.server.system_boot_at ?? new Date()
diff --git a/src/store/gui/reminders/actions.ts b/src/store/gui/reminders/actions.ts
new file mode 100644
index 000000000..2dc074945
--- /dev/null
+++ b/src/store/gui/reminders/actions.ts
@@ -0,0 +1,60 @@
+import Vue from 'vue'
+import { ActionTree } from 'vuex'
+import { GuiRemindersState } from '@/store/gui/reminders/types'
+import { RootState } from '@/store/types'
+import { v4 as uuidv4 } from 'uuid'
+
+export const actions: ActionTree = {
+ reset({ commit }) {
+ commit('reset')
+ },
+
+ init() {
+ Vue.$socket.emit('server.database.get_item', { namespace: 'reminders' }, { action: 'gui/reminders/initStore' })
+ },
+
+ async initStore({ commit, dispatch }, payload) {
+ await commit('reset')
+ await commit('initStore', payload)
+ await dispatch('socket/removeInitModule', 'gui/reminders/init', { root: true })
+ },
+
+ upload(_, payload) {
+ Vue.$socket.emit('server.database.post_item', { namespace: 'reminders', key: payload.id, value: payload.value })
+ },
+
+ store({ commit, dispatch, state }, payload) {
+ const id = uuidv4()
+
+ commit('store', { id, values: payload.values })
+ dispatch('upload', {
+ id,
+ value: state.reminders[id],
+ })
+ },
+
+ update({ commit, dispatch, state }, payload) {
+ commit('update', payload)
+ dispatch('upload', {
+ id: payload.id,
+ value: state.reminders[payload.id],
+ })
+ },
+
+ delete({ commit }, payload) {
+ commit('delete', payload)
+ Vue.$socket.emit('server.database.delete_item', { namespace: 'reminders', key: payload })
+ },
+
+ repeat({ dispatch, getters, state, rootState }, payload) {
+ if (!(payload.id in state.reminders)) return
+ const reminder = getters['getReminder'](payload.id)
+ const new_start_time = rootState.server?.history?.job_totals.total_print_time || 0
+ const snooze_epoch_time = Date.now()
+ dispatch('update', {
+ id: reminder.id,
+ snooze_print_hours_timestamps: [...reminder.snooze_print_hours_timestamps, new_start_time],
+ snooze_epoch_timestamps: [...reminder.snooze_epoch_timestamps, snooze_epoch_time],
+ })
+ },
+}
diff --git a/src/store/gui/reminders/getters.ts b/src/store/gui/reminders/getters.ts
new file mode 100644
index 000000000..40599e430
--- /dev/null
+++ b/src/store/gui/reminders/getters.ts
@@ -0,0 +1,29 @@
+import { GetterTree } from 'vuex'
+import { GuiRemindersState, GuiRemindersStateReminder } from '@/store/gui/reminders/types'
+
+// eslint-disable-next-line
+export const getters: GetterTree = {
+ getReminders: (state) => {
+ const reminders: GuiRemindersStateReminder[] = []
+
+ Object.keys(state.reminders).forEach((id: string) => {
+ reminders.push({ ...state.reminders[id], id })
+ })
+
+ return reminders
+ },
+
+ getReminder: (state, getters) => (id: string) => {
+ const reminders = getters['getReminders'] ?? []
+
+ return reminders.find((reminder: GuiRemindersStateReminder) => reminder.id === id)
+ },
+
+ getOverdueReminders: (state, getters, rootState) => {
+ const currentTotalPrintTime = rootState.server.history.job_totals.total_print_time
+ const reminders: GuiRemindersStateReminder[] = getters['getReminders'] ?? []
+ return reminders.filter(
+ (reminder) => reminder.time_delta - (currentTotalPrintTime - reminder.start_total_print_time) < 0
+ )
+ },
+}
diff --git a/src/store/gui/reminders/index.ts b/src/store/gui/reminders/index.ts
new file mode 100644
index 000000000..227263eba
--- /dev/null
+++ b/src/store/gui/reminders/index.ts
@@ -0,0 +1,23 @@
+import { Module } from 'vuex'
+import { GuiRemindersState } from '@/store/gui/reminders/types'
+import { actions } from '@/store/gui/reminders/actions'
+import { mutations } from '@/store/gui/reminders/mutations'
+import { getters } from '@/store/gui/reminders/getters'
+
+export const getDefaultState = (): GuiRemindersState => {
+ return {
+ reminders: {},
+ }
+}
+
+// initial state
+const state = getDefaultState()
+
+// eslint-disable-next-line
+export const reminders: Module = {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+}
diff --git a/src/store/gui/reminders/mutations.ts b/src/store/gui/reminders/mutations.ts
new file mode 100644
index 000000000..992f2bdf5
--- /dev/null
+++ b/src/store/gui/reminders/mutations.ts
@@ -0,0 +1,32 @@
+import Vue from 'vue'
+import { MutationTree } from 'vuex'
+import { GuiRemindersState } from '@/store/gui/reminders/types'
+import { getDefaultState } from './index'
+
+export const mutations: MutationTree = {
+ reset(state) {
+ Object.assign(state, getDefaultState())
+ },
+
+ initStore(state, payload) {
+ Vue.set(state, 'reminders', payload.value)
+ },
+
+ store(state, payload) {
+ Vue.set(state.reminders, payload.id, payload.values)
+ },
+
+ update(state, payload) {
+ if (payload.id in state.reminders) {
+ const reminder = { ...state.reminders[payload.id] }
+ Object.assign(reminder, payload)
+ Vue.set(state.reminders, payload.id, reminder)
+ }
+ },
+
+ delete(state, payload) {
+ if (payload in state.reminders) {
+ Vue.delete(state.reminders, payload)
+ }
+ },
+}
diff --git a/src/store/gui/reminders/types.ts b/src/store/gui/reminders/types.ts
new file mode 100644
index 000000000..7edc70ca8
--- /dev/null
+++ b/src/store/gui/reminders/types.ts
@@ -0,0 +1,16 @@
+export interface GuiRemindersState {
+ reminders: {
+ [key: string]: GuiRemindersStateReminder
+ }
+}
+
+export interface GuiRemindersStateReminder {
+ id: string
+ name: string
+ start_total_print_time: number
+ time_delta: number
+ repeating: boolean
+ snooze_print_hours_timestamps: number[]
+ snooze_epoch_timestamps: number[]
+ remaining_print_time?: number
+}
diff --git a/src/store/gui/types.ts b/src/store/gui/types.ts
index 1cd61ea6e..dffa3a873 100644
--- a/src/store/gui/types.ts
+++ b/src/store/gui/types.ts
@@ -168,6 +168,8 @@ export interface GuiState {
hidePrintStatus: string[]
hideColums: string[]
selectedJobs: ServerHistoryStateJob[]
+ showMaintenanceEntries: boolean
+ showPrintJobs: boolean
}
jobqueue: {
countPerPage: number
diff --git a/src/store/server/actions.ts b/src/store/server/actions.ts
index 93aeb4ce3..8fb762b75 100644
--- a/src/store/server/actions.ts
+++ b/src/store/server/actions.ts
@@ -63,6 +63,10 @@ export const actions: ActionTree = {
dispatch('socket/addInitModule', 'gui/webcam/init', { root: true })
dispatch('gui/webcams/init', null, { root: true })
}
+ if (payload.namespaces?.includes('maintenance')) {
+ dispatch('socket/addInitModule', 'gui/maintenance/init', { root: true })
+ dispatch('gui/maintenance/init', null, { root: true })
+ } else dispatch('gui/maintenance/initDb', null, { root: true })
commit('saveDbNamespaces', payload.namespaces)
diff --git a/src/store/server/history/getters.ts b/src/store/server/history/getters.ts
index 593ab593e..e6ce7d53c 100644
--- a/src/store/server/history/getters.ts
+++ b/src/store/server/history/getters.ts
@@ -1,14 +1,25 @@
import { GetterTree } from 'vuex'
import {
+ HistoryListRowJob,
ServerHistoryState,
ServerHistoryStateAllPrintStatusEntry,
ServerHistoryStateJob,
} from '@/store/server/history/types'
import { mdiAlertOutline, mdiCheckboxMarkedCircleOutline, mdiCloseCircleOutline, mdiProgressClock } from '@mdi/js'
import i18n from '@/plugins/i18n'
+import { HistoryListRowMaintenance } from '@/store/gui/maintenance/types'
+
+// I don't know why I cannot import the type from the HistoryListPanel, that's why I have to define it here again
+type HistoryListPanelRow = HistoryListRowJob | HistoryListRowMaintenance
// eslint-disable-next-line
export const getters: GetterTree = {
+ getSelectedJobs: (state, getters, rootState): ServerHistoryStateJob[] => {
+ const entries: HistoryListPanelRow[] = rootState.gui.view.history.selectedJobs ?? []
+
+ return entries.filter((entry) => entry.type === 'job') as ServerHistoryStateJob[]
+ },
+
getTotalPrintTime(state) {
let output = 0
@@ -150,11 +161,11 @@ export const getters: GetterTree = {
getSelectedPrintStatusArray(state, getters, rootState) {
const output: ServerHistoryStateAllPrintStatusEntry[] = []
- rootState.gui.view.history.selectedJobs.forEach((current: ServerHistoryStateJob) => {
+ getters.getSelectedJobs.forEach((current: ServerHistoryStateJob) => {
const index = output.findIndex((element) => element.name === current.status)
if (index !== -1) output[index].value += 1
else {
- const displayName = i18n.te(`History.StatusValues.${current.status}`, 'en')
+ const displayName = i18n.te(`History.StatusValues.${current.status}`, 'en').toString()
? i18n.t(`History.StatusValues.${current.status}`).toString()
: current.status
const itemStyle = {
@@ -192,7 +203,7 @@ export const getters: GetterTree = {
return output
},
- getFilamentUsageArray(state, getters, rootState) {
+ getFilamentUsageArray(state, getters) {
// eslint-disable-next-line
const output: any = []
const startDate = new Date()
@@ -202,9 +213,9 @@ export const getters: GetterTree = {
let jobsFiltered = [
...state.jobs.filter((job) => new Date(job.start_time * 1000) >= startDate && job.filament_used > 0),
]
- if (rootState.gui.view.history.selectedJobs.length)
+ if (getters.getSelectedJobs.length)
jobsFiltered = [
- ...rootState.gui.view.history.selectedJobs.filter(
+ ...getters.getSelectedJobs.filter(
(job: ServerHistoryStateJob) =>
new Date(job.start_time * 1000) >= startDate && job.filament_used > 0
),
@@ -239,9 +250,9 @@ export const getters: GetterTree = {
let jobsFiltered = [
...state.jobs.filter((job) => new Date(job.start_time * 1000) >= startDate && job.status === 'completed'),
]
- if (rootState.gui.view.history.selectedJobs.length)
+ if (getters.getSelectedJobs.length)
jobsFiltered = [
- ...rootState.gui.view.history.selectedJobs.filter(
+ ...getters.getSelectedJobs.filter(
(job: ServerHistoryStateJob) =>
new Date(job.start_time * 1000) >= startDate && job.status === 'completed'
),
@@ -292,7 +303,7 @@ export const getters: GetterTree = {
// find jobs via metadata
const jobs = state.jobs.filter((job) => {
- return job.metadata?.size === filesize && Math.round(job.metadata?.modified * 1000) === modified
+ return job.metadata?.size === filesize && Math.round((job.metadata?.modified ?? 0) * 1000) === modified
})
if (jobs.length) return jobs
if (job_id) return jobs.filter((job) => job.job_id === job_id)
@@ -303,7 +314,7 @@ export const getters: GetterTree = {
getPrintStatusByFilename: (state) => (filename: string, modified: number) => {
if (state.jobs.length) {
const job = state.jobs.find((job) => {
- return job.filename === filename && Math.round(job.metadata?.modified * 1000) === modified
+ return job.filename === filename && Math.round((job.metadata?.modified ?? 0) * 1000) === modified
})
return job?.status ?? ''
@@ -354,7 +365,7 @@ export const getters: GetterTree = {
}
},
- getFilterdJobList: (state, getters, rootState) => {
+ getFilteredJobList: (state, getters, rootState) => {
const hideStatus = rootState.gui.view.history.hidePrintStatus
return state.jobs.filter((job: ServerHistoryStateJob) => {
diff --git a/src/store/server/history/types.ts b/src/store/server/history/types.ts
index 90b5ce7e8..4d8cd56fc 100644
--- a/src/store/server/history/types.ts
+++ b/src/store/server/history/types.ts
@@ -1,3 +1,5 @@
+import { FileStateFileThumbnail } from '@/store/files/types'
+
export interface ServerHistoryState {
jobs: ServerHistoryStateJob[]
job_totals: {
@@ -18,7 +20,31 @@ export interface ServerHistoryStateJob {
filament_used: number
filename: string
// eslint-disable-next-line
- metadata: any
+ metadata: {
+ print_start_time?: number
+ job_id?: number
+ size?: number
+ slicer?: string
+ slicer_version?: string
+ layer_count?: number
+ layer_height?: number
+ first_layer_height?: number
+ object_height?: number
+ filament_total?: number
+ filament_weight_total?: number
+ estimated_time?: number
+ thumbnails?: FileStateFileThumbnail[]
+ first_layer_bed_temp?: number
+ first_layer_extr_temp?: number
+ gcode_start_byte?: number
+ gcode_end_byte?: number
+ filename?: string
+ filesize?: number
+ modified?: number
+ uuid?: string
+ nozzle_diameter?: number
+ [key: string]: any
+ }
note?: string
print_duration: number
status: string
@@ -26,6 +52,11 @@ export interface ServerHistoryStateJob {
total_duration: number
}
+export interface HistoryListRowJob extends ServerHistoryStateJob {
+ type: 'job'
+ select_id: string
+}
+
export interface ServerHistoryStateAllPrintStatusEntry {
name: string
displayName: string