File size: 9,963 Bytes
8c3e275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
from __future__ import annotations

import hashlib
import json
import sqlite3
from pathlib import Path

from pageparse.config import settings
from pageparse.schema import SourceParseResult


class Store:
    def __init__(self, db_path: str | Path | None = None) -> None:
        self.db_path = Path(db_path or settings.db_path)
        self._conn: sqlite3.Connection | None = None

    @property
    def conn(self) -> sqlite3.Connection:
        if self._conn is None:
            self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
            self._conn.row_factory = sqlite3.Row
        return self._conn

    def init_db(self) -> None:
        cursor = self.conn.cursor()
        try:
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='pages'")
            has_pages = cursor.fetchone() is not None
        except sqlite3.OperationalError:
            has_pages = False

        try:
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sources'")
            has_sources = cursor.fetchone() is not None
        except sqlite3.OperationalError:
            has_sources = False

        self.conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS sources (
                id            INTEGER PRIMARY KEY AUTOINCREMENT,
                filename      TEXT NOT NULL,
                source_type   TEXT CHECK (source_type IN ('image','audio','video','document','spreadsheet')),
                source_path   TEXT,
                captured_date TEXT,
                title         TEXT,
                summary       TEXT,
                raw_text      TEXT,
                created_at    TEXT DEFAULT (datetime('now')),
                image_path    TEXT,
                cleaned_image_path TEXT,
                content_hash  TEXT,
                barcodes      TEXT,
                tables        TEXT
            );

            CREATE TABLE IF NOT EXISTS records (
                id          INTEGER PRIMARY KEY AUTOINCREMENT,
                source_id   INTEGER REFERENCES sources(id),
                type        TEXT,
                content     TEXT NOT NULL,
                due_date    TEXT,
                priority    TEXT CHECK (priority IN ('high','medium','low') OR priority IS NULL),
                category    TEXT,
                speaker     TEXT,
                timestamp   TEXT,
                status      TEXT DEFAULT 'todo',
                confidence  REAL,
                user_edited INTEGER DEFAULT 0
            );
        """
        )

        try:
            cursor.execute("PRAGMA table_info(sources)")
            existing_columns = {row[1] for row in cursor.fetchall()}
            for col_name, col_type in [("content_hash", "TEXT"), ("barcodes", "TEXT"), ("tables", "TEXT")]:
                if col_name not in existing_columns:
                    self.conn.execute(f"ALTER TABLE sources ADD COLUMN {col_name} {col_type}")
            self.conn.commit()
        except Exception as e:
            print(f"Error migrating sources table columns: {e}")

        if has_pages and not has_sources:
            try:
                self.conn.execute(
                    """
                    INSERT INTO sources (id, filename, source_type, captured_date, raw_text, created_at, image_path, cleaned_image_path, summary)
                    SELECT id, filename, 'image', captured_date, raw_ocr_text, created_at, image_path, cleaned_image_path, summary
                    FROM pages
                """
                )
                self.conn.execute(
                    """
                    INSERT INTO records (source_id, type, content, due_date, priority, category, status, confidence)
                    SELECT page_id, 'task', task, due_date, priority, category, status, ocr_confidence
                    FROM tasks
                """
                )
                self.conn.execute("DROP TABLE tasks")
                self.conn.execute("DROP TABLE pages")
                self.conn.commit()
            except Exception as e:
                print(f"Migration error: {e}")
                self.conn.rollback()

        self.conn.commit()

    def save(
        self,
        result: SourceParseResult,
        raw_text: str,
        image_path: str | None = None,
        cleaned_image_path: str | None = None,
        content_hash: str | None = None,
    ) -> int:
        cursor = self.conn.execute(
            "INSERT INTO sources (filename, source_type, captured_date, title, summary, raw_text, image_path, cleaned_image_path, content_hash, barcodes, tables) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                result.source_file,
                result.source_type,
                str(result.captured_date) if result.captured_date else None,
                result.title,
                result.summary,
                raw_text,
                image_path,
                cleaned_image_path,
                content_hash,
                json.dumps(result.barcodes) if result.barcodes else None,
                json.dumps(result.tables) if result.tables else None,
            ),
        )
        source_id = cursor.lastrowid
        assert source_id is not None

        for record in result.records:
            self.conn.execute(
                "INSERT INTO records (source_id, type, content, due_date, priority, category, speaker, timestamp, status, confidence) "
                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                (
                    source_id,
                    record.type,
                    record.content,
                    str(record.due_date) if record.due_date else None,
                    record.priority,
                    record.category,
                    record.speaker,
                    record.timestamp,
                    record.status,
                    record.confidence,
                ),
            )
        self.conn.commit()

        self._fire_webhook(source_id, result, raw_text)
        return source_id

    def _fire_webhook(self, source_id: int, result: SourceParseResult, raw_text: str) -> None:
        url = settings.webhook_url
        if not url:
            return
        try:
            import httpx
            payload = {
                "event": "source.created",
                "source_id": source_id,
                "source_file": result.source_file,
                "source_type": result.source_type,
                "records_count": len(result.records),
                "records": [r.model_dump(mode="json") for r in result.records],
                "raw_text": raw_text[:5000],
            }
            headers = {"Content-Type": "application/json"}
            if settings.webhook_secret:
                headers["X-Webhook-Secret"] = settings.webhook_secret
            httpx.post(url, json=payload, headers=headers, timeout=10)
        except Exception as e:
            print(f"Webhook notification failed: {e}")

    def update_source_summary(self, source_id: int, summary: str) -> bool:
        self.conn.execute(
            "UPDATE sources SET summary = ? WHERE id = ?",
            (summary, source_id),
        )
        self.conn.commit()
        return True

    def list_sources(self) -> list[dict]:
        rows = self.conn.execute("SELECT * FROM sources ORDER BY created_at DESC").fetchall()
        return [dict(r) for r in rows]

    def get_records(
        self,
        source_id: int | None = None,
        priority: str | None = None,
        type: str | None = None,
    ) -> list[dict]:
        query = "SELECT r.*, s.filename FROM records r JOIN sources s ON r.source_id = s.id"
        params: list = []
        conditions = []
        if source_id is not None:
            conditions.append("r.source_id = ?")
            params.append(source_id)
        if priority:
            conditions.append("r.priority = ?")
            params.append(priority)
        if type:
            conditions.append("r.type = ?")
            params.append(type)
        if conditions:
            query += " WHERE " + " AND ".join(conditions)
        query += " ORDER BY r.id DESC"
        rows = self.conn.execute(query, params).fetchall()
        return [dict(r) for r in rows]

    def update_record(self, record_id: int, updates: dict) -> bool:
        if not updates:
            return False
        fields = []
        params = []
        for k, v in updates.items():
            if k in (
                "content",
                "due_date",
                "priority",
                "category",
                "status",
                "confidence",
                "speaker",
                "timestamp",
                "type",
                "user_edited",
            ):
                fields.append(f"{k} = ?")
                params.append(v)
        if not fields:
            return False
        params.append(record_id)
        self.conn.execute(f"UPDATE records SET {', '.join(fields)} WHERE id = ?", params)
        self.conn.commit()
        return True

    def delete_record(self, record_id: int) -> bool:
        self.conn.execute("DELETE FROM records WHERE id = ?", (record_id,))
        self.conn.commit()
        return True

    def get_source_by_hash(self, content_hash: str) -> dict | None:
        row = self.conn.execute(
            "SELECT * FROM sources WHERE content_hash = ?", (content_hash,)
        ).fetchone()
        return dict(row) if row else None

    def get_stats(self) -> dict:
        source_count = self.conn.execute("SELECT COUNT(*) FROM sources").fetchone()[0]
        record_count = self.conn.execute("SELECT COUNT(*) FROM records").fetchone()[0]
        return {
            "sources": source_count,
            "records": record_count,
        }

    def close(self) -> None:
        if self._conn is not None:
            self._conn.close()
            self._conn = None