mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Feature: system status (#5743)
This commit is contained in:
		| @@ -59,7 +59,8 @@ ARG GS_VERSION=10.02.1 | |||||||
| ENV PYTHONDONTWRITEBYTECODE=1 \ | ENV PYTHONDONTWRITEBYTECODE=1 \ | ||||||
|     PYTHONUNBUFFERED=1 \ |     PYTHONUNBUFFERED=1 \ | ||||||
|     # Ignore warning from Whitenoise |     # Ignore warning from Whitenoise | ||||||
|     PYTHONWARNINGS="ignore:::django.http.response:517" |     PYTHONWARNINGS="ignore:::django.http.response:517" \ | ||||||
|  |     PNGX_CONTAINERIZED=1 | ||||||
|  |  | ||||||
| # | # | ||||||
| # Begin installation and configuration | # Begin installation and configuration | ||||||
|   | |||||||
| @@ -77,7 +77,9 @@ | |||||||
|             "scripts": [], |             "scripts": [], | ||||||
|             "allowedCommonJsDependencies": [ |             "allowedCommonJsDependencies": [ | ||||||
|               "pdfjs-dist", |               "pdfjs-dist", | ||||||
|               "pdfjs-dist/web/pdf_viewer" |               "pdfjs-dist/web/pdf_viewer", | ||||||
|  |               "filesize", | ||||||
|  |               "file-saver" | ||||||
|             ], |             ], | ||||||
|             "vendorChunk": true, |             "vendorChunk": true, | ||||||
|             "extractLicenses": false, |             "extractLicenses": false, | ||||||
|   | |||||||
| @@ -458,7 +458,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> |           <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="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">23</context> |           <context context-type="linenumber">23</context> | ||||||
|         </context-group> |         </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-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context> |           <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context> | ||||||
|           <context context-type="linenumber">15</context> |           <context context-type="linenumber">15</context> | ||||||
| @@ -667,7 +671,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> |           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||||
| @@ -693,238 +697,249 @@ | |||||||
|         <source>Start tour</source> |         <source>Start tour</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4798013226763881638" datatype="html"> |       <trans-unit id="4798013226763881638" datatype="html"> | ||||||
|         <source>Open Django Admin</source> |         <source>Open Django Admin</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6439365426343089851" datatype="html"> |       <trans-unit id="6439365426343089851" datatype="html"> | ||||||
|         <source>General</source> |         <source>General</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8671234314555525900" datatype="html"> |       <trans-unit id="8671234314555525900" datatype="html"> | ||||||
|         <source>Appearance</source> |         <source>Appearance</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3777637051272512093" datatype="html"> |       <trans-unit id="3777637051272512093" datatype="html"> | ||||||
|         <source>Display language</source> |         <source>Display language</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="53523152145406584" datatype="html"> |       <trans-unit id="53523152145406584" datatype="html"> | ||||||
|         <source>You need to reload the page after applying a new language.</source> |         <source>You need to reload the page after applying a new language.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3766032098416558788" datatype="html"> |       <trans-unit id="3766032098416558788" datatype="html"> | ||||||
|         <source>Date display</source> |         <source>Date display</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3733378544613473393" datatype="html"> |       <trans-unit id="3733378544613473393" datatype="html"> | ||||||
|         <source>Date format</source> |         <source>Date format</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3407788781115661841" datatype="html"> |       <trans-unit id="3407788781115661841" datatype="html"> | ||||||
|         <source>Short: <x id="INTERPOLATION" equiv-text="{{today | customDate:'shortDate':null:computedDateLocale}}"/></source> |         <source>Short: <x id="INTERPOLATION" equiv-text="{{today | customDate:'shortDate':null:computedDateLocale}}"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6290748171049664628" datatype="html"> |       <trans-unit id="6290748171049664628" datatype="html"> | ||||||
|         <source>Medium: <x id="INTERPOLATION" equiv-text="{{today | customDate:'mediumDate':null:computedDateLocale}}"/></source> |         <source>Medium: <x id="INTERPOLATION" equiv-text="{{today | customDate:'mediumDate':null:computedDateLocale}}"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7189855711197998347" datatype="html"> |       <trans-unit id="7189855711197998347" datatype="html"> | ||||||
|         <source>Long: <x id="INTERPOLATION" equiv-text="{{today | customDate:'longDate':null:computedDateLocale}}"/></source> |         <source>Long: <x id="INTERPOLATION" equiv-text="{{today | customDate:'longDate':null:computedDateLocale}}"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8939587804990976924" datatype="html"> |       <trans-unit id="8939587804990976924" datatype="html"> | ||||||
|         <source>Items per page</source> |         <source>Items per page</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8028535997917730106" datatype="html"> |       <trans-unit id="8028535997917730106" datatype="html"> | ||||||
|         <source>Document editor</source> |         <source>Document editor</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6708098108196142028" datatype="html"> |       <trans-unit id="6708098108196142028" datatype="html"> | ||||||
|         <source>Use PDF viewer provided by the browser</source> |         <source>Use PDF viewer provided by the browser</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="9003921625412907981" datatype="html"> |       <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> |         <source>This is usually faster for displaying large PDF documents, but it might not work on some browsers.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3982403428275430291" datatype="html"> |       <trans-unit id="3982403428275430291" datatype="html"> | ||||||
|         <source>Sidebar</source> |         <source>Sidebar</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4608457133854405683" datatype="html"> |       <trans-unit id="4608457133854405683" datatype="html"> | ||||||
|         <source>Use 'slim' sidebar (icons only)</source> |         <source>Use 'slim' sidebar (icons only)</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1356890996281769972" datatype="html"> |       <trans-unit id="1356890996281769972" datatype="html"> | ||||||
|         <source>Dark mode</source> |         <source>Dark mode</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4913823100518391922" datatype="html"> |       <trans-unit id="4913823100518391922" datatype="html"> | ||||||
|         <source>Use system settings</source> |         <source>Use system settings</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5782828784040423650" datatype="html"> |       <trans-unit id="5782828784040423650" datatype="html"> | ||||||
|         <source>Enable dark mode</source> |         <source>Enable dark mode</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6336642923114460405" datatype="html"> |       <trans-unit id="6336642923114460405" datatype="html"> | ||||||
|         <source>Invert thumbnails in dark mode</source> |         <source>Invert thumbnails in dark mode</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7983234071833154796" datatype="html"> |       <trans-unit id="7983234071833154796" datatype="html"> | ||||||
|         <source>Theme Color</source> |         <source>Theme Color</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7808756054397155068" datatype="html"> |       <trans-unit id="7808756054397155068" datatype="html"> | ||||||
|         <source>Reset</source> |         <source>Reset</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8901931207592071833" datatype="html"> |       <trans-unit id="8901931207592071833" datatype="html"> | ||||||
|         <source>Update checking</source> |         <source>Update checking</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7141691772243630313" datatype="html"> |       <trans-unit id="7141691772243630313" datatype="html"> | ||||||
|         <source> Update checking works by pinging the public <x id="START_LINK" ctype="x-a" equiv-text="<a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">"/>GitHub API<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> for the latest release to determine whether a new version is available.<x id="LINE_BREAK" ctype="lb" equiv-text="<br/>"/> Actual updating of the app must still be performed manually. </source> |         <source> Update checking works by pinging the public <x id="START_LINK" ctype="x-a" equiv-text="<a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">"/>GitHub API<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> for the latest release to determine whether a new version is available.<x id="LINE_BREAK" ctype="lb" equiv-text="<br/>"/> Actual updating of the app must still be performed manually. </source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5489945693955857309" datatype="html"> |       <trans-unit id="5489945693955857309" datatype="html"> | ||||||
|         <source><x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text=">"/>No tracking data is collected by the app in any way.<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/></source> |         <source><x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text=">"/>No tracking data is collected by the app in any way.<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5070799004079086984" datatype="html"> |       <trans-unit id="5070799004079086984" datatype="html"> | ||||||
|         <source>Enable update checking</source> |         <source>Enable update checking</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="908152367861642592" datatype="html"> |       <trans-unit id="908152367861642592" datatype="html"> | ||||||
|         <source>Document editing</source> |         <source>Document editing</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2959590948110714366" datatype="html"> |       <trans-unit id="2959590948110714366" datatype="html"> | ||||||
|         <source>Automatically remove inbox tag(s) on save</source> |         <source>Automatically remove inbox tag(s) on save</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8508424367627989968" datatype="html"> |       <trans-unit id="8508424367627989968" datatype="html"> | ||||||
|         <source>Bulk editing</source> |         <source>Bulk editing</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8158899674926420054" datatype="html"> |       <trans-unit id="8158899674926420054" datatype="html"> | ||||||
|         <source>Show confirmation dialogs</source> |         <source>Show confirmation dialogs</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6906812245033969309" datatype="html"> |       <trans-unit id="6906812245033969309" datatype="html"> | ||||||
|         <source>Deleting documents will always ask for confirmation.</source> |         <source>Deleting documents will always ask for confirmation.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="290238406234356122" datatype="html"> |       <trans-unit id="290238406234356122" datatype="html"> | ||||||
|         <source>Apply on close</source> |         <source>Apply on close</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8104421162933956065" datatype="html"> |       <trans-unit id="8104421162933956065" datatype="html"> | ||||||
|         <source>Notes</source> |         <source>Notes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> | ||||||
| @@ -939,14 +954,14 @@ | |||||||
|         <source>Enable notes</source> |         <source>Enable notes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7314814725704332646" datatype="html"> |       <trans-unit id="7314814725704332646" datatype="html"> | ||||||
|         <source>Permissions</source> |         <source>Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html</context> |           <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> |         <source>Default Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8222269449891326545" datatype="html"> |       <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> |         <source> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI </source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4292903881380648974" datatype="html"> |       <trans-unit id="4292903881380648974" datatype="html"> | ||||||
|         <source>Default Owner</source> |         <source>Default Owner</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="734147282056744882" datatype="html"> |       <trans-unit id="734147282056744882" datatype="html"> | ||||||
|         <source>Objects without an owner can be viewed and edited by all users</source> |         <source>Objects without an owner can be viewed and edited by all users</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> |           <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> |         <source>Default View Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2191775412581217688" datatype="html"> |       <trans-unit id="2191775412581217688" datatype="html"> | ||||||
|         <source>Users:</source> |         <source>Users:</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> |           <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> |         <source>Groups:</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> |           <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> |         <source>Default Edit Permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3728984448750213892" datatype="html"> |       <trans-unit id="3728984448750213892" datatype="html"> | ||||||
|         <source>Edit permissions also grant viewing permissions</source> |         <source>Edit permissions also grant viewing permissions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> |           <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> |         <source>Notifications</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8545554728558600606" datatype="html"> |       <trans-unit id="8545554728558600606" datatype="html"> | ||||||
|         <source>Document processing</source> |         <source>Document processing</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3656786776644872398" datatype="html"> |       <trans-unit id="3656786776644872398" datatype="html"> | ||||||
|         <source>Show notifications when new documents are detected</source> |         <source>Show notifications when new documents are detected</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6057053428592387613" datatype="html"> |       <trans-unit id="6057053428592387613" datatype="html"> | ||||||
|         <source>Show notifications when document processing completes successfully</source> |         <source>Show notifications when document processing completes successfully</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="370315664367425513" datatype="html"> |       <trans-unit id="370315664367425513" datatype="html"> | ||||||
|         <source>Show notifications when document processing fails</source> |         <source>Show notifications when document processing fails</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6838309441164918531" datatype="html"> |       <trans-unit id="6838309441164918531" datatype="html"> | ||||||
|         <source>Suppress notifications on dashboard</source> |         <source>Suppress notifications on dashboard</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2741919327232918179" datatype="html"> |       <trans-unit id="2741919327232918179" datatype="html"> | ||||||
|         <source>This will suppress all messages about document processing status on the dashboard.</source> |         <source>This will suppress all messages about document processing status on the dashboard.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="472206565520537964" datatype="html"> |       <trans-unit id="472206565520537964" datatype="html"> | ||||||
|         <source>Saved views</source> |         <source>Saved views</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> |           <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> |         <source>Show warning when closing saved views with unsaved changes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2123659921722214537" datatype="html"> |       <trans-unit id="2123659921722214537" datatype="html"> | ||||||
|         <source>Views</source> |         <source>Views</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> | ||||||
| @@ -1194,7 +1209,7 @@ | |||||||
|         <source>Name</source> |         <source>Name</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> |           <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="<span class="visually-hidden">"/>Appears on<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source> |         <source> <x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="visually-hidden">"/>Appears on<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4104807402967139762" datatype="html"> |       <trans-unit id="4104807402967139762" datatype="html"> | ||||||
|         <source>Show on dashboard</source> |         <source>Show on dashboard</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context> |           <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> |         <source>Show in sidebar</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context> |           <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> |         <source>Actions</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> | ||||||
| @@ -1393,7 +1408,7 @@ | |||||||
|         <source>Delete</source> |         <source>Delete</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context> |           <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> |         <source>No saved views defined.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6839066544204061364" datatype="html"> |       <trans-unit id="6839066544204061364" datatype="html"> | ||||||
|         <source>Use system language</source> |         <source>Use system language</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7729897675462249787" datatype="html"> |       <trans-unit id="7729897675462249787" datatype="html"> | ||||||
|         <source>Use date format of display language</source> |         <source>Use date format of display language</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1235706724900303689" datatype="html"> |       <trans-unit id="1235706724900303689" datatype="html"> | ||||||
|         <source>Error retrieving users</source> |         <source>Error retrieving users</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> | ||||||
| @@ -1536,7 +1551,7 @@ | |||||||
|         <source>Error retrieving groups</source> |         <source>Error retrieving groups</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> | ||||||
| @@ -1547,35 +1562,35 @@ | |||||||
|         <source>Saved view "<x id="PH" equiv-text="savedView.name"/>" deleted.</source> |         <source>Saved view "<x id="PH" equiv-text="savedView.name"/>" deleted.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7217000812750597833" datatype="html"> |       <trans-unit id="7217000812750597833" datatype="html"> | ||||||
|         <source>Settings were saved successfully.</source> |         <source>Settings were saved successfully.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="525012668859298131" datatype="html"> |       <trans-unit id="525012668859298131" datatype="html"> | ||||||
|         <source>Settings were saved successfully. Reload is required to apply some changes.</source> |         <source>Settings were saved successfully. Reload is required to apply some changes.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8491974984518503778" datatype="html"> |       <trans-unit id="8491974984518503778" datatype="html"> | ||||||
|         <source>Reload now</source> |         <source>Reload now</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3011185103048412841" datatype="html"> |       <trans-unit id="3011185103048412841" datatype="html"> | ||||||
|         <source>An error occurred while saving settings.</source> |         <source>An error occurred while saving settings.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context> |           <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> |         <source>Error while storing settings on server.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2991443309752293110" datatype="html"> |       <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="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> | ||||||
|           <context context-type="linenumber">5</context> |           <context context-type="linenumber">5</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="1230154438678955604" datatype="html"> |       <trans-unit id="1230154438678955604" datatype="html"> | ||||||
|         <source>Change</source> |         <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="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context> | ||||||
|           <context context-type="linenumber">29</context> |           <context context-type="linenumber">29</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="595732867213154214" datatype="html"> |       <trans-unit id="595732867213154214" datatype="html"> | ||||||
|         <source>Regenerate auth token</source> |         <source>Regenerate auth token</source> | ||||||
| @@ -4370,8 +4393,68 @@ | |||||||
|           <context context-type="linenumber">151</context> |           <context context-type="linenumber">151</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </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"> |       <trans-unit id="5611592591303869712" datatype="html"> | ||||||
|         <source>Status</source> |         <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-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context> |           <context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context> | ||||||
|           <context context-type="linenumber">26</context> |           <context context-type="linenumber">26</context> | ||||||
| @@ -4381,6 +4464,76 @@ | |||||||
|           <context context-type="linenumber">19</context> |           <context context-type="linenumber">19</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </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"> |       <trans-unit id="6732151329960766506" datatype="html"> | ||||||
|         <source>Copy Raw Error</source> |         <source>Copy Raw Error</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -29,6 +29,7 @@ | |||||||
|         "ngx-color": "^9.0.0", |         "ngx-color": "^9.0.0", | ||||||
|         "ngx-cookie-service": "^17.1.0", |         "ngx-cookie-service": "^17.1.0", | ||||||
|         "ngx-file-drop": "^16.0.0", |         "ngx-file-drop": "^16.0.0", | ||||||
|  |         "ngx-filesize": "^3.0.3", | ||||||
|         "ngx-ui-tour-ng-bootstrap": "^14.0.2", |         "ngx-ui-tour-ng-bootstrap": "^14.0.2", | ||||||
|         "pdfjs-dist": "^3.11.174", |         "pdfjs-dist": "^3.11.174", | ||||||
|         "rxjs": "^7.8.1", |         "rxjs": "^7.8.1", | ||||||
| @@ -9844,6 +9845,15 @@ | |||||||
|         "node": ">=10" |         "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": { |     "node_modules/fill-range": { | ||||||
|       "version": "7.0.1", |       "version": "7.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", |       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", | ||||||
| @@ -14105,6 +14115,19 @@ | |||||||
|         "@angular/core": ">=14.0.0" |         "@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": { |     "node_modules/ngx-ui-tour-core": { | ||||||
|       "version": "12.0.1", |       "version": "12.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-12.0.1.tgz", |       "resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-12.0.1.tgz", | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ | |||||||
|     "ngx-color": "^9.0.0", |     "ngx-color": "^9.0.0", | ||||||
|     "ngx-cookie-service": "^17.1.0", |     "ngx-cookie-service": "^17.1.0", | ||||||
|     "ngx-file-drop": "^16.0.0", |     "ngx-file-drop": "^16.0.0", | ||||||
|  |     "ngx-filesize": "^3.0.3", | ||||||
|     "ngx-ui-tour-ng-bootstrap": "^14.0.2", |     "ngx-ui-tour-ng-bootstrap": "^14.0.2", | ||||||
|     "pdfjs-dist": "^3.11.174", |     "pdfjs-dist": "^3.11.174", | ||||||
|     "rxjs": "^7.8.1", |     "rxjs": "^7.8.1", | ||||||
|   | |||||||
| @@ -114,7 +114,10 @@ import { FileComponent } from './components/common/input/file/file.component' | |||||||
| import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | ||||||
| import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component' | import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component' | ||||||
| import { MonetaryComponent } from './components/common/input/monetary/monetary.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 { | import { | ||||||
|  |   airplane, | ||||||
|   archive, |   archive, | ||||||
|   arrowCounterclockwise, |   arrowCounterclockwise, | ||||||
|   arrowDown, |   arrowDown, | ||||||
| @@ -129,12 +132,14 @@ import { | |||||||
|   boxes, |   boxes, | ||||||
|   calendar, |   calendar, | ||||||
|   calendarEvent, |   calendarEvent, | ||||||
|  |   cardChecklist, | ||||||
|   caretDown, |   caretDown, | ||||||
|   caretUp, |   caretUp, | ||||||
|   chatLeftText, |   chatLeftText, | ||||||
|   check, |   check, | ||||||
|   check2All, |   check2All, | ||||||
|   checkAll, |   checkAll, | ||||||
|  |   checkCircleFill, | ||||||
|   checkLg, |   checkLg, | ||||||
|   chevronDoubleLeft, |   chevronDoubleLeft, | ||||||
|   chevronDoubleRight, |   chevronDoubleRight, | ||||||
| @@ -148,7 +153,9 @@ import { | |||||||
|   doorOpen, |   doorOpen, | ||||||
|   download, |   download, | ||||||
|   envelope, |   envelope, | ||||||
|  |   exclamationCircleFill, | ||||||
|   exclamationTriangle, |   exclamationTriangle, | ||||||
|  |   exclamationTriangleFill, | ||||||
|   eye, |   eye, | ||||||
|   fileEarmark, |   fileEarmark, | ||||||
|   fileEarmarkCheck, |   fileEarmarkCheck, | ||||||
| @@ -200,6 +207,7 @@ import { | |||||||
| } from 'ngx-bootstrap-icons' | } from 'ngx-bootstrap-icons' | ||||||
|  |  | ||||||
| const icons = { | const icons = { | ||||||
|  |   airplane, | ||||||
|   archive, |   archive, | ||||||
|   arrowCounterclockwise, |   arrowCounterclockwise, | ||||||
|   arrowDown, |   arrowDown, | ||||||
| @@ -214,12 +222,14 @@ const icons = { | |||||||
|   boxes, |   boxes, | ||||||
|   calendar, |   calendar, | ||||||
|   calendarEvent, |   calendarEvent, | ||||||
|  |   cardChecklist, | ||||||
|   caretDown, |   caretDown, | ||||||
|   caretUp, |   caretUp, | ||||||
|   chatLeftText, |   chatLeftText, | ||||||
|   check, |   check, | ||||||
|   check2All, |   check2All, | ||||||
|   checkAll, |   checkAll, | ||||||
|  |   checkCircleFill, | ||||||
|   checkLg, |   checkLg, | ||||||
|   chevronDoubleLeft, |   chevronDoubleLeft, | ||||||
|   chevronDoubleRight, |   chevronDoubleRight, | ||||||
| @@ -233,7 +243,9 @@ const icons = { | |||||||
|   doorOpen, |   doorOpen, | ||||||
|   download, |   download, | ||||||
|   envelope, |   envelope, | ||||||
|  |   exclamationCircleFill, | ||||||
|   exclamationTriangle, |   exclamationTriangle, | ||||||
|  |   exclamationTriangleFill, | ||||||
|   eye, |   eye, | ||||||
|   fileEarmark, |   fileEarmark, | ||||||
|   fileEarmarkCheck, |   fileEarmarkCheck, | ||||||
| @@ -445,6 +457,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     FileComponent, |     FileComponent, | ||||||
|     ConfirmButtonComponent, |     ConfirmButtonComponent, | ||||||
|     MonetaryComponent, |     MonetaryComponent, | ||||||
|  |     SystemStatusDialogComponent, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
| @@ -459,6 +472,7 @@ function initializeApp(settings: SettingsService) { | |||||||
|     TourNgBootstrapModule, |     TourNgBootstrapModule, | ||||||
|     DragDropModule, |     DragDropModule, | ||||||
|     NgxBootstrapIconsModule.pick(icons), |     NgxBootstrapIconsModule.pick(icons), | ||||||
|  |     NgxFilesizeModule, | ||||||
|   ], |   ], | ||||||
|   providers: [ |   providers: [ | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -4,10 +4,31 @@ | |||||||
|   info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>." |   info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>." | ||||||
|   i18n-info |   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> <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"> |   <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> |     <ng-container i18n>Open Django Admin</ng-container> | ||||||
|     <i-bs name="arrow-up-right"></i-bs> |      <i-bs name="arrow-up-right"></i-bs> | ||||||
|   </a> |   </a> | ||||||
| </pngx-page-header> | </pngx-page-header> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,8 @@ import { | |||||||
|   NgbModule, |   NgbModule, | ||||||
|   NgbAlertModule, |   NgbAlertModule, | ||||||
|   NgbNavLink, |   NgbNavLink, | ||||||
|  |   NgbModal, | ||||||
|  |   NgbModalModule, | ||||||
| } from '@ng-bootstrap/ng-bootstrap' | } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { NgSelectModule } from '@ng-select/ng-select' | import { NgSelectModule } from '@ng-select/ng-select' | ||||||
| import { of, throwError } from 'rxjs' | import { of, throwError } from 'rxjs' | ||||||
| @@ -39,6 +41,13 @@ import { SettingsComponent } from './settings.component' | |||||||
| import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' | import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' | ||||||
| import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | ||||||
| import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' | 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 = [ | const savedViews = [ | ||||||
|   { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true }, |   { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true }, | ||||||
| @@ -65,6 +74,8 @@ describe('SettingsComponent', () => { | |||||||
|   let userService: UserService |   let userService: UserService | ||||||
|   let permissionsService: PermissionsService |   let permissionsService: PermissionsService | ||||||
|   let groupService: GroupService |   let groupService: GroupService | ||||||
|  |   let modalService: NgbModal | ||||||
|  |   let systemStatusService: SystemStatusService | ||||||
|  |  | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     TestBed.configureTestingModule({ |     TestBed.configureTestingModule({ | ||||||
| @@ -96,6 +107,7 @@ describe('SettingsComponent', () => { | |||||||
|         NgbAlertModule, |         NgbAlertModule, | ||||||
|         NgSelectModule, |         NgSelectModule, | ||||||
|         NgxBootstrapIconsModule.pick(allIcons), |         NgxBootstrapIconsModule.pick(allIcons), | ||||||
|  |         NgbModalModule, | ||||||
|       ], |       ], | ||||||
|     }).compileComponents() |     }).compileComponents() | ||||||
|  |  | ||||||
| @@ -107,6 +119,8 @@ describe('SettingsComponent', () => { | |||||||
|     settingsService.currentUser = users[0] |     settingsService.currentUser = users[0] | ||||||
|     userService = TestBed.inject(UserService) |     userService = TestBed.inject(UserService) | ||||||
|     permissionsService = TestBed.inject(PermissionsService) |     permissionsService = TestBed.inject(PermissionsService) | ||||||
|  |     modalService = TestBed.inject(NgbModal) | ||||||
|  |     systemStatusService = TestBed.inject(SystemStatusService) | ||||||
|     jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) |     jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) | ||||||
|     jest |     jest | ||||||
|       .spyOn(permissionsService, 'currentUserHasObjectPermissions') |       .spyOn(permissionsService, 'currentUserHasObjectPermissions') | ||||||
| @@ -372,4 +386,54 @@ describe('SettingsComponent', () => { | |||||||
|     fixture.detectChanges() |     fixture.detectChanges() | ||||||
|     expect(toastErrorSpy).toBeCalled() |     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', | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -9,7 +9,11 @@ import { | |||||||
| } from '@angular/core' | } from '@angular/core' | ||||||
| import { FormGroup, FormControl } from '@angular/forms' | import { FormGroup, FormControl } from '@angular/forms' | ||||||
| import { ActivatedRoute, Router } from '@angular/router' | 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 { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms' | ||||||
| import { TourService } from 'ngx-ui-tour-ng-bootstrap' | import { TourService } from 'ngx-ui-tour-ng-bootstrap' | ||||||
| import { | import { | ||||||
| @@ -40,6 +44,12 @@ import { | |||||||
| } from 'src/app/services/settings.service' | } from 'src/app/services/settings.service' | ||||||
| import { ToastService, Toast } from 'src/app/services/toast.service' | import { ToastService, Toast } from 'src/app/services/toast.service' | ||||||
| import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' | 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 { | enum SettingsNavIDs { | ||||||
|   General = 1, |   General = 1, | ||||||
| @@ -111,6 +121,18 @@ export class SettingsComponent | |||||||
|   users: User[] |   users: User[] | ||||||
|   groups: Group[] |   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 { |   get computedDateLocale(): string { | ||||||
|     return ( |     return ( | ||||||
|       this.settingsForm.value.dateLocale || |       this.settingsForm.value.dateLocale || | ||||||
| @@ -131,7 +153,9 @@ export class SettingsComponent | |||||||
|     private usersService: UserService, |     private usersService: UserService, | ||||||
|     private groupsService: GroupService, |     private groupsService: GroupService, | ||||||
|     private router: Router, |     private router: Router, | ||||||
|     public permissionsService: PermissionsService |     public permissionsService: PermissionsService, | ||||||
|  |     private modalService: NgbModal, | ||||||
|  |     private systemStatusService: SystemStatusService | ||||||
|   ) { |   ) { | ||||||
|     super() |     super() | ||||||
|     this.settings.settingsSaved.subscribe(() => { |     this.settings.settingsSaved.subscribe(() => { | ||||||
| @@ -360,6 +384,17 @@ export class SettingsComponent | |||||||
|       // prevents loss of unsaved changes |       // prevents loss of unsaved changes | ||||||
|       this.settingsForm.patchValue(currentFormValue) |       this.settingsForm.patchValue(currentFormValue) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       this.permissionsService.currentUserCan( | ||||||
|  |         PermissionAction.View, | ||||||
|  |         PermissionType.Admin | ||||||
|  |       ) | ||||||
|  |     ) { | ||||||
|  |       this.systemStatusService.get().subscribe((status) => { | ||||||
|  |         this.systemStatus = status | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private emptyGroup(group: FormGroup) { |   private emptyGroup(group: FormGroup) { | ||||||
| @@ -565,4 +600,14 @@ export class SettingsComponent | |||||||
|   clearThemeColor() { |   clearThemeColor() { | ||||||
|     this.settingsForm.get('themeColor').patchValue('') |     this.settingsForm.get('themeColor').patchValue('') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   showSystemStatus() { | ||||||
|  |     const modal: NgbModalRef = this.modalService.open( | ||||||
|  |       SystemStatusDialogComponent, | ||||||
|  |       { | ||||||
|  |         size: 'xl', | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |     modal.componentInstance.status = this.systemStatus | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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>  | ||||||
|  |     } | ||||||
|  |     @if (copied) { | ||||||
|  |       <i-bs name="clipboard-check-fill"></i-bs>  | ||||||
|  |     } | ||||||
|  |     <ng-container i18n>Copy</ng-container> | ||||||
|  |   </button> | ||||||
|  | </div> | ||||||
| @@ -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() | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @@ -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 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								src-ui/src/app/data/system-status.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src-ui/src/app/data/system-status.ts
									
									
									
									
									
										Normal 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 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								src-ui/src/app/services/system-status.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src-ui/src/app/services/system-status.service.spec.ts
									
									
									
									
									
										Normal 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') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
							
								
								
									
										20
									
								
								src-ui/src/app/services/system-status.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src-ui/src/app/services/system-status.service.ts
									
									
									
									
									
										Normal 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}/` | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										186
									
								
								src/documents/tests/test_api_status.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/documents/tests/test_api_status.py
									
									
									
									
									
										Normal 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"]) | ||||||
| @@ -2,6 +2,7 @@ import itertools | |||||||
| import json | import json | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
|  | import platform | ||||||
| import re | import re | ||||||
| import tempfile | import tempfile | ||||||
| import urllib | import urllib | ||||||
| @@ -13,8 +14,12 @@ from unicodedata import normalize | |||||||
| from urllib.parse import quote | from urllib.parse import quote | ||||||
|  |  | ||||||
| import pathvalidate | import pathvalidate | ||||||
|  | from django.apps import apps | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import User | 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 Case | ||||||
| from django.db.models import Count | from django.db.models import Count | ||||||
| from django.db.models import IntegerField | 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.shortcuts import get_object_or_404 | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.decorators import method_decorator | from django.utils.decorators import method_decorator | ||||||
|  | from django.utils.timezone import make_aware | ||||||
| from django.utils.translation import get_language | from django.utils.translation import get_language | ||||||
| from django.views import View | from django.views import View | ||||||
| from django.views.decorators.cache import cache_control | 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 django_filters.rest_framework import DjangoFilterBackend | ||||||
| from langdetect import detect | from langdetect import detect | ||||||
| from packaging import version as packaging_version | from packaging import version as packaging_version | ||||||
|  | from redis import Redis | ||||||
| from rest_framework import parsers | from rest_framework import parsers | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.exceptions import NotFound | from rest_framework.exceptions import NotFound | ||||||
| @@ -61,6 +68,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet | |||||||
| from rest_framework.viewsets import ViewSet | from rest_framework.viewsets import ViewSet | ||||||
|  |  | ||||||
| from documents import bulk_edit | from documents import bulk_edit | ||||||
|  | from documents import index | ||||||
| from documents.bulk_download import ArchiveOnlyStrategy | from documents.bulk_download import ArchiveOnlyStrategy | ||||||
| from documents.bulk_download import OriginalAndArchiveStrategy | from documents.bulk_download import OriginalAndArchiveStrategy | ||||||
| from documents.bulk_download import OriginalsOnlyStrategy | from documents.bulk_download import OriginalsOnlyStrategy | ||||||
| @@ -138,6 +146,7 @@ from documents.serialisers import WorkflowTriggerSerializer | |||||||
| from documents.signals import document_updated | from documents.signals import document_updated | ||||||
| from documents.tasks import consume_file | from documents.tasks import consume_file | ||||||
| from paperless import version | from paperless import version | ||||||
|  | from paperless.celery import app as celery_app | ||||||
| from paperless.config import GeneralConfig | from paperless.config import GeneralConfig | ||||||
| from paperless.db import GnuPG | from paperless.db import GnuPG | ||||||
| from paperless.views import StandardPagination | from paperless.views import StandardPagination | ||||||
| @@ -1539,3 +1548,132 @@ class CustomFieldViewSet(ModelViewSet): | |||||||
|     model = CustomField |     model = CustomField | ||||||
|  |  | ||||||
|     queryset = CustomField.objects.all().order_by("-created") |     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, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ from documents.views import SharedLinkView | |||||||
| from documents.views import ShareLinkViewSet | from documents.views import ShareLinkViewSet | ||||||
| from documents.views import StatisticsView | from documents.views import StatisticsView | ||||||
| from documents.views import StoragePathViewSet | from documents.views import StoragePathViewSet | ||||||
|  | from documents.views import SystemStatusView | ||||||
| from documents.views import TagViewSet | from documents.views import TagViewSet | ||||||
| from documents.views import TasksViewSet | from documents.views import TasksViewSet | ||||||
| from documents.views import UiSettingsView | from documents.views import UiSettingsView | ||||||
| @@ -147,6 +148,11 @@ urlpatterns = [ | |||||||
|                     ProfileView.as_view(), |                     ProfileView.as_view(), | ||||||
|                     name="profile_view", |                     name="profile_view", | ||||||
|                 ), |                 ), | ||||||
|  |                 re_path( | ||||||
|  |                     "^status/", | ||||||
|  |                     SystemStatusView.as_view(), | ||||||
|  |                     name="system_status", | ||||||
|  |                 ), | ||||||
|                 *api_router.urls, |                 *api_router.urls, | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon