Add email sending (service + 3 flows) and a general asset label generator
All checks were successful
CI / backend (push) Successful in 1m23s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

Email: a stdlib SMTP mailer (settings-first config, graceful no-op when
unconfigured), a test-email endpoint wired to the Email settings page,
forced first-login password change (users.mustchangepassword, migration
7d23, /change-password flow), new-user welcome mail, and on-demand
report/alert delivery (POST /api/reports/email + Email Report buttons)
with an external-cron-with-a-scoped-PAT path documented for automation.
All tests patch smtplib - no network.

Labels: a shared /print/asset-label/<type>/<id> view any asset detail
page opens - card or plain style, QR or barcode, configurable encoding.
Per-type qr_target_* templates plus label_default_style/codetype/encodes
settings on the Printing page. Measuring-tool labels default to encoding
their inspection-operation code (derived from the location name, e.g.
0615), so every tool in an area shares the area code - verified by
decoding the rendered QR. Machine labels default to the machine number;
blank-serial handled gracefully.

808 tests pass; both features verified live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 11:58:30 -04:00
parent 7d309aabeb
commit a846587f39
34 changed files with 1819 additions and 12 deletions

View File

@@ -40,6 +40,114 @@
</label>
</div>
<div class="setting-row">
<label>
<span>Machine label target</span>
<input
type="text"
v-model="settings.qr_target_machine"
placeholder="(blank = machine page)"
@blur="saveSetting('qr_target_machine', settings.qr_target_machine)"
:disabled="saving"
>
<small class="input-hint">Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}</small>
</label>
</div>
<div class="setting-row">
<label>
<span>Computer label target</span>
<input
type="text"
v-model="settings.qr_target_computer"
placeholder="(blank = computer page)"
@blur="saveSetting('qr_target_computer', settings.qr_target_computer)"
:disabled="saving"
>
<small class="input-hint">Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}</small>
</label>
</div>
<div class="setting-row">
<label>
<span>Network device label target</span>
<input
type="text"
v-model="settings.qr_target_network_device"
placeholder="(blank = network device page)"
@blur="saveSetting('qr_target_network_device', settings.qr_target_network_device)"
:disabled="saving"
>
<small class="input-hint">Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}</small>
</label>
</div>
<div class="setting-row">
<label>
<span>Measuring tool label target</span>
<input
type="text"
v-model="settings.qr_target_measuring_tool"
placeholder="(blank = measuring tool page)"
@blur="saveSetting('qr_target_measuring_tool', settings.qr_target_measuring_tool)"
:disabled="saving"
>
<small class="input-hint">Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}, {locationcode}, {locationname}</small>
</label>
</div>
<div class="setting-row">
<label>
<span>Default label style</span>
<select
v-model="settings.label_default_style"
@change="saveSetting('label_default_style', settings.label_default_style)"
:disabled="saving"
>
<option value="card">Card (badge with image and identity)</option>
<option value="plain">Plain (just the code and a caption)</option>
</select>
<small class="input-hint">House style used when an asset label first opens.</small>
</label>
</div>
<div class="setting-row">
<label>
<span>Default label code type</span>
<select
v-model="settings.label_default_codetype"
@change="saveSetting('label_default_codetype', settings.label_default_codetype)"
:disabled="saving"
>
<option value="qr">QR code</option>
<option value="barcode">Barcode (CODE128)</option>
</select>
<small class="input-hint">Code type used when an asset label first opens.</small>
</label>
</div>
<p class="setting-description">
What each asset type's label encodes by default (still overridable on
the label page itself).
</p>
<div class="setting-row" v-for="row in encodesRows" :key="row.key">
<label>
<span>{{ row.label }} label content</span>
<select
v-model="settings[row.key]"
@change="saveSetting(row.key, settings[row.key])"
:disabled="saving"
>
<option value="assetpage">Link to the asset page</option>
<option value="assetnumber">Asset / machine number</option>
<option value="serialnumber">Serial number</option>
<option v-if="row.hasLocation" value="location">Inspection location code</option>
<option value="custom">Custom target template</option>
</select>
</label>
</div>
<div class="setting-row">
<label>
<span>USB label style</span>
@@ -71,5 +179,14 @@ const {
loadSettings, saveSetting,
} = useSystemSettings()
// Per-asset-type "what does the code encode" defaults.
const encodesRows = [
{ key: 'label_default_encodes_machine', label: 'Machine' },
{ key: 'label_default_encodes_computer', label: 'Computer' },
{ key: 'label_default_encodes_printer', label: 'Printer' },
{ key: 'label_default_encodes_network_device', label: 'Network device' },
{ key: 'label_default_encodes_measuring_tool', label: 'Measuring tool', hasLocation: true },
]
onMounted(loadSettings)
</script>