mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
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:
259
backend/app.py
259
backend/app.py
@@ -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)
|
||||
|
||||
131
src/lib/db.ts
131
src/lib/db.ts
@@ -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] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user