feat: refine ISAC media and notification workflows

This commit is contained in:
JB
2026-07-26 20:36:30 -04:00
parent 338ea8761d
commit 6ea45c05fa
37 changed files with 1002 additions and 138 deletions
+163
View File
@@ -39,6 +39,7 @@ const statements = schema
for (const statement of statements) {
// This index is deferred until the additive legacy-column migration below.
if (statement.startsWith('CREATE INDEX IF NOT EXISTS skill_chip_sets_ability_idx')) continue;
if (statement.startsWith('CREATE UNIQUE INDEX IF NOT EXISTS managed_images_source_entity_idx')) continue;
await database.exec(statement);
}
@@ -290,6 +291,168 @@ if (typeof specializationIconDefinition?.sql === 'string' && !specializationIcon
await database.exec('ALTER TABLE game_data_icons_specializations RENAME TO game_data_icons');
}
const managedImageColumns = await (await database.prepare("PRAGMA table_info('managed_images')")).all();
for (const [name, definition] of [
['source_entity_type', 'TEXT'],
['source_entity_id', 'TEXT']
]) {
if (!managedImageColumns.some((column) => column.name === name)) {
console.log(`Linking Image Library assets to ISAC entities: ${name}.`);
await database.exec(`ALTER TABLE managed_images ADD COLUMN ${name} ${definition}`);
}
}
await database.exec(`CREATE UNIQUE INDEX IF NOT EXISTS managed_images_source_entity_idx
ON managed_images(source_entity_type,source_entity_id)
WHERE source_entity_type IS NOT NULL AND source_entity_id IS NOT NULL`);
const iconLibraryCreator = await (await database.prepare(`SELECT users.id
FROM users
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
LEFT JOIN user_roles ON user_roles.user_id=users.id
LEFT JOIN roles ON roles.id=user_roles.role_id
LEFT JOIN role_permissions ON role_permissions.role_id=roles.id
LEFT JOIN permissions ON permissions.id=role_permissions.permission_id
WHERE deleted_accounts.user_id IS NULL
GROUP BY users.id,users.created_at
ORDER BY MAX(CASE WHEN roles.name='Admin' THEN 1
WHEN permissions.name='*' THEN 1 ELSE 0 END) DESC,
users.created_at ASC LIMIT 1`)).get();
const iconLibraryCategory = {
talent: 'Talents',
brand_set: 'Brands',
os_protocol: 'OS Protocols',
skill_chip: 'Skill Chips',
specialization: 'Specializations',
specialization_ability: 'Specializations',
specialization_node: 'Specializations'
};
const iconLibraryLabel = {
talent: 'Talent',
brand_set: 'Brand Set',
os_protocol: 'OS Protocol',
skill_chip: 'Skill Chip',
specialization: 'Specialization',
specialization_ability: 'Specialization Ability',
specialization_node: 'Specialization Tree Node'
};
const normalizeLibraryTags = (values) => [...new Set(values
.flatMap((value) => String(value ?? '').split(','))
.map((value) => value.normalize('NFKC').trim().toLocaleLowerCase())
.filter(Boolean))].slice(0, 20);
const linkedIconMetadata = async (entityType, entityId) => {
switch (entityType) {
case 'talent': {
const row = await (await database.prepare(`SELECT talent.name,talent.category,scope.scope
FROM talents talent LEFT JOIN talent_weapon_scopes scope ON scope.talent_id=talent.id
WHERE talent.id=?`)).get(entityId);
return { name: row?.name, tags: [row?.category === 'Vest' ? 'Body Armor' : row?.category, row?.scope] };
}
case 'brand_set': {
const row = await (await database.prepare('SELECT name FROM brand_sets WHERE id=?')).get(entityId);
return { name: row?.name, tags: ['gear', 'set bonus'] };
}
case 'os_protocol': {
const row = await (await database.prepare('SELECT name,quality,primary_attribute_type FROM os_protocols WHERE id=?')).get(entityId);
return { name: row?.name, tags: [row?.quality, row?.primary_attribute_type] };
}
case 'skill_chip': {
const row = await (await database.prepare(`SELECT chip.name,ability.name AS ability_name,specialization.name AS specialization_name
FROM skill_chip_sets chip
LEFT JOIN specialization_abilities ability ON ability.id=chip.specialization_ability_id
LEFT JOIN specializations specialization ON specialization.id=ability.specialization_id
WHERE chip.id=?`)).get(entityId);
return { name: row?.name, tags: [row?.ability_name, row?.specialization_name] };
}
case 'specialization': {
const row = await (await database.prepare('SELECT name FROM specializations WHERE id=?')).get(entityId);
return { name: row?.name, tags: ['class'] };
}
case 'specialization_ability': {
const row = await (await database.prepare(`SELECT ability.name,specialization.name AS specialization_name
FROM specialization_abilities ability
JOIN specializations specialization ON specialization.id=ability.specialization_id
WHERE ability.id=?`)).get(entityId);
return { name: row?.name, tags: [row?.specialization_name, 'ability'] };
}
case 'specialization_node': {
const row = await (await database.prepare(`SELECT node.name,node.row_number,focus.name AS focus_name,
specialization.name AS specialization_name
FROM specialization_tree_nodes node
JOIN specialization_focuses focus ON focus.id=node.focus_id
JOIN specializations specialization ON specialization.id=focus.specialization_id
WHERE node.id=?`)).get(entityId);
return { name: row?.name, tags: [row?.specialization_name, row?.focus_name, `row ${row?.row_number}`, 'tree node'] };
}
default:
return { name: null, tags: [] };
}
};
const uniqueLibraryName = async (preferred, entityType, entityId) => {
const candidates = [
preferred,
`${preferred} · ${iconLibraryLabel[entityType] ?? 'ISAC Asset'}`,
`${preferred} · ${entityId.slice(0, 8)}`
];
for (const candidate of candidates) {
const normalized = candidate.slice(0, 80);
const conflict = await (await database.prepare('SELECT id FROM managed_images WHERE name=? COLLATE NOCASE')).get(normalized);
if (!conflict) return normalized;
}
return `${preferred.slice(0, 64)} · ${entityId.slice(0, 8)}`;
};
if (iconLibraryCreator?.id) {
const unregisteredIcons = await (await database.prepare(`SELECT icon.*
FROM game_data_icons icon
LEFT JOIN managed_images image
ON image.source_entity_type=icon.entity_type AND image.source_entity_id=icon.entity_id
WHERE image.id IS NULL
ORDER BY icon.entity_type,icon.entity_id`)).all();
if (unregisteredIcons.length) console.log(`Registering ${unregisteredIcons.length} existing ISAC icons in the Image Library.`);
for (const icon of unregisteredIcons) {
const metadata = await linkedIconMetadata(String(icon.entity_type), String(icon.entity_id));
const label = iconLibraryLabel[icon.entity_type] ?? 'ISAC Asset';
const preferredName = String(metadata.name ?? `${label} ${String(icon.entity_id).slice(0, 8)}`);
const name = await uniqueLibraryName(preferredName, String(icon.entity_type), String(icon.entity_id));
const tags = normalizeLibraryTags([
'isac', 'game data', String(icon.entity_type).replaceAll('_', ' '), preferredName, ...metadata.tags
]);
const now = String(icon.updated_at ?? new Date().toISOString());
await (await database.prepare(`INSERT INTO managed_images
(id,name,category,tags_json,is_icon,force_white,storage_key,original_filename,byte_size,width,height,
source_entity_type,source_entity_id,created_by,created_at,updated_at)
VALUES(lower(hex(randomblob(4)))||'-'||lower(hex(randomblob(2)))||'-4'||substr(lower(hex(randomblob(2))),2)
||'-'||substr('89ab',abs(random())%4+1,1)||substr(lower(hex(randomblob(2))),2)||'-'||lower(hex(randomblob(6))),
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)).run(
name, iconLibraryCategory[icon.entity_type] ?? 'General', JSON.stringify(tags), 1, 0,
icon.storage_key, icon.storage_key, icon.byte_size, 256, 256,
icon.entity_type, icon.entity_id, iconLibraryCreator.id, now, now
);
}
const linkedIcons = await (await database.prepare(`SELECT icon.*,image.id AS library_image_id,image.tags_json
FROM game_data_icons icon
JOIN managed_images image
ON image.source_entity_type=icon.entity_type AND image.source_entity_id=icon.entity_id
ORDER BY icon.entity_type,icon.entity_id`)).all();
for (const icon of linkedIcons) {
const metadata = await linkedIconMetadata(String(icon.entity_type), String(icon.entity_id));
const preferredName = String(metadata.name ?? `${iconLibraryLabel[icon.entity_type] ?? 'ISAC Asset'} ${String(icon.entity_id).slice(0, 8)}`);
let existingTags = [];
try { existingTags = JSON.parse(String(icon.tags_json)); } catch { /* replace malformed legacy tags below */ }
existingTags = existingTags.filter((tag) => String(tag).toLocaleLowerCase() !== 'vest');
const tags = normalizeLibraryTags([
...existingTags, 'isac', 'game data', String(icon.entity_type).replaceAll('_', ' '), preferredName, ...metadata.tags
]);
await (await database.prepare(`UPDATE managed_images SET
category=?,tags_json=?,storage_key=?,original_filename=?,byte_size=?,width=256,height=256
WHERE id=?`)).run(
iconLibraryCategory[icon.entity_type] ?? 'General', JSON.stringify(tags),
icon.storage_key, icon.storage_key, icon.byte_size, icon.library_image_id
);
}
}
const gearAugmentColumns = await (await database.prepare("PRAGMA table_info('gear_tactical_augments')")).all();
if (gearAugmentColumns.length && !gearAugmentColumns.some((column) => column.name === 'effect_type')) {
console.log('Separating custom Gear Tactical Augments from fixed base-item percentage upgrades.');