License rules are the source of truth for sellability
All checks were successful
build-and-push / docker (push) Successful in 28s
All checks were successful
build-and-push / docker (push) Successful in 28s
- Changing a license policy applies to all models with that license, and opening Settings reconciles old imports to current rules (store.reconcileModelsToRules) - Editing a model's license re-resolves its sellability from the rule - Removed the per-model commercial-use override (set by license now) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2768a71c63
commit
a2e08ecd03
4 changed files with 16 additions and 13 deletions
|
|
@ -241,6 +241,14 @@ async function applyLicenseRuleToModels(name, commercial) {
|
||||||
await db.query('UPDATE models SET commercial_use = $2 WHERE license = $1', [name, commercial ?? null]);
|
await db.query('UPDATE models SET commercial_use = $2 WHERE license = $1', [name, commercial ?? null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Align every model's commercial_use to its license rule (license = source of truth).
|
||||||
|
async function reconcileModelsToRules() {
|
||||||
|
await db.query(`
|
||||||
|
UPDATE models m SET commercial_use = lr.commercial
|
||||||
|
FROM license_rules lr
|
||||||
|
WHERE m.license = lr.name AND m.commercial_use IS DISTINCT FROM lr.commercial`);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- users / auth
|
// ---------------------------------------------------------------- users / auth
|
||||||
async function countUsers() {
|
async function countUsers() {
|
||||||
const { rows } = await db.query('SELECT COUNT(*)::int AS n FROM users');
|
const { rows } = await db.query('SELECT COUNT(*)::int AS n FROM users');
|
||||||
|
|
@ -321,4 +329,5 @@ module.exports = {
|
||||||
countUsers, getUserByEmail, getUserById, createUser, updateUserPassword,
|
countUsers, getUserByEmail, getUserById, createUser, updateUserPassword,
|
||||||
createPasswordReset, getValidReset, markResetUsed,
|
createPasswordReset, getValidReset, markResetUsed,
|
||||||
getLicenseRule, upsertLicenseRule, listLicenseRules, distinctModelLicenses, applyLicenseRuleToModels,
|
getLicenseRule, upsertLicenseRule, listLicenseRules, distinctModelLicenses, applyLicenseRuleToModels,
|
||||||
|
reconcileModelsToRules,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -557,8 +557,6 @@ function openEditModal() {
|
||||||
document.getElementById('editDescription').value = model.description || '';
|
document.getElementById('editDescription').value = model.description || '';
|
||||||
document.getElementById('editTags').value = (model.tags || []).join(', ');
|
document.getElementById('editTags').value = (model.tags || []).join(', ');
|
||||||
document.getElementById('editLicense').value = model.license || '';
|
document.getElementById('editLicense').value = model.license || '';
|
||||||
document.getElementById('editCommercial').value =
|
|
||||||
model.commercial_use === true ? 'true' : model.commercial_use === false ? 'false' : '';
|
|
||||||
|
|
||||||
const ps = model.print_settings || {};
|
const ps = model.print_settings || {};
|
||||||
document.getElementById('editMaterial').value = ps.material || '';
|
document.getElementById('editMaterial').value = ps.material || '';
|
||||||
|
|
@ -577,12 +575,10 @@ function openEditModal() {
|
||||||
async function handleEditModel(e) {
|
async function handleEditModel(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const id = editModal.dataset.editId;
|
const id = editModal.dataset.editId;
|
||||||
const commercial = document.getElementById('editCommercial').value;
|
|
||||||
const body = {
|
const body = {
|
||||||
name: document.getElementById('editName').value,
|
name: document.getElementById('editName').value,
|
||||||
description: document.getElementById('editDescription').value,
|
description: document.getElementById('editDescription').value,
|
||||||
license: document.getElementById('editLicense').value || null,
|
license: document.getElementById('editLicense').value || null,
|
||||||
commercial_use: commercial === '' ? null : commercial === 'true',
|
|
||||||
tags: splitTags(document.getElementById('editTags').value),
|
tags: splitTags(document.getElementById('editTags').value),
|
||||||
projects: Array.from(document.getElementById('editProjects').selectedOptions).map((o) => Number(o.value)),
|
projects: Array.from(document.getElementById('editProjects').selectedOptions).map((o) => Number(o.value)),
|
||||||
print_settings: {
|
print_settings: {
|
||||||
|
|
|
||||||
|
|
@ -433,14 +433,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="editLicense">License</label>
|
<label for="editLicense">License</label>
|
||||||
<input type="text" id="editLicense">
|
<input type="text" id="editLicense">
|
||||||
</div>
|
<p class="form-hint">Sellable/non-commercial is set per license in Settings → License types.</p>
|
||||||
<div class="form-group">
|
|
||||||
<label for="editCommercial">Commercial use (sellable?)</label>
|
|
||||||
<select id="editCommercial">
|
|
||||||
<option value="">Unknown</option>
|
|
||||||
<option value="true">Allowed — sellable</option>
|
|
||||||
<option value="false">Non-commercial only</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Print Settings</label>
|
<label>Print Settings</label>
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@ app.get('/api/licenses', async (req, res, next) => {
|
||||||
for (const name of await store.distinctModelLicenses()) {
|
for (const name of await store.distinctModelLicenses()) {
|
||||||
if (!(await store.getLicenseRule(name))) await store.upsertLicenseRule(name, licenses.inferCommercial(name));
|
if (!(await store.getLicenseRule(name))) await store.upsertLicenseRule(name, licenses.inferCommercial(name));
|
||||||
}
|
}
|
||||||
|
await store.reconcileModelsToRules(); // keep old imports aligned to current policy
|
||||||
res.json(await store.listLicenseRules());
|
res.json(await store.listLicenseRules());
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|
@ -337,7 +338,11 @@ app.post('/api/models/:id/files', upload.array('files'), async (req, res, next)
|
||||||
|
|
||||||
app.put('/api/models/:id', async (req, res, next) => {
|
app.put('/api/models/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const model = await store.updateModel(req.params.id, req.body);
|
const updates = { ...req.body };
|
||||||
|
// Sellability is derived from the license rule, so re-resolve it if the
|
||||||
|
// license changed (registers the new license if it's not seen before).
|
||||||
|
if (updates.license !== undefined) updates.commercial_use = await resolveCommercial(updates.license);
|
||||||
|
const model = await store.updateModel(req.params.id, updates);
|
||||||
if (!model) return res.status(404).json({ error: 'Model not found' });
|
if (!model) return res.status(404).json({ error: 'Model not found' });
|
||||||
res.json(publicModel(model));
|
res.json(publicModel(model));
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue