List each account's statement files, and open them
The Accounts screen gets a Statements list: every file in every account folder beside what the index made of it -- imported, changed since, not imported yet (or failed), or gone from disk while its rows remain -- with its size, modified and import times, and how many transactions it brought in. Clicking a name opens the file. The list is read from the folders, not the index, through importer.StatementFiles, so a file shows exactly when import would read it; store.SourceFiles and importer.Checksum then say how far each has got. A file the index remembers but the disk lost is listed as missing rather than vanishing, since its rows would not survive a rebuild. A file is served only by finding it in that list, never by joining the requested name onto a path. Statements come from outside and are served from the app's origin, so none is rendered as a page: text is text/plain under CSP sandbox, anything not text or PDF is a sandboxed download, and PDFs -- whose viewers refuse a sandbox -- open in the browser's own isolated viewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -393,6 +393,42 @@ func (d *DB) Months() ([]string, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// SourceFileInfo is what the index recorded about one imported statement.
|
||||
type SourceFileInfo struct {
|
||||
AccountSlug string
|
||||
Path string // relative to the data root, as the importer records it
|
||||
SHA256 string
|
||||
ImportedAt string
|
||||
// Added counts the transactions this file introduced. A row seen again in
|
||||
// an overlapping statement is deduplicated and stays with the file that
|
||||
// brought it first, so this is not how many rows the file holds.
|
||||
Added int
|
||||
}
|
||||
|
||||
// SourceFiles lists every statement the index has imported.
|
||||
func (d *DB) SourceFiles() ([]SourceFileInfo, error) {
|
||||
rows, err := d.sql.Query(`
|
||||
SELECT a.slug, s.path, s.sha256, s.imported_at, COUNT(t.id)
|
||||
FROM source_files s
|
||||
JOIN accounts a ON a.id = s.account_id
|
||||
LEFT JOIN transactions t ON t.source_file_id = s.id
|
||||
GROUP BY s.id
|
||||
ORDER BY a.slug, s.path`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list source files: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []SourceFileInfo
|
||||
for rows.Next() {
|
||||
var f SourceFileInfo
|
||||
if err := rows.Scan(&f.AccountSlug, &f.Path, &f.SHA256, &f.ImportedAt, &f.Added); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// Tags lists every tag in use, for completion in the rule builder.
|
||||
func (d *DB) Tags() ([]string, error) {
|
||||
rows, err := d.sql.Query(`
|
||||
|
||||
Reference in New Issue
Block a user