diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 280180f..312d428 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,9 +6,12 @@
+
+
+
{
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Cache-Control', 'public, max-age=86400');
+ res.sendFile(path.join(__dirname, 'public', '.well-known', 'assetlinks.json'));
+});
+
app.use(loggingMiddleware);
app.use(express.static(path.join(__dirname, 'public'), {
setHeaders: (res, filePath) => {
@@ -134,7 +141,7 @@ app.get('/api/models', async (req, res) => {
const { activeProvider } = require('./src/utils/ai');
app.get('/api/health', (req, res) => {
res.json({
- status: 'running', version: '3.1.0', provider: activeProvider,
+ status: 'running', version: '9.0.0', provider: activeProvider,
timestamp: new Date().toISOString(),
openrouter: process.env.OPENROUTER_API_KEY ? 'configured' : 'missing',
bedrock: process.env.AWS_BEDROCK_REGION ? 'configured' : 'not configured',
@@ -197,7 +204,7 @@ const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log('');
console.log('==========================================');
- console.log('🏥 PEDIATRIC AI SCRIBE v3.1');
+ console.log('🏥 PEDIATRIC AI SCRIBE v9.0');
console.log('==========================================');
console.log('🌐 http://localhost:' + PORT);
console.log('🤖 Provider: ' + activeProvider);
diff --git a/src/routes/adminConfig.js b/src/routes/adminConfig.js
index 15eae0d..1e5a2ad 100644
--- a/src/routes/adminConfig.js
+++ b/src/routes/adminConfig.js
@@ -257,9 +257,21 @@ router.put('/config/models/toggle', async function(req, res) {
disabled = disabled.filter(function(id) { return id !== modelId; });
} else {
if (!disabled.includes(modelId)) disabled.push(modelId);
+
+ // Safety: prevent disabling all models
+ var { getAvailableModels } = require('../utils/models');
+ var allModels = getAvailableModels();
+ var customRaw = await db.getSetting('models.custom') || '[]';
+ var custom;
+ try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
+ var enabledCount = allModels.filter(function(m) { return !disabled.includes(m.id); }).length + custom.length;
+ if (enabledCount === 0) {
+ return res.status(400).json({ error: 'Cannot disable all models. At least one model must remain enabled.' });
+ }
}
await db.setSetting('models.disabled', JSON.stringify(disabled));
+ logger.audit(req.user.id, 'admin_model_toggle', (enabled ? 'Enabled' : 'Disabled') + ' model: ' + modelId, req, { category: 'admin' });
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});
@@ -270,16 +282,34 @@ router.post('/config/models/custom', async function(req, res) {
var { id, name, cost, category } = req.body;
if (!id || !name) return res.status(400).json({ error: 'id and name required' });
+ // Validate model ID format (provider/model-name)
+ var trimmedId = id.trim();
+ if (trimmedId.length > 200) return res.status(400).json({ error: 'Model ID too long (max 200 chars)' });
+ if (!/^[a-zA-Z0-9._\-\/\:]+$/.test(trimmedId)) {
+ return res.status(400).json({ error: 'Invalid model ID format. Use only letters, numbers, dots, dashes, slashes, and colons.' });
+ }
+
+ // Check if model ID conflicts with a built-in model
+ var { OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS } = require('../utils/models');
+ var allBuiltIn = [].concat(OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS);
+ if (allBuiltIn.find(function(m) { return m.id === trimmedId; })) {
+ return res.status(400).json({ error: 'Model ID conflicts with a built-in model. Use the toggle to enable/disable built-in models.' });
+ }
+
+ var validCategories = ['free', 'fast', 'smart', 'premium'];
+ var cat = validCategories.includes(category) ? category : 'smart';
+
var customRaw = await db.getSetting('models.custom') || '[]';
var custom;
try { custom = JSON.parse(customRaw); } catch(e) { custom = []; }
// Prevent duplicates
- custom = custom.filter(function(m) { return m.id !== id; });
- custom.push({ id: id.trim(), name: name.trim(), cost: cost || '?', category: category || 'smart', tag: 'CUSTOM' });
+ var existing = custom.find(function(m) { return m.id === trimmedId; });
+ custom = custom.filter(function(m) { return m.id !== trimmedId; });
+ custom.push({ id: trimmedId, name: name.trim().substring(0, 100), cost: (cost || '?').substring(0, 20), category: cat, tag: 'CUSTOM' });
await db.setSetting('models.custom', JSON.stringify(custom));
- logger.audit(req.user.id, 'admin_model_add', 'Added custom model: ' + id, req, { category: 'admin' });
+ logger.audit(req.user.id, existing ? 'admin_model_update' : 'admin_model_add', (existing ? 'Updated' : 'Added') + ' custom model: ' + trimmedId, req, { category: 'admin' });
res.json({ success: true });
} catch (e) { res.status(500).json({ error: e.message }); }
});