Feature: system status (#5743)

This commit is contained in:
shamoon 2024-03-04 09:26:25 -08:00 committed by GitHub
parent 23ceb2a5ec
commit f6084acfc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1129 additions and 83 deletions

View File

@ -59,7 +59,8 @@ ARG GS_VERSION=10.02.1
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
# Ignore warning from Whitenoise
PYTHONWARNINGS="ignore:::django.http.response:517"
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1
#
# Begin installation and configuration

View File

@ -77,7 +77,9 @@
"scripts": [],
"allowedCommonJsDependencies": [
"pdfjs-dist",
"pdfjs-dist/web/pdf_viewer"
"pdfjs-dist/web/pdf_viewer",
"filesize",
"file-saver"
],
"vendorChunk": true,
"extractLicenses": false,

View File

@ -458,7 +458,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">354</context>
<context context-type="linenumber">375</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
@ -600,7 +600,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">342</context>
<context context-type="linenumber">363</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
@ -622,6 +622,10 @@
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">10</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
<context context-type="linenumber">15</context>
@ -667,7 +671,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">293</context>
<context context-type="linenumber">314</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
@ -693,238 +697,249 @@
<source>Start tour</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">7</context>
<context context-type="linenumber">8</context>
</context-group>
</trans-unit>
<trans-unit id="3276228498925657259" datatype="html">
<source>System Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
</trans-unit>
<trans-unit id="4798013226763881638" datatype="html">
<source>Open Django Admin</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">30</context>
</context-group>
</trans-unit>
<trans-unit id="6439365426343089851" datatype="html">
<source>General</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">39</context>
</context-group>
</trans-unit>
<trans-unit id="8671234314555525900" datatype="html">
<source>Appearance</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">21</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="3777637051272512093" datatype="html">
<source>Display language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">25</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="53523152145406584" datatype="html">
<source>You need to reload the page after applying a new language.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">38</context>
<context context-type="linenumber">59</context>
</context-group>
</trans-unit>
<trans-unit id="3766032098416558788" datatype="html">
<source>Date display</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">46</context>
<context context-type="linenumber">67</context>
</context-group>
</trans-unit>
<trans-unit id="3733378544613473393" datatype="html">
<source>Date format</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">63</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit id="3407788781115661841" datatype="html">
<source>Short: <x id="INTERPOLATION" equiv-text="{{today | customDate:&apos;shortDate&apos;:null:computedDateLocale}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">69,70</context>
<context context-type="linenumber">90,91</context>
</context-group>
</trans-unit>
<trans-unit id="6290748171049664628" datatype="html">
<source>Medium: <x id="INTERPOLATION" equiv-text="{{today | customDate:&apos;mediumDate&apos;:null:computedDateLocale}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">73,74</context>
<context context-type="linenumber">94,95</context>
</context-group>
</trans-unit>
<trans-unit id="7189855711197998347" datatype="html">
<source>Long: <x id="INTERPOLATION" equiv-text="{{today | customDate:&apos;longDate&apos;:null:computedDateLocale}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">77,78</context>
<context context-type="linenumber">98,99</context>
</context-group>
</trans-unit>
<trans-unit id="8939587804990976924" datatype="html">
<source>Items per page</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="8028535997917730106" datatype="html">
<source>Document editor</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">101</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="6708098108196142028" datatype="html">
<source>Use PDF viewer provided by the browser</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">105</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="9003921625412907981" datatype="html">
<source>This is usually faster for displaying large PDF documents, but it might not work on some browsers.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">105</context>
<context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="3982403428275430291" datatype="html">
<source>Sidebar</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">133</context>
</context-group>
</trans-unit>
<trans-unit id="4608457133854405683" datatype="html">
<source>Use &apos;slim&apos; sidebar (icons only)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit id="1356890996281769972" datatype="html">
<source>Dark mode</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">123</context>
<context context-type="linenumber">144</context>
</context-group>
</trans-unit>
<trans-unit id="4913823100518391922" datatype="html">
<source>Use system settings</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">147</context>
</context-group>
</trans-unit>
<trans-unit id="5782828784040423650" datatype="html">
<source>Enable dark mode</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">127</context>
<context context-type="linenumber">148</context>
</context-group>
</trans-unit>
<trans-unit id="6336642923114460405" datatype="html">
<source>Invert thumbnails in dark mode</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">128</context>
<context context-type="linenumber">149</context>
</context-group>
</trans-unit>
<trans-unit id="7983234071833154796" datatype="html">
<source>Theme Color</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">155</context>
</context-group>
</trans-unit>
<trans-unit id="7808756054397155068" datatype="html">
<source>Reset</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">141</context>
<context context-type="linenumber">162</context>
</context-group>
</trans-unit>
<trans-unit id="8901931207592071833" datatype="html">
<source>Update checking</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">146</context>
<context context-type="linenumber">167</context>
</context-group>
</trans-unit>
<trans-unit id="7141691772243630313" datatype="html">
<source> Update checking works by pinging the public <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;"/>GitHub API<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> for the latest release to determine whether a new version is available.<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/> Actual updating of the app must still be performed manually. </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">150,153</context>
<context context-type="linenumber">171,174</context>
</context-group>
</trans-unit>
<trans-unit id="5489945693955857309" datatype="html">
<source><x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="&gt;"/>No tracking data is collected by the app in any way.<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">155,157</context>
<context context-type="linenumber">176,178</context>
</context-group>
</trans-unit>
<trans-unit id="5070799004079086984" datatype="html">
<source>Enable update checking</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">157</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="908152367861642592" datatype="html">
<source>Document editing</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">161</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit id="2959590948110714366" datatype="html">
<source>Automatically remove inbox tag(s) on save</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">165</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="8508424367627989968" datatype="html">
<source>Bulk editing</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">169</context>
<context context-type="linenumber">190</context>
</context-group>
</trans-unit>
<trans-unit id="8158899674926420054" datatype="html">
<source>Show confirmation dialogs</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">173</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="6906812245033969309" datatype="html">
<source>Deleting documents will always ask for confirmation.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">173</context>
<context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="290238406234356122" datatype="html">
<source>Apply on close</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">174</context>
<context context-type="linenumber">195</context>
</context-group>
</trans-unit>
<trans-unit id="8104421162933956065" datatype="html">
<source>Notes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">178</context>
<context context-type="linenumber">199</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -939,14 +954,14 @@
<source>Enable notes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">182</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit id="7314814725704332646" datatype="html">
<source>Permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">211</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html</context>
@ -1001,28 +1016,28 @@
<source>Default Permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">193</context>
<context context-type="linenumber">214</context>
</context-group>
</trans-unit>
<trans-unit id="8222269449891326545" datatype="html">
<source> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">197,199</context>
<context context-type="linenumber">218,220</context>
</context-group>
</trans-unit>
<trans-unit id="4292903881380648974" datatype="html">
<source>Default Owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">225</context>
</context-group>
</trans-unit>
<trans-unit id="734147282056744882" datatype="html">
<source>Objects without an owner can be viewed and edited by all users</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">208</context>
<context context-type="linenumber">229</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
@ -1033,18 +1048,18 @@
<source>Default View Permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">213</context>
<context context-type="linenumber">234</context>
</context-group>
</trans-unit>
<trans-unit id="2191775412581217688" datatype="html">
<source>Users:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">218</context>
<context context-type="linenumber">239</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">245</context>
<context context-type="linenumber">266</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
@ -1067,11 +1082,11 @@
<source>Groups:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">228</context>
<context context-type="linenumber">249</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">255</context>
<context context-type="linenumber">276</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
@ -1094,14 +1109,14 @@
<source>Default Edit Permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">240</context>
<context context-type="linenumber">261</context>
</context-group>
</trans-unit>
<trans-unit id="3728984448750213892" datatype="html">
<source>Edit permissions also grant viewing permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">264</context>
<context context-type="linenumber">285</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
@ -1116,56 +1131,56 @@
<source>Notifications</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">272</context>
<context context-type="linenumber">293</context>
</context-group>
</trans-unit>
<trans-unit id="8545554728558600606" datatype="html">
<source>Document processing</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">275</context>
<context context-type="linenumber">296</context>
</context-group>
</trans-unit>
<trans-unit id="3656786776644872398" datatype="html">
<source>Show notifications when new documents are detected</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">279</context>
<context context-type="linenumber">300</context>
</context-group>
</trans-unit>
<trans-unit id="6057053428592387613" datatype="html">
<source>Show notifications when document processing completes successfully</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">280</context>
<context context-type="linenumber">301</context>
</context-group>
</trans-unit>
<trans-unit id="370315664367425513" datatype="html">
<source>Show notifications when document processing fails</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">281</context>
<context context-type="linenumber">302</context>
</context-group>
</trans-unit>
<trans-unit id="6838309441164918531" datatype="html">
<source>Suppress notifications on dashboard</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">282</context>
<context context-type="linenumber">303</context>
</context-group>
</trans-unit>
<trans-unit id="2741919327232918179" datatype="html">
<source>This will suppress all messages about document processing status on the dashboard.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">282</context>
<context context-type="linenumber">303</context>
</context-group>
</trans-unit>
<trans-unit id="472206565520537964" datatype="html">
<source>Saved views</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">290</context>
<context context-type="linenumber">311</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
@ -1176,14 +1191,14 @@
<source>Show warning when closing saved views with unsaved changes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">296</context>
<context context-type="linenumber">317</context>
</context-group>
</trans-unit>
<trans-unit id="2123659921722214537" datatype="html">
<source>Views</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">300</context>
<context context-type="linenumber">321</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -1194,7 +1209,7 @@
<source>Name</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">306</context>
<context context-type="linenumber">327</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
@ -1301,14 +1316,14 @@
<source> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;visually-hidden&quot;&gt;"/>Appears on<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">310,311</context>
<context context-type="linenumber">331,332</context>
</context-group>
</trans-unit>
<trans-unit id="4104807402967139762" datatype="html">
<source>Show on dashboard</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">313</context>
<context context-type="linenumber">334</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -1319,7 +1334,7 @@
<source>Show in sidebar</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">317</context>
<context context-type="linenumber">338</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -1330,7 +1345,7 @@
<source>Actions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">321</context>
<context context-type="linenumber">342</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
@ -1393,7 +1408,7 @@
<source>Delete</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">324</context>
<context context-type="linenumber">345</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context>
@ -1504,28 +1519,28 @@
<source>No saved views defined.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">336</context>
<context context-type="linenumber">357</context>
</context-group>
</trans-unit>
<trans-unit id="6839066544204061364" datatype="html">
<source>Use system language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">51</context>
<context context-type="linenumber">61</context>
</context-group>
</trans-unit>
<trans-unit id="7729897675462249787" datatype="html">
<source>Use date format of display language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">54</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="1235706724900303689" datatype="html">
<source>Error retrieving users</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">159</context>
<context context-type="linenumber">183</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
@ -1536,7 +1551,7 @@
<source>Error retrieving groups</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">178</context>
<context context-type="linenumber">202</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
@ -1547,35 +1562,35 @@
<source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">380</context>
<context context-type="linenumber">415</context>
</context-group>
</trans-unit>
<trans-unit id="7217000812750597833" datatype="html">
<source>Settings were saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">506</context>
<context context-type="linenumber">541</context>
</context-group>
</trans-unit>
<trans-unit id="525012668859298131" datatype="html">
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">510</context>
<context context-type="linenumber">545</context>
</context-group>
</trans-unit>
<trans-unit id="8491974984518503778" datatype="html">
<source>Reload now</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">511</context>
<context context-type="linenumber">546</context>
</context-group>
</trans-unit>
<trans-unit id="3011185103048412841" datatype="html">
<source>An error occurred while saving settings.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">521</context>
<context context-type="linenumber">556</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
@ -1586,7 +1601,7 @@
<source>Error while storing settings on server.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">555</context>
<context context-type="linenumber">590</context>
</context-group>
</trans-unit>
<trans-unit id="2991443309752293110" datatype="html">
@ -4075,6 +4090,10 @@
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
<context context-type="linenumber">5</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">45</context>
</context-group>
</trans-unit>
<trans-unit id="1230154438678955604" datatype="html">
<source>Change</source>
@ -4139,6 +4158,10 @@
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="595732867213154214" datatype="html">
<source>Regenerate auth token</source>
@ -4370,8 +4393,68 @@
<context context-type="linenumber">151</context>
</context-group>
</trans-unit>
<trans-unit id="9180110319941008393" datatype="html">
<source>Environment</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
</trans-unit>
<trans-unit id="5973078531069712831" datatype="html">
<source>Paperless-ngx Version</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit>
<trans-unit id="6269705781013540301" datatype="html">
<source>Install Type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="7962174670320694437" datatype="html">
<source>Server OS</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="2903495470702110128" datatype="html">
<source>Media Storage</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="2571831784751497241" datatype="html">
<source>available</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="6489441800790477240" datatype="html">
<source>total</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">31</context>
</context-group>
</trans-unit>
<trans-unit id="4198035112366277884" datatype="html">
<source>Database</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="5611592591303869712" datatype="html">
<source>Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context>
<context context-type="linenumber">26</context>
@ -4381,6 +4464,76 @@
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="2256165083739630668" datatype="html">
<source>Migration Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="7881311375431899727" datatype="html">
<source>Latest Migration</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="4632965004151576238" datatype="html">
<source>Pending Migrations</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">66</context>
</context-group>
</trans-unit>
<trans-unit id="6904866445262015585" datatype="html">
<source>Tasks</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit id="6911698235105017958" datatype="html">
<source>Redis Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">87</context>
</context-group>
</trans-unit>
<trans-unit id="5349496739889768589" datatype="html">
<source>Celery Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="31377277941774469" datatype="html">
<source>Search Index</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">105</context>
</context-group>
</trans-unit>
<trans-unit id="4089509911694721896" datatype="html">
<source>Last Updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="46628344485199198" datatype="html">
<source>Classifier</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">121</context>
</context-group>
</trans-unit>
<trans-unit id="6096684179126491743" datatype="html">
<source>Last Trained</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit id="6732151329960766506" datatype="html">
<source>Copy Raw Error</source>
<context-group purpose="location">

View File

@ -29,6 +29,7 @@
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.1.0",
"ngx-file-drop": "^16.0.0",
"ngx-filesize": "^3.0.3",
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
"pdfjs-dist": "^3.11.174",
"rxjs": "^7.8.1",
@ -9844,6 +9845,15 @@
"node": ">=10"
}
},
"node_modules/filesize": {
"version": "9.0.11",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-9.0.11.tgz",
"integrity": "sha512-gTAiTtI0STpKa5xesyTA9hA3LX4ga8sm2nWRcffEa1L/5vQwb4mj2MdzMkoHoGv4QzfDshQZuYscQSf8c4TKOA==",
"peer": true,
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -14105,6 +14115,19 @@
"@angular/core": ">=14.0.0"
}
},
"node_modules/ngx-filesize": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ngx-filesize/-/ngx-filesize-3.0.3.tgz",
"integrity": "sha512-qqP2p4WbbF7R+NXC9NqRQdAfWfMAYJ2Ijf4ezRCq7j3tPY6ybSP9AZ3FY1U7/95n1hmOJ2U5oY+oFb7LhHQRBw==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">= 14.2.0 < 18.0.0",
"@angular/core": ">= 14.2.0 < 18.0.0",
"filesize": ">= 6.0.0 < 10.0.0"
}
},
"node_modules/ngx-ui-tour-core": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-12.0.1.tgz",

