printedparts stage 15: print-file revision history + role-based alerts
Some checks failed
CI / backend (push) Successful in 1m44s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s

printeditemfiles lands as the plugin's first incremental migration
(0002 on the plugin chain - the ADR-008 payoff). Revisions are
append-only per item: upload assigns the next number, records the
uploader from the JWT, enforces an extension allowlist and a 100 MB
cap; download serves the original filename; a permission-gated delete
covers wrong-file mistakes. The detail page gains the revision table
with a current badge. Unique storedfilename is sized 191 so the index
fits MySQL's 767-byte prefix - the per-plugin chain does not apply the
core env's ROW_FORMAT hook.

Alert recipients gain roles: Role joins the 0.13.0 surface, a role
picker on the settings page, and every active member of the selected
roles is folded into the deduped recipient list.
This commit is contained in:
cproudlock
2026-07-17 08:47:41 -04:00
parent 26b6b6b32f
commit aa4bfcd41c
14 changed files with 479 additions and 11 deletions

View File

@@ -49,6 +49,22 @@
</p>
</div>
<div class="form-group">
<label>Alert roles</label>
<div class="user-picker">
<label v-for="role in roles" :key="role.roleid" class="user-row">
<input type="checkbox" :value="String(role.roleid)"
v-model="selectedRoleids" />
<span>{{ role.rolename }}</span>
<span class="user-email">{{ role.description }}</span>
</label>
<p v-if="roles.length === 0" class="field-hint">No roles loaded</p>
</div>
<p class="field-hint">
Every active member of a selected role receives low-stock alerts.
</p>
</div>
<div class="form-group">
<label>Additional alert emails</label>
<input v-model="values.printedparts_alert_email" type="text"
@@ -76,7 +92,8 @@ const KEYS = [
'printedparts_default_threshold',
'printedparts_unknown_badge',
'printedparts_alert_email',
'printedparts_alert_userids'
'printedparts_alert_userids',
'printedparts_alert_roleids'
]
const values = ref({
@@ -84,10 +101,13 @@ const values = ref({
printedparts_default_threshold: 5,
printedparts_unknown_badge: 'deny',
printedparts_alert_email: '',
printedparts_alert_userids: ''
printedparts_alert_userids: '',
printedparts_alert_roleids: ''
})
const users = ref([])
const selectedUserids = ref([])
const roles = ref([])
const selectedRoleids = ref([])
const saving = ref(false)
const message = ref('')
const error = ref('')
@@ -106,6 +126,10 @@ onMounted(async () => {
const usersResponse = await usersApi.list()
users.value = (usersResponse.data.data || []).filter(
candidate => candidate.isactive && candidate.email)
selectedRoleids.value = (values.value.printedparts_alert_roleids || '')
.split(',').map(id => id.trim()).filter(Boolean)
const rolesResponse = await usersApi.roles.list()
roles.value = rolesResponse.data.data || []
} catch (loadError) {
error.value = 'Could not load settings'
console.error(loadError)
@@ -118,6 +142,7 @@ async function save() {
error.value = ''
try {
values.value.printedparts_alert_userids = selectedUserids.value.join(',')
values.value.printedparts_alert_roleids = selectedRoleids.value.join(',')
for (const key of KEYS) {
await settingsApi.update(key, String(values.value[key] ?? ''))
}