feat: smart note history with manual/auto modes, delete entries, i18n fixes
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m16s

- Add noteHistoryMode setting (manual default / auto) with DB migration
- Manual mode: commit button in editor toolbar creates snapshots on demand
- Auto mode: smart snapshots with 20-char diff threshold + 5min cooldown,
  structural changes (color, pin, archive, labels) bypass cooldown
- Add delete individual history entries from history modal
- Fix sidebar: Notes nav no longer active on notebook pages
- Fix sidebar icon: replace filled Lightbulb with outlined FileText
- Fix title suggestions: change from amber to sky blue color scheme
- Fix hydration mismatch: add suppressHydrationWarning on locale dates
- Complete i18n: add history, sort, and AI chat translations for all 16 languages
- Translate French AI assistant section (40+ keys) from English to French
- Update README with new features and stack info

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:05:55 +02:00
parent ed807d3b2a
commit 69ea064ca8
40 changed files with 2110 additions and 250 deletions

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
require('dotenv').config({ path: path.join(__dirname, '..', '.env') })
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
stdio: 'inherit',
shell: process.platform === 'win32',
...options,
})
if (result.status !== 0) {
process.exit(result.status || 1)
}
}
function nowStamp() {
const d = new Date()
const pad = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
}
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
console.error('[safe-migrate] DATABASE_URL is missing in environment/.env')
process.exit(1)
}
const backupsDir = path.join(__dirname, '..', 'backups', 'migrations')
fs.mkdirSync(backupsDir, { recursive: true })
const isPostgres = databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://')
const isSqlite = databaseUrl.startsWith('file:')
console.log('[safe-migrate] Starting safe migration flow')
if (isPostgres) {
const backupFile = path.join(backupsDir, `pre_migrate_${nowStamp()}.sql`)
console.log(`[safe-migrate] Creating PostgreSQL backup: ${backupFile}`)
let dump = spawnSync(
'pg_dump',
['--no-owner', '--no-privileges', '--format=plain', '--file', backupFile, databaseUrl],
{ stdio: 'inherit', shell: process.platform === 'win32' }
)
if (dump.status !== 0) {
console.warn('[safe-migrate] Local pg_dump unavailable, trying Docker fallback...')
const pgUser = process.env.POSTGRES_USER || 'memento'
const pgDb = process.env.POSTGRES_DB || 'memento'
const dockerCmd = `docker exec memento-postgres pg_dump -U ${pgUser} -d ${pgDb} --no-owner --no-privileges --format=plain > "${backupFile}"`
dump = spawnSync(dockerCmd, { stdio: 'inherit', shell: true })
}
if (dump.status !== 0) {
console.error('[safe-migrate] Backup failed (local + docker). Migration aborted to protect data.')
process.exit(dump.status || 1)
}
} else if (isSqlite) {
const dbPath = databaseUrl.replace(/^file:/, '')
const absoluteDbPath = path.isAbsolute(dbPath) ? dbPath : path.join(__dirname, '..', dbPath)
if (fs.existsSync(absoluteDbPath)) {
const backupFile = path.join(backupsDir, `pre_migrate_${nowStamp()}.sqlite`)
console.log(`[safe-migrate] Creating SQLite backup: ${backupFile}`)
fs.copyFileSync(absoluteDbPath, backupFile)
} else {
console.warn(`[safe-migrate] SQLite file not found at ${absoluteDbPath}, skipping backup`)
}
} else {
console.warn('[safe-migrate] Unknown DATABASE_URL protocol, skipping backup step')
}
console.log('[safe-migrate] Applying migrations with prisma migrate deploy')
run('npx', ['prisma', 'migrate', 'deploy'])
console.log('[safe-migrate] Migration completed successfully')