View File

@ -31,6 +31,7 @@
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.1.0",
"ngx-file-drop": "^16.0.0",
"ngx-filesize": "^3.0.3",
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
"pdfjs-dist": "^3.11.174",
"rxjs": "^7.8.1",

View File

@ -114,7 +114,10 @@ import { FileComponent } from './components/common/input/file/file.component'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
import { NgxFilesizeModule } from 'ngx-filesize'
import {
airplane,
archive,
arrowCounterclockwise,
arrowDown,
@ -129,12 +132,14 @@ import {
boxes,
calendar,
calendarEvent,
cardChecklist,
caretDown,
caretUp,
chatLeftText,
check,
check2All,
checkAll,
checkCircleFill,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
@ -148,7 +153,9 @@ import {
doorOpen,
download,
envelope,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
eye,
fileEarmark,
fileEarmarkCheck,
@ -200,6 +207,7 @@ import {
} from 'ngx-bootstrap-icons'
const icons = {
airplane,
archive,
arrowCounterclockwise,
arrowDown,
@ -214,12 +222,14 @@ const icons = {
boxes,
calendar,
calendarEvent,
cardChecklist,
caretDown,
caretUp,
chatLeftText,
check,
check2All,
checkAll,
checkCircleFill,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
@ -233,7 +243,9 @@ const icons = {
doorOpen,
download,
envelope,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
eye,
fileEarmark,
fileEarmarkCheck,
@ -445,6 +457,7 @@ function initializeApp(settings: SettingsService) {
FileComponent,
ConfirmButtonComponent,
MonetaryComponent,
SystemStatusDialogComponent,
],
imports: [
BrowserModule,
@ -459,6 +472,7 @@ function initializeApp(settings: SettingsService) {
TourNgBootstrapModule,
DragDropModule,
NgxBootstrapIconsModule.pick(icons),
NgxFilesizeModule,
],
providers: [
{

View File

@ -4,10 +4,31 @@
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
<i-bs class="me-1" name="airplane"></i-bs>&nbsp;<ng-container i18n>Start tour</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary position-relative ms-5" (click)="showSystemStatus()"
[disabled]="!systemStatus"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
@if (!systemStatus) {
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
} @else {
<i-bs class="me-2" name="card-checklist"></i-bs>
@if (systemStatusHasErrors) {
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
</span>
} @else {
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
</span>
}
}
<ng-container i18n>System Status</ng-container>
</button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container>
<i-bs name="arrow-up-right"></i-bs>
&nbsp;<i-bs name="arrow-up-right"></i-bs>
</a>
</pngx-page-header>

View File

@ -9,6 +9,8 @@ import {
NgbModule,
NgbAlertModule,
NgbNavLink,
NgbModal,
NgbModalModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { of, throwError } from 'rxjs'
@ -39,6 +41,13 @@ import { SettingsComponent } from './settings.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
SystemStatus,
InstallType,
SystemStatusItemStatus,
} from 'src/app/data/system-status'
const savedViews = [
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
@ -65,6 +74,8 @@ describe('SettingsComponent', () => {
let userService: UserService
let permissionsService: PermissionsService
let groupService: GroupService
let modalService: NgbModal
let systemStatusService: SystemStatusService
beforeEach(async () => {
TestBed.configureTestingModule({
@ -96,6 +107,7 @@ describe('SettingsComponent', () => {
NgbAlertModule,
NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
NgbModalModule,
],
}).compileComponents()
@ -107,6 +119,8 @@ describe('SettingsComponent', () => {
settingsService.currentUser = users[0]
userService = TestBed.inject(UserService)
permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal)
systemStatusService = TestBed.inject(SystemStatusService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
@ -372,4 +386,54 @@ describe('SettingsComponent', () => {
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
})
it('should load system status on initialize, show errors if needed', () => {
const status: SystemStatus = {
pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit',
install_type: InstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 },
database: {
type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3',
status: SystemStatusItemStatus.ERROR,
error: null,
migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
unapplied_migrations: [],
},
},
tasks: {
redis_url: 'redis://localhost:6379',
redis_status: SystemStatusItemStatus.ERROR,
redis_error:
'Error 61 connecting to localhost:6379. Connection refused.',
celery_status: SystemStatusItemStatus.ERROR,
index_status: SystemStatusItemStatus.OK,
index_last_modified: new Date().toISOString(),
index_error: null,
classifier_status: SystemStatusItemStatus.OK,
classifier_last_trained: new Date().toISOString(),
classifier_error: null,
},
}
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
completeSetup()
expect(component['systemStatus']).toEqual(status) // private
expect(component.systemStatusHasErrors).toBeTruthy()
// coverage
component['systemStatus'].database.status = SystemStatusItemStatus.OK
component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
expect(component.systemStatusHasErrors).toBeFalsy()
})
it('should open system status dialog', () => {
const modalOpenSpy = jest.spyOn(modalService, 'open')
completeSetup()
component.showSystemStatus()
expect(modalOpenSpy).toHaveBeenCalledWith(SystemStatusDialogComponent, {
size: 'xl',
})
})
})

View File

@ -9,7 +9,11 @@ import {
} from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
import {
NgbModal,
NgbModalRef,
NgbNavChangeEvent,
} from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import {
@ -40,6 +44,12 @@ import {
} from 'src/app/services/settings.service'
import { ToastService, Toast } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
SystemStatusItemStatus,
SystemStatus,
} from 'src/app/data/system-status'
enum SettingsNavIDs {
General = 1,
@ -111,6 +121,18 @@ export class SettingsComponent
users: User[]
groups: Group[]
private systemStatus: SystemStatus
get systemStatusHasErrors(): boolean {
return (
this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
)
}
get computedDateLocale(): string {
return (
this.settingsForm.value.dateLocale ||
@ -131,7 +153,9 @@ export class SettingsComponent
private usersService: UserService,
private groupsService: GroupService,
private router: Router,
public permissionsService: PermissionsService
public permissionsService: PermissionsService,
private modalService: NgbModal,
private systemStatusService: SystemStatusService
) {
super()
this.settings.settingsSaved.subscribe(() => {
@ -360,6 +384,17 @@ export class SettingsComponent
// prevents loss of unsaved changes
this.settingsForm.patchValue(currentFormValue)
}
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.Admin
)
) {
this.systemStatusService.get().subscribe((status) => {
this.systemStatus = status
})
}
}
private emptyGroup(group: FormGroup) {
@ -565,4 +600,14 @@ export class SettingsComponent
clearThemeColor() {
this.settingsForm.get('themeColor').patchValue('')
}
showSystemStatus() {
const modal: NgbModalRef = this.modalService.open(
SystemStatusDialogComponent,
{
size: 'xl',
}
)
modal.componentInstance.status = this.systemStatus
}
}

View File

@ -0,0 +1,154 @@
<div class="modal-header">
<h5 class="modal-title" id="modal-basic-title" i18n>System Status</h5>
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body">
@if (!status) {
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
<div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
</div>
} @else {
<div class="row row-cols-1 row-cols-md-3 g-3">
<div class="col">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="card-title mb-0" i18n>Environment</h5>
</div>
<div class="card-body">
<dl class="card-text">
<dt i18n>Paperless-ngx Version</dt>
<dd>{{status.pngx_version}}</dd>
<dt i18n>Install Type</dt>
<dd>{{status.install_type}}</dd>
<dt i18n>Server OS</dt>
<dd>{{status.server_os}}</dd>
<dt i18n>Media Storage</dt>
<dd>
<ngb-progressbar style="height: 4px;" class="mt-2 mb-1" type="primary" [max]="status.storage.total" [value]="status.storage.total - status.storage.available"></ngb-progressbar>
<span class="small">{{status.storage.available | filesize}} <ng-container i18n>available</ng-container> ({{status.storage.total | filesize}} <ng-container i18n>total</ng-container>)</span>
</dd>
</dl>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="card-title mb-0" i18n>Database</h5>
</div>
<div class="card-body">
<dl class="card-text">
<dt i18n>Type</dt>
<dd>{{status.database.type}}</dd>
<dt i18n>Status</dt>
<dd class="d-flex align-items-center">
{{status.database.status}}
@if (status.database.status === 'OK') {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.database.url}}" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.database.url}}: {{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<dt i18n>Migration Status</dt>
<dd class="d-flex align-items-center">
@if (status.database.migration_status.unapplied_migrations.length === 0) {
<ng-container>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
}
<ng-template #migrationStatus>
<h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
@if (status.database.migration_status.unapplied_migrations.length > 0) {
<h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
<ul>
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
<li class="font-monospace small">{{migration}}</li>
}
</ul>
}
</ng-template>
</dd>
</dl>
</div>
</div>
</div>
<div class="col">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="card-title mb-0" i18n>Tasks</h5>
</div>
<div class="card-body">
<dl class="card-text">
<dt i18n>Redis Status</dt>
<dd class="d-flex align-items-center">
{{status.tasks.redis_status}}
@if (status.tasks.redis_status === 'OK') {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}: {{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<dt i18n>Celery Status</dt>
<dd class="d-flex align-items-center">
{{status.tasks.celery_status}}
@if (status.tasks.celery_status === 'OK') {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
}
</dd>
<dt i18n>Search Index</dt>
<dd class="d-flex align-items-center">
{{status.tasks.index_status}}
@if (status.tasks.index_status === 'OK') {
@if (isStale(status.tasks.index_last_modified)) {
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
}
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.index_error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<ng-template #indexStatus>
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
</ng-template>
<dt i18n>Classifier</dt>
<dd class="d-flex align-items-center">
{{status.tasks.classifier_status}}
@if (status.tasks.classifier_status === 'OK') {
@if (isStale(status.tasks.classifier_last_trained)) {
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
}
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.classifier_error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<ng-template #classifierStatus>
<h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
</ng-template>
</dl>
</div>
</div>
</div>
</div>
}
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-outline-secondary" (click)="copy()">
@if (!copied) {
<i-bs name="clipboard-fill"></i-bs>&nbsp;
}
@if (copied) {
<i-bs name="clipboard-check-fill"></i-bs>&nbsp;
}
<ng-container i18n>Copy</ng-container>
</button>
</div>

View File

@ -0,0 +1,103 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import {
NgbActiveModal,
NgbModalModule,
NgbPopoverModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
import { SystemStatusDialogComponent } from './system-status-dialog.component'
import {
SystemStatusItemStatus,
InstallType,
SystemStatus,
} from 'src/app/data/system-status'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NgxFilesizeModule } from 'ngx-filesize'
const status: SystemStatus = {
pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit',
install_type: InstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 },
database: {
type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3',
status: SystemStatusItemStatus.ERROR,
error: null,
migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
unapplied_migrations: [],
},
},
tasks: {
redis_url: 'redis://localhost:6379',
redis_status: SystemStatusItemStatus.ERROR,
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
celery_status: SystemStatusItemStatus.ERROR,
index_status: SystemStatusItemStatus.OK,
index_last_modified: new Date().toISOString(),
index_error: null,
classifier_status: SystemStatusItemStatus.OK,
classifier_last_trained: new Date().toISOString(),
classifier_error: null,
},
}
describe('SystemStatusDialogComponent', () => {
let component: SystemStatusDialogComponent
let fixture: ComponentFixture<SystemStatusDialogComponent>
let clipboard: Clipboard
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SystemStatusDialogComponent],
providers: [NgbActiveModal],
imports: [
NgbModalModule,
ClipboardModule,
HttpClientTestingModule,
NgxBootstrapIconsModule.pick(allIcons),
NgxFilesizeModule,
NgbPopoverModule,
NgbProgressbarModule,
],
}).compileComponents()
fixture = TestBed.createComponent(SystemStatusDialogComponent)
component = fixture.componentInstance
component.status = status
clipboard = TestBed.inject(Clipboard)
fixture.detectChanges()
})
it('should close the active modal', () => {
const closeSpy = jest.spyOn(component.activeModal, 'close')
component.close()
expect(closeSpy).toHaveBeenCalled()
})
it('should copy the system status to clipboard', fakeAsync(() => {
jest.spyOn(clipboard, 'copy')
component.copy()
expect(clipboard.copy).toHaveBeenCalledWith(
JSON.stringify(component.status)
)
expect(component.copied).toBeTruthy()
tick(3000)
expect(component.copied).toBeFalsy()
}))
it('should calculate if date is stale', () => {
const date = new Date()
date.setHours(date.getHours() - 25)
expect(component.isStale(date.toISOString())).toBeTruthy()
expect(component.isStale(date.toISOString(), 26)).toBeFalsy()
})
})

View File

@ -0,0 +1,39 @@
import { Component, Input } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SystemStatus } from 'src/app/data/system-status'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Clipboard } from '@angular/cdk/clipboard'
@Component({
selector: 'pngx-system-status-dialog',
templateUrl: './system-status-dialog.component.html',
styleUrl: './system-status-dialog.component.scss',
})
export class SystemStatusDialogComponent {
public status: SystemStatus
public copied: boolean = false
constructor(
public activeModal: NgbActiveModal,
private clipboard: Clipboard
) {}
public close() {
this.activeModal.close()
}
public copy() {
this.clipboard.copy(JSON.stringify(this.status))
this.copied = true
setTimeout(() => {
this.copied = false
}, 3000)
}
public isStale(dateStr: string, hours: number = 24): boolean {
const date = new Date(dateStr)
const now = new Date()
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
}
}

View File

@ -0,0 +1,41 @@
export enum InstallType {
Containerized = 'containerized',
BareMetal = 'bare-metal',
}
export enum SystemStatusItemStatus {
OK = 'OK',
ERROR = 'ERROR',
}
export interface SystemStatus {
pngx_version: string
server_os: string
install_type: InstallType
storage: {
total: number
available: number
}
database: {
type: string
url: string
status: SystemStatusItemStatus
error?: string
migration_status: {
latest_migration: string
unapplied_migrations: string[]
}
}
tasks: {
redis_url: string
redis_status: SystemStatusItemStatus
redis_error: string
celery_status: SystemStatusItemStatus
index_status: SystemStatusItemStatus
index_last_modified: string // ISO date string
index_error: string
classifier_status: SystemStatusItemStatus
classifier_last_trained: string // ISO date string
classifier_error: string
}
}

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing'
import { SystemStatusService } from './system-status.service'
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { environment } from 'src/environments/environment'
describe('SystemStatusService', () => {
let httpTestingController: HttpTestingController
let service: SystemStatusService
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SystemStatusService],
imports: [HttpClientTestingModule],
})
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(SystemStatusService)
})
afterEach(() => {
httpTestingController.verify()
})
it('calls get status endpoint', () => {
service.get().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}status/`
)
expect(req.request.method).toEqual('GET')
})
})

View File

@ -0,0 +1,20 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { SystemStatus } from '../data/system-status'
import { environment } from 'src/environments/environment'
@Injectable({
providedIn: 'root',
})
export class SystemStatusService {
private endpoint = 'status'
constructor(private http: HttpClient) {}
get(): Observable<SystemStatus> {
return this.http.get<SystemStatus>(
`${environment.apiBaseUrl}${this.endpoint}/`
)
}
}

View File

@ -0,0 +1,186 @@
import os
from pathlib import Path
from unittest import mock
from django.contrib.auth.models import User
from django.test import override_settings
from rest_framework import status
from rest_framework.test import APITestCase
from documents.classifier import DocumentClassifier
from documents.classifier import load_classifier
from paperless import version
class TestSystemStatus(APITestCase):
ENDPOINT = "/api/status/"
def setUp(self):
self.user = User.objects.create_superuser(
username="temp_admin",
)
def test_system_status(self):
"""
GIVEN:
- A user is logged in
WHEN:
- The user requests the system status
THEN:
- The response contains relevant system status information
"""
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["pngx_version"], version.__full_version_str__)
self.assertIsNotNone(response.data["server_os"])
self.assertEqual(response.data["install_type"], "bare-metal")
self.assertIsNotNone(response.data["storage"]["total"])
self.assertIsNotNone(response.data["storage"]["available"])
self.assertEqual(response.data["database"]["type"], "sqlite")
self.assertIsNotNone(response.data["database"]["url"])
self.assertEqual(response.data["database"]["status"], "OK")
self.assertIsNone(response.data["database"]["error"])
self.assertIsNotNone(response.data["database"]["migration_status"])
self.assertEqual(response.data["tasks"]["redis_url"], "redis://localhost:6379")
self.assertEqual(response.data["tasks"]["redis_status"], "ERROR")
self.assertIsNotNone(response.data["tasks"]["redis_error"])
def test_system_status_insufficient_permissions(self):
"""
GIVEN:
- A user is not logged in or does not have permissions
WHEN:
- The user requests the system status
THEN:
- The response contains a 401 status code or a 403 status code
"""
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
normal_user = User.objects.create_user(username="normal_user")
self.client.force_login(normal_user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_system_status_container_detection(self):
"""
GIVEN:
- The application is running in a containerized environment
WHEN:
- The user requests the system status
THEN:
- The response contains the correct install type
"""
self.client.force_login(self.user)
os.environ["PNGX_CONTAINERIZED"] = "1"
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["install_type"], "docker")
os.environ["KUBERNETES_SERVICE_HOST"] = "http://localhost"
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.data["install_type"], "kubernetes")
@mock.patch("redis.Redis.execute_command")
def test_system_status_redis_ping(self, mock_ping):
"""
GIVEN:
- Redies ping returns True
WHEN:
- The user requests the system status
THEN:
- The response contains the correct redis status
"""
mock_ping.return_value = True
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["redis_status"], "OK")
@mock.patch("celery.app.control.Inspect.ping")
def test_system_status_celery_ping(self, mock_ping):
"""
GIVEN:
- Celery ping returns pong
WHEN:
- The user requests the system status
THEN:
- The response contains the correct celery status
"""
mock_ping.return_value = {"hostname": {"ok": "pong"}}
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["celery_status"], "OK")
@override_settings(INDEX_DIR=Path("/tmp/index"))
@mock.patch("whoosh.index.FileIndex.last_modified")
def test_system_status_index_ok(self, mock_last_modified):
"""
GIVEN:
- The index last modified time is set
WHEN:
- The user requests the system status
THEN:
- The response contains the correct index status
"""
mock_last_modified.return_value = 1707839087
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["index_status"], "OK")
self.assertIsNotNone(response.data["tasks"]["index_last_modified"])
@override_settings(INDEX_DIR="/tmp/index/")
@mock.patch("documents.index.open_index", autospec=True)
def test_system_status_index_error(self, mock_open_index):
"""
GIVEN:
- The index is not found
WHEN:
- The user requests the system status
THEN:
- The response contains the correct index status
"""
mock_open_index.return_value = None
mock_open_index.side_effect = Exception("Index error")
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
mock_open_index.assert_called_once()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["index_status"], "ERROR")
self.assertIsNotNone(response.data["tasks"]["index_error"])
@override_settings(DATA_DIR="/tmp/does_not_exist/data/")
def test_system_status_classifier_ok(self):
"""
GIVEN:
- The classifier is found
WHEN:
- The user requests the system status
THEN:
- The response contains the correct classifier status
"""
load_classifier()
test_classifier = DocumentClassifier()
test_classifier.save()
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["classifier_status"], "OK")
self.assertIsNone(response.data["tasks"]["classifier_error"])
def test_system_status_classifier_error(self):
"""
GIVEN:
- The classifier is not found
WHEN:
- The user requests the system status
THEN:
- The response contains an error classifier status
"""
with override_settings(MODEL_FILE="does_not_exist"):
self.client.force_login(self.user)
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["classifier_status"], "ERROR")
self.assertIsNotNone(response.data["tasks"]["classifier_error"])

View File

@ -2,6 +2,7 @@ import itertools
import json
import logging
import os
import platform
import re
import tempfile
import urllib
@ -13,8 +14,12 @@ from unicodedata import normalize
from urllib.parse import quote
import pathvalidate
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import User
from django.db import connections
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder
from django.db.models import Case
from django.db.models import Count
from django.db.models import IntegerField
@ -31,6 +36,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.timezone import make_aware
from django.utils.translation import get_language
from django.views import View
from django.views.decorators.cache import cache_control
@ -40,6 +46,7 @@ from django.views.generic import TemplateView
from django_filters.rest_framework import DjangoFilterBackend
from langdetect import detect
from packaging import version as packaging_version
from redis import Redis
from rest_framework import parsers
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
@ -61,6 +68,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.viewsets import ViewSet
from documents import bulk_edit
from documents import index
from documents.bulk_download import ArchiveOnlyStrategy
from documents.bulk_download import OriginalAndArchiveStrategy
from documents.bulk_download import OriginalsOnlyStrategy
@ -138,6 +146,7 @@ from documents.serialisers import WorkflowTriggerSerializer
from documents.signals import document_updated
from documents.tasks import consume_file
from paperless import version
from paperless.celery import app as celery_app
from paperless.config import GeneralConfig
from paperless.db import GnuPG
from paperless.views import StandardPagination
@ -1539,3 +1548,132 @@ class CustomFieldViewSet(ModelViewSet):
model = CustomField
queryset = CustomField.objects.all().order_by("-created")
class SystemStatusView(GenericAPIView, PassUserMixin):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
if not request.user.has_perm("admin.view_logentry"):
return HttpResponseForbidden("Insufficient permissions")
current_version = version.__full_version_str__
install_type = "bare-metal"
if os.environ.get("KUBERNETES_SERVICE_HOST") is not None:
install_type = "kubernetes"
elif os.environ.get("PNGX_CONTAINERIZED") == "1":
install_type = "docker"
db_conn = connections["default"]
db_url = db_conn.settings_dict["NAME"]
db_error = None
try:
db_conn.ensure_connection()
db_status = "OK"
loader = MigrationLoader(connection=db_conn)
all_migrations = [f"{app}.{name}" for app, name in loader.graph.nodes]
applied_migrations = [
f"{m.app}.{m.name}"
for m in MigrationRecorder.Migration.objects.all().order_by("id")
]
except Exception as e: # pragma: no cover
applied_migrations = []
db_status = "ERROR"
logger.exception(f"System status error connecting to database: {e}")
db_error = "Error connecting to database, check logs for more detail."
media_stats = os.statvfs(settings.MEDIA_ROOT)
redis_url = settings._CHANNELS_REDIS_URL
redis_error = None
with Redis.from_url(url=redis_url) as client:
try:
client.ping()
redis_status = "OK"
except Exception as e:
redis_status = "ERROR"
logger.exception(f"System status error connecting to redis: {e}")
redis_error = "Error connecting to redis, check logs for more detail."
try:
celery_ping = celery_app.control.inspect().ping()
first_worker_ping = celery_ping[next(iter(celery_ping.keys()))]
if first_worker_ping["ok"] == "pong":
celery_active = "OK"
except Exception:
celery_active = "ERROR"
index_error = None
try:
ix = index.open_index()
index_status = "OK"
index_last_modified = make_aware(
datetime.fromtimestamp(ix.last_modified()),
)
except Exception as e:
index_status = "ERROR"
index_error = "Error opening index, check logs for more detail."
logger.exception(f"System status error opening index: {e}")
index_last_modified = None
classifier_error = None
try:
classifier = load_classifier()
if classifier is None:
raise Exception("Classifier not loaded")
classifier_status = "OK"
task_result_model = apps.get_model("django_celery_results", "taskresult")
result = (
task_result_model.objects.filter(
task_name="documents.tasks.train_classifier",
status="SUCCESS",
)
.order_by(
"-date_done",
)
.first()
)
classifier_last_trained = result.date_done if result else None
except Exception as e:
classifier_status = "ERROR"
classifier_last_trained = None
classifier_error = "Error loading classifier, check logs for more detail."
logger.exception(f"System status error loading classifier: {e}")
return Response(
{
"pngx_version": current_version,
"server_os": platform.platform(),
"install_type": install_type,
"storage": {
"total": media_stats.f_frsize * media_stats.f_blocks,
"available": media_stats.f_frsize * media_stats.f_bavail,
},
"database": {
"type": db_conn.vendor,
"url": db_url,
"status": db_status,
"error": db_error,
"migration_status": {
"latest_migration": applied_migrations[-1],
"unapplied_migrations": [
m for m in all_migrations if m not in applied_migrations
],
},
},
"tasks": {
"redis_url": redis_url,
"redis_status": redis_status,
"redis_error": redis_error,
"celery_status": celery_active,
"index_status": index_status,
"index_last_modified": index_last_modified,
"index_error": index_error,
"classifier_status": classifier_status,
"classifier_last_trained": classifier_last_trained,
"classifier_error": classifier_error,
},
},
)

View File

@ -32,6 +32,7 @@ from documents.views import SharedLinkView
from documents.views import ShareLinkViewSet
from documents.views import StatisticsView
from documents.views import StoragePathViewSet
from documents.views import SystemStatusView
from documents.views import TagViewSet
from documents.views import TasksViewSet
from documents.views import UiSettingsView
@ -147,6 +148,11 @@ urlpatterns = [
ProfileView.as_view(),
name="profile_view",
),
re_path(
"^status/",
SystemStatusView.as_view(),
name="system_status",
),
*api_router.urls,
],
),