Generated by Spark: I think namespaces might of messed up the schema - It could detect this and wipe the slate clean. Update IndexedDB and Flask.

This commit is contained in:
2026-01-17 20:07:27 +00:00
committed by GitHub
parent c143b3d586
commit 18e211b774
4 changed files with 471 additions and 40 deletions

View File

@@ -30,26 +30,105 @@ def get_db():
conn.row_factory = sqlite3.Row
return conn
def check_and_migrate_schema():
"""Check if schema needs migration and perform it if necessary"""
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
namespaces_exists = cursor.fetchone() is not None
cursor.execute("PRAGMA table_info(snippets)")
columns = [row[1] for row in cursor.fetchall()]
has_namespace_id = 'namespaceId' in columns
if not namespaces_exists or not has_namespace_id:
print("Schema migration needed - recreating tables with namespace support...")
cursor.execute("DROP TABLE IF EXISTS snippets")
cursor.execute("DROP TABLE IF EXISTS namespaces")
cursor.execute('''
CREATE TABLE namespaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
createdAt INTEGER NOT NULL,
isDefault INTEGER DEFAULT 0
)
''')
cursor.execute('''
CREATE TABLE snippets (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
code TEXT NOT NULL,
language TEXT NOT NULL,
category TEXT NOT NULL,
namespaceId TEXT,
hasPreview INTEGER DEFAULT 0,
functionName TEXT,
inputParameters TEXT,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
)
''')
cursor.execute('''
INSERT INTO namespaces (id, name, createdAt, isDefault)
VALUES ('default', 'Default', ?, 1)
''', (int(datetime.utcnow().timestamp() * 1000),))
conn.commit()
print("Schema migration completed")
conn.close()
def init_db():
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS namespaces (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
createdAt INTEGER NOT NULL,
isDefault INTEGER DEFAULT 0
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS snippets (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
code TEXT NOT NULL,
language TEXT NOT NULL,
description TEXT,
tags TEXT,
category TEXT DEFAULT 'general',
componentName TEXT,
previewParams TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
category TEXT NOT NULL,
namespaceId TEXT,
hasPreview INTEGER DEFAULT 0,
functionName TEXT,
inputParameters TEXT,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
FOREIGN KEY (namespaceId) REFERENCES namespaces(id)
)
''')
cursor.execute("SELECT COUNT(*) FROM namespaces WHERE isDefault = 1")
default_count = cursor.fetchone()[0]
if default_count == 0:
cursor.execute('''
INSERT INTO namespaces (id, name, createdAt, isDefault)
VALUES ('default', 'Default', ?, 1)
''', (int(datetime.utcnow().timestamp() * 1000),))
conn.commit()
conn.close()
check_and_migrate_schema()
@app.route('/health', methods=['GET'])
def health():
@@ -67,10 +146,12 @@ def get_snippets():
snippets = []
for row in rows:
snippet = dict(row)
if snippet.get('tags'):
snippet['tags'] = json.loads(snippet['tags'])
if snippet.get('previewParams'):
snippet['previewParams'] = json.loads(snippet['previewParams'])
if snippet.get('inputParameters'):
try:
snippet['inputParameters'] = json.loads(snippet['inputParameters'])
except:
snippet['inputParameters'] = None
snippet['hasPreview'] = bool(snippet.get('hasPreview', 0))
snippets.append(snippet)
return jsonify(snippets)
@@ -90,10 +171,12 @@ def get_snippet(snippet_id):
return jsonify({'error': 'Snippet not found'}), 404
snippet = dict(row)
if snippet.get('tags'):
snippet['tags'] = json.loads(snippet['tags'])
if snippet.get('previewParams'):
snippet['previewParams'] = json.loads(snippet['previewParams'])
if snippet.get('inputParameters'):
try:
snippet['inputParameters'] = json.loads(snippet['inputParameters'])
except:
snippet['inputParameters'] = None
snippet['hasPreview'] = bool(snippet.get('hasPreview', 0))
return jsonify(snippet)
except Exception as e:
@@ -106,24 +189,32 @@ def create_snippet():
conn = get_db()
cursor = conn.cursor()
tags_json = json.dumps(data.get('tags', []))
preview_params_json = json.dumps(data.get('previewParams', {}))
input_params_json = json.dumps(data.get('inputParameters')) if data.get('inputParameters') else None
created_at = data.get('createdAt')
if isinstance(created_at, str):
created_at = int(datetime.fromisoformat(created_at.replace('Z', '+00:00')).timestamp() * 1000)
updated_at = data.get('updatedAt')
if isinstance(updated_at, str):
updated_at = int(datetime.fromisoformat(updated_at.replace('Z', '+00:00')).timestamp() * 1000)
cursor.execute('''
INSERT INTO snippets (id, title, code, language, description, tags, category, componentName, previewParams, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO snippets (id, title, description, code, language, category, namespaceId, hasPreview, functionName, inputParameters, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data['id'],
data['title'],
data.get('description', ''),
data['code'],
data['language'],
data.get('description', ''),
tags_json,
data.get('category', 'general'),
data.get('componentName', ''),
preview_params_json,
data['createdAt'],
data['updatedAt']
data.get('namespaceId'),
1 if data.get('hasPreview') else 0,
data.get('functionName'),
input_params_json,
created_at,
updated_at
))
conn.commit()
@@ -140,23 +231,27 @@ def update_snippet(snippet_id):
conn = get_db()
cursor = conn.cursor()
tags_json = json.dumps(data.get('tags', []))
preview_params_json = json.dumps(data.get('previewParams', {}))
input_params_json = json.dumps(data.get('inputParameters')) if data.get('inputParameters') else None
updated_at = data.get('updatedAt')
if isinstance(updated_at, str):
updated_at = int(datetime.fromisoformat(updated_at.replace('Z', '+00:00')).timestamp() * 1000)
cursor.execute('''
UPDATE snippets
SET title = ?, code = ?, language = ?, description = ?, tags = ?, category = ?, componentName = ?, previewParams = ?, updatedAt = ?
SET title = ?, description = ?, code = ?, language = ?, category = ?, namespaceId = ?, hasPreview = ?, functionName = ?, inputParameters = ?, updatedAt = ?
WHERE id = ?
''', (
data['title'],
data.get('description', ''),
data['code'],
data['language'],
data.get('description', ''),
tags_json,
data.get('category', 'general'),
data.get('componentName', ''),
preview_params_json,
data['updatedAt'],
data.get('namespaceId'),
1 if data.get('hasPreview') else 0,
data.get('functionName'),
input_params_json,
updated_at,
snippet_id
))
@@ -186,6 +281,104 @@ def delete_snippet(snippet_id):
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/namespaces', methods=['GET'])
def get_namespaces():
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
rows = cursor.fetchall()
conn.close()
namespaces = []
for row in rows:
namespace = dict(row)
namespace['isDefault'] = bool(namespace.get('isDefault', 0))
namespaces.append(namespace)
return jsonify(namespaces)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/namespaces', methods=['POST'])
def create_namespace():
try:
data = request.json
conn = get_db()
cursor = conn.cursor()
created_at = data.get('createdAt')
if isinstance(created_at, str):
created_at = int(datetime.fromisoformat(created_at.replace('Z', '+00:00')).timestamp() * 1000)
cursor.execute('''
INSERT INTO namespaces (id, name, createdAt, isDefault)
VALUES (?, ?, ?, ?)
''', (
data['id'],
data['name'],
created_at,
1 if data.get('isDefault') else 0
))
conn.commit()
conn.close()
return jsonify(data), 201
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/namespaces/<namespace_id>', methods=['DELETE'])
def delete_namespace(namespace_id):
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT isDefault FROM namespaces WHERE id = ?', (namespace_id,))
row = cursor.fetchone()
if not row:
conn.close()
return jsonify({'error': 'Namespace not found'}), 404
if row['isDefault']:
conn.close()
return jsonify({'error': 'Cannot delete default namespace'}), 400
cursor.execute('SELECT id FROM namespaces WHERE isDefault = 1')
default_row = cursor.fetchone()
default_id = default_row['id'] if default_row else 'default'
cursor.execute('UPDATE snippets SET namespaceId = ? WHERE namespaceId = ?', (default_id, namespace_id))
cursor.execute('DELETE FROM namespaces WHERE id = ?', (namespace_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/wipe', methods=['POST'])
def wipe_database():
"""Emergency endpoint to wipe and recreate the database"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS snippets")
cursor.execute("DROP TABLE IF EXISTS namespaces")
conn.commit()
conn.close()
init_db()
return jsonify({'success': True, 'message': 'Database wiped and recreated'})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5000, debug=False)

View File

@@ -112,6 +112,64 @@ function saveToLocalStorage(data: Uint8Array): boolean {
}
}
async function validateSchema(db: Database): Promise<boolean> {
try {
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
if (snippetsCheck.length === 0) return true
const columns = snippetsCheck[0].values.map(row => row[1] as string)
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
for (const col of requiredColumns) {
if (!columns.includes(col)) {
console.warn(`Schema validation failed: missing column '${col}'`)
return false
}
}
const namespacesCheck = db.exec("PRAGMA table_info(namespaces)")
if (namespacesCheck.length === 0) {
console.warn('Schema validation failed: namespaces table missing')
return false
}
return true
} catch (error) {
console.error('Schema validation error:', error)
return false
}
}
async function wipeAndRecreateDB(): Promise<void> {
console.warn('Wiping corrupted database and creating fresh schema...')
await saveToIndexedDB(new Uint8Array())
saveToLocalStorage(new Uint8Array())
const idb = await openIndexedDB()
if (idb) {
try {
const transaction = idb.transaction([IDB_STORE], 'readwrite')
const store = transaction.objectStore(IDB_STORE)
await new Promise<void>((resolve) => {
const request = store.delete(DB_KEY)
request.onsuccess = () => resolve()
request.onerror = () => resolve()
})
} catch (error) {
console.warn('Error clearing IndexedDB:', error)
}
}
try {
localStorage.removeItem(DB_KEY)
} catch (error) {
console.warn('Error clearing localStorage:', error)
}
dbInstance = null
}
export async function initDB(): Promise<Database> {
if (dbInstance) return dbInstance
@@ -122,6 +180,7 @@ export async function initDB(): Promise<Database> {
}
let loadedData: Uint8Array | null = null
let schemaValid = false
loadedData = await loadFromIndexedDB()
@@ -129,11 +188,22 @@ export async function initDB(): Promise<Database> {
loadedData = loadFromLocalStorage()
}
if (loadedData) {
if (loadedData && loadedData.length > 0) {
try {
dbInstance = new sqlInstance.Database(loadedData)
const testDb = new sqlInstance.Database(loadedData)
schemaValid = await validateSchema(testDb)
if (schemaValid) {
dbInstance = testDb
} else {
console.warn('Schema validation failed, wiping database')
testDb.close()
await wipeAndRecreateDB()
dbInstance = new sqlInstance.Database()
}
} catch (error) {
console.error('Failed to load saved database, creating new one:', error)
await wipeAndRecreateDB()
dbInstance = new sqlInstance.Database()
}
} else {
@@ -836,6 +906,12 @@ export async function getDatabaseStats(): Promise<{
}
export async function clearDatabase(): Promise<void> {
const adapter = getFlaskAdapter()
if (adapter) {
await adapter.wipeDatabase()
return
}
const db = await openIndexedDB()
if (db) {
try {
@@ -875,6 +951,11 @@ export async function syncTemplatesFromJSON(templates: SnippetTemplate[]): Promi
}
export async function getAllNamespaces(): Promise<import('./types').Namespace[]> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.getAllNamespaces()
}
const db = await initDB()
const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC')
@@ -898,14 +979,20 @@ export async function getAllNamespaces(): Promise<import('./types').Namespace[]>
}
export async function createNamespace(name: string): Promise<import('./types').Namespace> {
const db = await initDB()
const namespace: import('./types').Namespace = {
id: Date.now().toString(),
name,
createdAt: Date.now(),
isDefault: false
}
const adapter = getFlaskAdapter()
if (adapter) {
await adapter.createNamespace(namespace)
return namespace
}
const db = await initDB()
db.run(
`INSERT INTO namespaces (id, name, createdAt, isDefault)
@@ -918,6 +1005,11 @@ export async function createNamespace(name: string): Promise<import('./types').N
}
export async function deleteNamespace(id: string): Promise<void> {
const adapter = getFlaskAdapter()
if (adapter) {
return await adapter.deleteNamespace(id)
}
const db = await initDB()
const defaultNamespace = db.exec('SELECT id FROM namespaces WHERE isDefault = 1')
@@ -1009,3 +1101,34 @@ export async function getNamespaceById(id: string): Promise<import('./types').Na
return namespace
}
export async function validateDatabaseSchema(): Promise<{ valid: boolean; issues: string[] }> {
try {
const db = await initDB()
const issues: string[] = []
const snippetsCheck = db.exec("PRAGMA table_info(snippets)")
if (snippetsCheck.length === 0) {
issues.push('Snippets table missing')
return { valid: false, issues }
}
const columns = snippetsCheck[0].values.map(row => row[1] as string)
const requiredColumns = ['id', 'title', 'code', 'language', 'category', 'namespaceId', 'createdAt', 'updatedAt']
for (const col of requiredColumns) {
if (!columns.includes(col)) {
issues.push(`Missing column '${col}' in snippets table`)
}
}
const namespacesCheck = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='namespaces'")
if (namespacesCheck.length === 0) {
issues.push('Namespaces table missing')
}
return { valid: issues.length === 0, issues }
} catch (error) {
return { valid: false, issues: ['Failed to validate schema: ' + (error as Error).message] }
}
}

View File

@@ -186,4 +186,53 @@ export class FlaskStorageAdapter {
async migrateToIndexedDB(): Promise<Snippet[]> {
return this.getAllSnippets()
}
async getAllNamespaces(): Promise<import('./types').Namespace[]> {
if (!this.isValidUrl()) {
throw new Error('Invalid Flask backend URL')
}
const response = await fetch(`${this.baseUrl}/api/namespaces`)
if (!response.ok) {
throw new Error(`Failed to fetch namespaces: ${response.statusText}`)
}
return await response.json()
}
async createNamespace(namespace: import('./types').Namespace): Promise<void> {
if (!this.isValidUrl()) {
throw new Error('Invalid Flask backend URL')
}
const response = await fetch(`${this.baseUrl}/api/namespaces`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(namespace)
})
if (!response.ok) {
throw new Error(`Failed to create namespace: ${response.statusText}`)
}
}
async deleteNamespace(id: string): Promise<void> {
if (!this.isValidUrl()) {
throw new Error('Invalid Flask backend URL')
}
const response = await fetch(`${this.baseUrl}/api/namespaces/${id}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error(`Failed to delete namespace: ${response.statusText}`)
}
}
async wipeDatabase(): Promise<void> {
if (!this.isValidUrl()) {
throw new Error('Invalid Flask backend URL')
}
const response = await fetch(`${this.baseUrl}/api/wipe`, {
method: 'POST'
})
if (!response.ok) {
throw new Error(`Failed to wipe database: ${response.statusText}`)
}
}
}

View File

@@ -4,8 +4,8 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/com
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Database, Download, Upload, Trash, CloudArrowUp, CloudCheck, CloudSlash } from '@phosphor-icons/react'
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets } from '@/lib/db'
import { Database, Download, Upload, Trash, CloudArrowUp, CloudCheck, CloudSlash, FirstAid, CheckCircle, Warning } from '@phosphor-icons/react'
import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets, validateDatabaseSchema } from '@/lib/db'
import { toast } from 'sonner'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
@@ -30,6 +30,8 @@ export function SettingsPage() {
const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown')
const [testingConnection, setTestingConnection] = useState(false)
const [envVarSet, setEnvVarSet] = useState(false)
const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown')
const [checkingSchema, setCheckingSchema] = useState(false)
const loadStats = async () => {
setLoading(true)
@@ -60,8 +62,26 @@ export function SettingsPage() {
}
}
const checkSchemaHealth = async () => {
setCheckingSchema(true)
try {
const result = await validateDatabaseSchema()
setSchemaHealth(result.valid ? 'healthy' : 'corrupted')
if (!result.valid) {
console.warn('Schema validation failed:', result.issues)
}
} catch (error) {
console.error('Schema check failed:', error)
setSchemaHealth('corrupted')
} finally {
setCheckingSchema(false)
}
}
useEffect(() => {
loadStats()
checkSchemaHealth()
const config = loadStorageConfig()
const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL
@@ -116,8 +136,9 @@ export function SettingsPage() {
try {
await clearDatabase()
toast.success('Database cleared successfully')
toast.success('Database cleared and schema recreated successfully')
await loadStats()
await checkSchemaHealth()
} catch (error) {
console.error('Failed to clear:', error)
toast.error('Failed to clear database')
@@ -247,6 +268,51 @@ export function SettingsPage() {
</div>
<div className="grid gap-6 max-w-3xl">
{schemaHealth === 'corrupted' && (
<Card className="border-destructive bg-destructive/10">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<Warning weight="fill" size={24} />
Schema Corruption Detected
</CardTitle>
<CardDescription>
Your database schema is outdated or corrupted and needs to be repaired
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="border-destructive">
<AlertDescription>
The database schema is missing required tables or columns (likely due to namespace feature addition).
This can cause errors when loading or saving snippets. Click the button below to wipe and recreate the database with the correct schema.
</AlertDescription>
</Alert>
<div className="flex gap-2">
<Button onClick={handleClear} variant="destructive" className="gap-2">
<FirstAid weight="bold" size={16} />
Repair Database (Wipe & Recreate)
</Button>
<Button onClick={checkSchemaHealth} variant="outline" disabled={checkingSchema}>
{checkingSchema ? 'Checking...' : 'Re-check Schema'}
</Button>
</div>
</CardContent>
</Card>
)}
{schemaHealth === 'healthy' && (
<Card className="border-green-600 bg-green-600/10">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-green-600">
<CheckCircle weight="fill" size={24} />
Schema Healthy
</CardTitle>
<CardDescription>
Your database schema is up to date and functioning correctly
</CardDescription>
</CardHeader>
</Card>
)}
{envVarSet && (
<Card className="border-accent">
<CardHeader>