Files
Lauri Ojansivu c99aa7f845 Admin Panel / Problems / Password length, and a changed password signs every session out.
PASSWORD_MIN_LENGTH (default 8) is now the length below which a sign-in
is REPORTED, in the new Admin Panel / Problems / Password length, and
PASSWORD_MIN_LENGTH_ENFORCE (off by default) is what makes it a rule: a
shorter new password is refused, and an account that signs in with an
older shorter one sees only "Change your password" until it has a long
enough one. Signing in is never refused, enforced or not, so nobody is
locked out by a minimum set after their password. LDAP, OAuth2, OIDC,
SAML, CAS and Sandstorm accounts are neither reported nor asked: their
password is their provider's. An administrator's Set password no longer
applies WeKan's policy to such an account.

Meteor sends a password hashed, so only the browser knows its length: it
notes it on the sign-in form and, after a sign-in that worked, says "it
was short" - never the password or its length.

The page has both settings and a table, one row per account: username,
first and last short-password login, the latest IPv4 and IPv6 address
with the country's flag and city when a CDN says where it is, the count,
and when it was fixed, with the Too short (default) / Fixed / All filter.
Fixed is a changed password not seen short since; a short one seen
again reopens the row.

Changing one's own password - Member Settings, the forced change, a
reset link - now signs EVERY session of the account out, the one that
changed it too, and only the new password signs in again. The browser
goes to the sign-in page as soon as it is signed out, since the server
closes the connection before the answer can arrive.

Documented in docs/Features/Admin-Panel/Problems/Password-length.md, the
settings on every platform. The new strings are in every locale in
English for now. Tests: passwordPolicy, passwordChangeRequired and
shortPasswordLogins (node); password-min-length-signup-login,
password-length-report and password-change-other-sessions (Playwright,
Chromium and WebKit), which change each setting on the page and check
what it does, and that sign-up with no mail server registers and signs
the user in.

Thanks to xet7 !
2026-10-31 14:54:22 +02:00

527 lines
21 KiB
Plaintext

template(name="adminProblems")
.setting-content.admin-reports-content
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-body
+leftMenu(menuItems)
.main-body
//- Every Admin Panel pane opens with the same heading, and it is the
//- open menu entry's own label (docs/Features/All-Boards/Left-Menu.md).
+paneTitle(paneTitleData)
if showFileStatusAudit
+fileStatusAudit
if isPane 'report-recovery'
+emailRecoveryReports
+activityNotificationRecoveryReports
+syncRuleEmailRecoveryReports
+syncRuleEmailLegacyCommands
+listSyncStuckOperations
+interruptedImports
+syncRecoveryReports
if loading.get
+spinner
else if isPane 'report-summary'
+problemsSummary
else if isPane 'features-performance'
+featuresPerformance
else if isPane 'features-security'
+featuresSecurity
else if isPane 'features-delete'
+featuresDelete
else if isPane 'features-notifications'
+featuresNotifications
else if isPane 'features-backup'
+backupRunsStatus(settingsLink=true)
else if isPane 'report-security'
+eventStreamReport(stream="security")
else if isPane 'report-speed'
+eventStreamReport(stream="speed")
else if isPane 'report-tests'
+eventStreamReport(stream="tests")
else if isPane 'report-cpu'
+eventStreamReport(stream="cpu")
else if isPane 'report-ram'
+eventStreamReport(stream="ram")
else if isPane 'report-disk'
+eventStreamReport(stream="disk")
else if isPane 'report-instrumentation'
+instrumentationReport
else if isPane 'report-database'
+eventStreamReport(stream="database")
else if isPane 'report-integrity'
+eventStreamReport(stream="integrity")
else if isPane 'report-mail'
+eventStreamReport(stream="mail")
else if isPane 'report-api'
+eventStreamReport(stream="api")
else if isPane 'report-office'
+officeReport
else if isPane 'report-password-length'
+passwordLengthReport
else if tablePageData
+tablePage(tablePageData)
template(name="officeReport")
//- Where people log in from, grouped. Through the shared table page like every
//- other report here (docs/Features/General/Table.md).
+tablePage(tablePageData)
template(name="eventStreamReport")
//- Security / Speed / Tests / CPU usage all render through the shared table
//- page (docs/Features/General/Table.md). CPU usage additionally passes the live
//- figure as the status row between the title and the controls.
+tablePage(tablePageData)
template(name="instrumentationReport")
p.description Live measurements for this WeKan server process since startup. Refreshes every 5 seconds. Arguments, results, addresses and user identifiers are not stored. Values reset when the server restarts.
if error
p.error {{error}}
else
//- The whole picture first: one bar per area of WeKan, its length the
//- area's calls against the busiest area's, its red end the share that
//- failed. A row filters the table below to that area; again, all of it.
//- models/lib/instrumentationAreas.js groups the operations.
if areas.length
.instrumentation-areas
.instrumentation-area-head(aria-hidden="true")
span
span
span.instrumentation-area-num {{_ 'api-calls'}}
span.instrumentation-area-num {{_ 'duration'}}
span.instrumentation-area-num {{_ 'errors'}}
each area in areas
button.instrumentation-area.js-instrumentation-area(type="button" data-area=area.id class="{{#if area.active}}is-active{{/if}}" aria-pressed="{{#if area.active}}true{{else}}false{{/if}}" title=area.title)
span.instrumentation-area-name {{area.label}}
span.instrumentation-area-bar
span.instrumentation-area-fill(style="width: {{area.barPct}}%")
if area.errors
span.instrumentation-area-errors(style="width: {{area.errorPct}}%")
span.instrumentation-area-num {{area.callsText}}
span.instrumentation-area-num {{area.avgText}}
span.instrumentation-area-num(class="{{#if area.errors}}has-errors{{/if}}") {{area.errorsText}}
+tablePage(tablePageData)
template(name="instrumentationStatus")
.admin-cpu-current
span DDP connections: {{active}} active, {{opened}} opened, {{closed}} closed
span.quiet · Started {{startedAt}}
template(name="cpuCurrentStatus")
.admin-cpu-current(class="{{#if high}}admin-cpu-high{{/if}}")
span.admin-cpu-label {{_ 'cpu-usage-current'}}
span.admin-cpu-percent {{percent}}%
span.quiet
| {{cores}} {{_ 'cpu-cores-suffix'}} · {{_ 'cpu-load-average'}} {{loadAverage}}
if statistics
span.quiet
| · min/avg/max {{statistics.minimum}}%/{{statistics.average}}%/{{statistics.maximum}}%
| ({{statistics.samples}} samples, lowest {{statistics.lowestAt}})
if activity
span.quiet · {{activity}}
template(name="ramCurrentStatus")
.admin-cpu-current(class="{{#if high}}admin-cpu-high{{/if}}")
span.admin-cpu-label {{_ 'memory-usage'}}
span.admin-cpu-percent {{percent}}%
span.quiet {{used}} / {{total}} GB
if swap
span.quiet · swap {{swap.percent}}% ({{swap.used}} / {{swap.total}} GB)
template(name="diskCurrentStatus")
each disk in disks
.admin-cpu-current(class="{{#if disk.high}}admin-cpu-high{{/if}}")
span.admin-cpu-label {{_ 'sandstorm-disk-usage'}}
span.admin-cpu-percent {{disk.percent}}%
span.quiet {{disk.available}} / {{disk.total}} GB · {{disk.path}} ({{disk.roles}})
//- Performance, Security and Notifications. They were the whole of Admin Panel /
//- Features, which is gone; they are Problems panes now and live with the page that
//- renders them. Each is its own template so its helpers and handlers can live ON it -
//- Blaze resolves both against the template an element is IN, never an enclosing one.
//- Admin Panel / Problems / Performance.
template(name="featuresPerformance")
ul.setting-detail
li.layout-form
.title {{_ 'cards-loading'}}
p.description {{_ 'cards-loading-description'}}
//- Admin Panel / Problems / Security.
template(name="featuresSecurity")
ul.setting-detail
li
a.flex.js-toggle-render-links-as-plain-text
.materialCheckBox(class="{{#if renderLinksAsPlainText}}is-checked{{/if}}")
span {{_ 'render-links-as-plain-text'}}
p.description {{_ 'render-links-as-plain-text-description'}}
li
a.flex.js-toggle-always-show-code-as-text
.materialCheckBox(class="{{#if alwaysShowCodeAsText}}is-checked{{/if}}")
span {{_ 'always-show-code-as-text'}}
p.description {{_ 'always-show-code-as-text-description'}}
li
a.flex.js-toggle-disable-all-import
.materialCheckBox(class="{{#if disableAllImport}}is-checked{{/if}}")
span {{_ 'disable-all-import'}}
p.description {{_ 'disable-all-import-description'}}
li
a.flex.js-toggle-disable-all-export
.materialCheckBox(class="{{#if disableAllExport}}is-checked{{/if}}")
span {{_ 'disable-all-export'}}
p.description {{_ 'disable-all-export-description'}}
li
a.flex.js-toggle-disable-import-avatars
.materialCheckBox(class="{{#if disableImportAvatars}}is-checked{{/if}}")
span {{_ 'disable-import-avatars'}}
p.description {{_ 'disable-import-avatars-description'}}
li
a.flex.js-toggle-disable-export-avatars
.materialCheckBox(class="{{#if disableExportAvatars}}is-checked{{/if}}")
span {{_ 'disable-export-avatars'}}
p.description {{_ 'disable-export-avatars-description'}}
li
a.flex.js-toggle-anonymize-import-users
.materialCheckBox(class="{{#if anonymizeImportUsers}}is-checked{{/if}}")
span {{_ 'anonymize-import-users'}}
p.description {{_ 'anonymize-import-users-description'}}
li
a.flex.js-toggle-anonymize-export-users
.materialCheckBox(class="{{#if anonymizeExportUsers}}is-checked{{/if}}")
span {{_ 'anonymize-export-users'}}
p.description {{_ 'anonymize-export-users-description'}}
//- Admin Panel / Problems / Delete.
template(name="featuresDelete")
ul.setting-detail
li
a.flex.js-toggle-enable-permanent-delete
.materialCheckBox(class="{{#if enablePermanentDelete}}is-checked{{/if}}")
span {{_ 'enable-permanent-delete'}}
p.description {{_ 'enable-permanent-delete-description'}}
p.description {{permanentDeleteRecoveryDescription}}
//- Admin Panel / Problems / Notifications.
template(name="featuresNotifications")
ul.setting-detail
li
a.flex.js-toggle-disable-activities
.materialCheckBox(class="{{#if disableActivities}}is-checked{{/if}}")
span {{_ 'disable-activities'}}
p.description {{_ 'disable-activities-description'}}
li
a.flex.js-toggle-disable-notifications
.materialCheckBox(class="{{#if disableNotifications}}is-checked{{/if}}")
span {{_ 'disable-notifications'}}
p.description {{_ 'disable-notifications-description'}}
li
a.flex.js-toggle-disable-watch
.materialCheckBox(class="{{#if disableWatch}}is-checked{{/if}}")
span {{_ 'disable-watch'}}
p.description {{_ 'disable-watch-description'}}
//- Read-only storage diagnostics are shared by Files, Integrity and Recovery.
template(name="fileStatusAudit")
section.file-status-audit(aria-label="File status checks")
p Check file contents, database metadata and recovery evidence. No files or records are changed.
.file-status-audit-actions
button.js-file-audit-all(type="button" disabled=running) Check files and history
button.js-file-audit-types(type="button" disabled=running) Check file types and extensions
button.js-file-audit-inventory(type="button" disabled=running) Check missing and untracked files
if running
button.js-file-audit-cancel(type="button") Cancel check
if result
button.js-file-audit-download(type="button") Download status summary
if error
p(role="alert") {{error}}
if result
.file-status-audit-result(role="status" aria-live="polite")
p {{statusText}}
p {{countText}}
if coverageText
p {{coverageText}}
if result.limitations
details
summary Coverage and limitations
ul
each result.limitations
li {{this}}
if totals
ul
each totals
li {{kind}}: {{count}}
details
summary Storage directories
ul
each result.roots
li {{path}} — {{status}} {{resolved}}
if result.findings
details
summary Findings (bounded sample; full counts above)
each findings
pre {{this}}
if result.omittedFindings
p Additional findings not displayed: {{result.omittedFindings}}
template(name="syncRecoveryReports")
section.sync-recovery-reports
h2 {{_ 'sync-recovery-heading'}}
p.quiet {{_ 'sync-recovery-description'}}
if error
p.error {{error}}
+tablePage(tablePageData)
template(name="syncRecoveryReportRow")
tr
td {{started}}
td
p {{_ 'board'}}: {{boardId}}
p {{_ 'list'}}: {{listId}}
td {{_ statusLabel}}
td
details
summary {{_ 'details'}}
p.quiet {{_ 'sync-report-partial'}}
if hasCounts
p {{_ 'sync-preview-create'}}: {{created}}
p {{_ 'sync-preview-update'}}: {{updated}}
p {{_ 'sync-preview-archive'}}: {{archived}}
each sourceRows
p {{path}}: {{count}} — {{_ reasonLabel}}
each normalizedRows
p {{field}}: {{count}} — {{_ reasonLabel}}
if coverage.source.truncated
p {{_ 'sync-source-truncated'}}
p {{_ 'sync-source-omitted'}}: {{coverage.source.omittedOccurrences}}
p {{_ 'sync-preview-parser-warnings'}}: {{coverage.parserWarnings}}
p {{_ 'sync-preview-parser-unsupported'}}: {{coverage.parserUnsupported}}
template(name="emailRecoveryReports")
section.email-recovery-reports
h2 {{_ 'email-recovery-heading'}}
p.quiet {{_ 'email-recovery-description'}}
if error
p.error(role="alert") {{error}}
if busy
p.quiet {{_ 'email-recovery-saving'}}
+tablePage(tablePageData)
template(name="emailRecoveryReportRow")
tr(data-recipient=userId)
td
p {{username}}
p.quiet {{userId}}
td {{_ statusLabel}}
td
p {{_ 'email-recovery-queued'}}: {{queued}}
p {{_ 'email-recovery-retrying'}}: {{retrying}}
p {{_ 'email-recovery-stopped'}}: {{failed}}
each failureRows
p.quiet {{_ labelKey}}: {{count}}
p {{_ 'email-recovery-attempts'}}: {{attempts}}
td
p {{_ 'email-recovery-oldest'}}: {{oldestText}}
p {{_ 'email-recovery-next'}}: {{nextText}}
if changedText
p {{_ 'email-recovery-changed'}}: {{changedText}}
p.quiet {{changedBy}}
td
if paused
button.js-email-queue-action(type="button" data-action="resume" disabled=busy) {{_ 'email-recovery-resume'}}
else
button.js-email-queue-action(type="button" data-action="pause" disabled=busy) {{_ 'email-recovery-pause'}}
if failed
button.js-email-queue-action(type="button" data-action="retry" disabled=busy) {{_ 'email-recovery-retry'}}
if queued
button.js-email-queue-action(type="button" data-action="cancel" disabled=busy) {{_ 'email-recovery-cancel'}}
template(name="activityNotificationRecoveryReports")
section.activity-notification-recovery-reports
h2 {{_ 'activity-recovery-heading'}}
p.quiet {{_ 'activity-recovery-description'}}
if error
p.error(role="alert") {{error}}
if busy
p.quiet {{_ 'activity-recovery-retrying'}}
+tablePage(tablePageData)
template(name="activityNotificationRecoveryRow")
tr(data-intent=intentId)
td
p {{activityId}}
if cardId
p.quiet {{_ 'card'}}: {{cardId}}
td {{boardId}}
td
p {{_ statusLabel}}
if showPaused
p {{_ 'activity-recovery-paused'}}
td {{createdText}}
td
button.js-cancel-activity-notification(type="button" disabled=controlDisabled) {{_ 'activity-recovery-cancel'}}
button.js-control-activity-notification(type="button" disabled=controlDisabled) {{_ controlLabel}}
button.js-retry-activity-notification(type="button" disabled=retryDisabled) {{_ 'activity-recovery-retry'}}
template(name="syncRuleEmailRecoveryReports")
section.sync-rule-email-recovery-reports
h2 {{_ 'rule-email-recovery-heading'}}
p.quiet {{_ 'rule-email-recovery-description'}}
label
| {{_ 'status'}}
select.js-rule-email-status
option(value="all") {{_ 'rule-email-recovery-all'}}
option(value="unconfirmed") {{_ 'rule-email-recovery-unconfirmed'}}
option(value="sent") {{_ 'rule-email-recovery-sent'}}
option(value="dropped") {{_ 'rule-email-recovery-dropped'}}
if error
p.error(role="alert") {{error}}
+tablePage(tablePageData)
if detail
with detail
section.rule-email-recovery-detail(data-command=commandId aria-live="polite")
h3 {{_ 'rule-email-recovery-recipients'}}
ul
each recipients
li {{address}} — {{_ statusKey}}
if resends.length
p.quiet {{_ 'rule-email-recovery-resends' count=resends.length}}
p.quiet {{_ 'rule-email-recovery-actions-hint'}}
unless resolvable
if waiting
p {{_ 'rule-email-recovery-wait' time=resolvableText}}
if actionError
p.error(role="alert") {{actionError}}
button.js-rule-email-resend(type="button" disabled=actionsDisabled) {{_ 'rule-email-recovery-resend'}}
button.js-rule-email-mark-sent(type="button" disabled=actionsDisabled) {{_ 'rule-email-recovery-mark-sent'}}
button.js-rule-email-drop(type="button" disabled=actionsDisabled) {{_ 'rule-email-recovery-drop'}}
template(name="syncRuleEmailRecoveryRow")
tr(data-command=commandId)
td.rule-email-recovery-identifiers
p {{commandId}}
p.quiet {{invocationId}}
p.quiet {{attemptId}}
td {{_ statusLabel}}
td {{startedText}}
td {{finishedText}}
td
if canReview
button.js-rule-email-review(type="button") {{_ 'rule-email-recovery-review'}}
template(name="syncRuleEmailLegacyCommands")
section.sync-rule-email-legacy-commands
h2 {{_ 'rule-email-legacy-heading'}}
p.quiet {{_ 'rule-email-legacy-description'}}
if error
p.error(role="alert") {{error}}
if rows.length
table
thead
tr
th {{_ 'rule-email-legacy-source'}}
th {{_ 'rule-email-legacy-mail'}}
th {{_ 'rule-email-legacy-reason'}}
th {{_ 'actions'}}
tbody
each rows
tr(data-command=commandId)
td
p {{boardId}}
p.quiet {{cardId}}
td
p {{to}}
p.quiet {{subject}}
td {{_ reasonKey}}
td
button.js-rule-email-legacy-rebind(type="button" disabled=rebindDisabled) {{_ 'rule-email-legacy-rebind'}}
button.js-rule-email-legacy-discard(type="button" disabled=busy) {{_ 'rule-email-legacy-discard'}}
if hasPages
button.js-rule-email-legacy-prev(type="button" disabled=noPrev) ‹
span {{pageLabel}}
button.js-rule-email-legacy-next(type="button" disabled=noNext) ›
else
p.quiet {{_ 'rule-email-legacy-empty'}}
//- A saved List Sync operation that can no longer be replayed blocks that
//- list's Sync; an administrator reviews and discards it here
//- (server/lib/listSyncStuck.js, docs/Features/Admin-Panel/Problems/Recovery.md).
template(name="listSyncStuckOperations")
section.list-sync-stuck-operations
h2 {{_ 'stuck-sync-operation-heading'}}
p.quiet {{_ 'stuck-sync-operation-description'}}
if error
p.error(role="alert") {{error}}
if rows.length
table
thead
tr
th {{_ 'stuck-sync-operation-list'}}
th {{_ 'stuck-sync-operation-progress'}}
th {{_ 'stuck-sync-operation-reason'}}
th {{_ 'actions'}}
tbody
each rows
tr(data-list=listId data-operation=operationId)
td
p {{_ 'board'}}: {{boardId}}
p {{_ 'list'}}: {{listId}}
p.quiet {{stuckAtText}}
td {{_ 'stuck-sync-operation-applied' applied=applied total=total}}
td
p {{_ reasonKey}}
if replayableNow
p.quiet {{_ 'stuck-sync-operation-replayable-now'}}
td
button.js-list-sync-stuck-discard(type="button" disabled=discardDisabled) {{_ 'stuck-sync-operation-discard'}}
if truncated
p.quiet {{_ 'stuck-sync-operation-truncated'}}
else
p.quiet {{_ 'stuck-sync-operation-empty'}}
button.js-list-sync-stuck-refresh(type="button" disabled=busy) {{_ 'stuck-sync-operation-refresh'}}
//- A board import that stopped before finishing left a partial board; an
//- administrator keeps it as it is or discards it here
//- (server/lib/importRuns.js, docs/Features/Board-Sidebar/Board-Settings/Import-Export/Import-Run-Recovery.md).
template(name="interruptedImports")
section.interrupted-imports
h2 {{_ 'interrupted-import-heading'}}
p.quiet {{_ 'interrupted-import-description'}}
if error
p.error(role="alert") {{error}}
if rows.length
table
thead
tr
th {{_ 'interrupted-import-board'}}
th {{_ 'interrupted-import-progress'}}
th {{_ 'interrupted-import-created'}}
th {{_ 'actions'}}
tbody
each rows
tr(data-run=runId)
td
p {{_ 'board'}}: {{boardLabel}}
p.quiet {{_ 'interrupted-import-source' source=source}}
p.quiet {{startedAtText}}
td
p {{_ stateKey stage=stage error=errorCode}}
if scrum
p.quiet {{_ 'interrupted-import-scrum' state=scrum.state next=scrum.next total=scrum.total}}
td
if boardExists
p {{_ 'interrupted-import-counts' swimlanes=counts.swimlanes lists=counts.lists cards=counts.cards checklists=counts.checklists comments=counts.comments attachments=counts.attachments}}
else
p {{_ 'interrupted-import-no-board'}}
td
button.js-interrupted-import-keep(type="button" disabled=keepDisabled) {{_ 'interrupted-import-keep'}}
button.js-interrupted-import-discard(type="button" disabled=discardDisabled) {{_ 'interrupted-import-discard'}}
if truncated
p.quiet {{_ 'interrupted-import-truncated'}}
else
p.quiet {{_ 'interrupted-import-empty'}}
button.js-interrupted-import-refresh(type="button" disabled=busy) {{_ 'interrupted-import-refresh'}}