mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 01:32:19 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8228974c0c | ||
|
|
848eccb35b | ||
|
|
25747d7d37 | ||
|
|
b7d6cf8128 | ||
|
|
6589c2dd66 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -143,19 +143,19 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install git-cliff
|
- name: Install git-cliff
|
||||||
run: |
|
run: |
|
||||||
wget -qO- https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-2.10.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
wget -qO- https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-2.10.0-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
||||||
sudo mv git-cliff-*/git-cliff /usr/local/bin/
|
sudo mv git-cliff-*/git-cliff /usr/local/bin/
|
||||||
|
|
||||||
- name: Generate release notes
|
- name: Generate release notes
|
||||||
id: release_notes
|
id: release_notes
|
||||||
run: |
|
run: |
|
||||||
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||||
git-cliff --current >> $GITHUB_OUTPUT
|
git-cliff --current >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,4 +1,46 @@
|
|||||||
|
|
||||||
|
## 2025.9.17 (2025/09/18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **api:** Prevent duplicate notifications for existing transactions during sync. ([25747d7d](https://github.com/elisiariocouto/leggen/commit/25747d7d372e291090764a6814f9d8d0b76aea3b))
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Format files. ([848eccb3](https://github.com/elisiariocouto/leggen/commit/848eccb35b910c8121d15611547dca8da0b12756))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025.9.17 (2025/09/18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **api:** Prevent duplicate notifications for existing transactions during sync. ([25747d7d](https://github.com/elisiariocouto/leggen/commit/25747d7d372e291090764a6814f9d8d0b76aea3b))
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Format files. ([848eccb3](https://github.com/elisiariocouto/leggen/commit/848eccb35b910c8121d15611547dca8da0b12756))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025.9.16 (2025/09/18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **frontend:** Add iOS safe area support for PWA sticky header ([6589c2dd](https://github.com/elisiariocouto/leggen/commit/6589c2dd666f8605cf6d1bf9ad7277734d4cd302))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025.9.16 (2025/09/18)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **frontend:** Add iOS safe area support for PWA sticky header ([6589c2dd](https://github.com/elisiariocouto/leggen/commit/6589c2dd666f8605cf6d1bf9ad7277734d4cd302))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 2025.9.15 (2025/09/18)
|
## 2025.9.15 (2025/09/18)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
<TileColor>#3B82F6</TileColor>
|
<TileColor>#3B82F6</TileColor>
|
||||||
</tile>
|
</tile>
|
||||||
</msapplication>
|
</msapplication>
|
||||||
</browserconfig>
|
</browserconfig>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
Sitemap: /sitemap.xml
|
Sitemap: /sitemap.xml
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"preset": "minimal-2023",
|
"preset": "minimal-2023",
|
||||||
"images": ["public/favicon.svg"]
|
"images": ["public/favicon.svg"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function Header({ setSidebarOpen }: HeaderProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="lg:static sticky top-0 z-50 bg-card shadow-sm border-b border-border">
|
<header className="lg:static sticky top-0 z-50 bg-card shadow-sm border-b border-border pt-safe-top">
|
||||||
<div className="flex items-center justify-between h-16 px-6">
|
<div className="flex items-center justify-between h-16 px-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ export function PWAInstallPrompt({ onInstall }: PWAPromptProps) {
|
|||||||
try {
|
try {
|
||||||
await deferredPrompt.prompt();
|
await deferredPrompt.prompt();
|
||||||
const { outcome } = await deferredPrompt.userChoice;
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
|
||||||
if (outcome === "accepted") {
|
if (outcome === "accepted") {
|
||||||
onInstall?.();
|
onInstall?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
setDeferredPrompt(null);
|
setDeferredPrompt(null);
|
||||||
setShowPrompt(false);
|
setShowPrompt(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -153,4 +153,4 @@ export function PWAUpdatePrompt({ updateAvailable, onUpdate }: PWAUpdatePromptPr
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ export function usePWA(): PWAUpdate {
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
updateSW,
|
updateSW,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,12 @@
|
|||||||
--chart-4: 43 74% 66%;
|
--chart-4: 43 74% 66%;
|
||||||
--chart-5: 27 87% 67%;
|
--chart-5: 27 87% 67%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* iOS Safe Area Support for PWA */
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ export default {
|
|||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
|
spacing: {
|
||||||
|
'safe-top': 'var(--safe-area-inset-top)',
|
||||||
|
'safe-bottom': 'var(--safe-area-inset-bottom)',
|
||||||
|
'safe-left': 'var(--safe-area-inset-left)',
|
||||||
|
'safe-right': 'var(--safe-area-inset-right)',
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
background: "hsl(var(--background))",
|
background: "hsl(var(--background))",
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: "hsl(var(--foreground))",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@ class DatabaseService:
|
|||||||
|
|
||||||
# Add the display_name column
|
# Add the display_name column
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
ALTER TABLE accounts
|
ALTER TABLE accounts
|
||||||
ADD COLUMN display_name TEXT
|
ADD COLUMN display_name TEXT
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -857,6 +857,14 @@ class DatabaseService:
|
|||||||
|
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
try:
|
try:
|
||||||
|
# Check if transaction already exists before insertion
|
||||||
|
cursor.execute(
|
||||||
|
"""SELECT COUNT(*) FROM transactions
|
||||||
|
WHERE accountId = ? AND transactionId = ?""",
|
||||||
|
(transaction["accountId"], transaction["transactionId"]),
|
||||||
|
)
|
||||||
|
exists = cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
insert_sql,
|
insert_sql,
|
||||||
(
|
(
|
||||||
@@ -873,7 +881,11 @@ class DatabaseService:
|
|||||||
json.dumps(transaction["rawTransaction"]),
|
json.dumps(transaction["rawTransaction"]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
new_transactions.append(transaction)
|
|
||||||
|
# Only add to new_transactions if it didn't exist before
|
||||||
|
if not exists:
|
||||||
|
new_transactions.append(transaction)
|
||||||
|
|
||||||
except sqlite3.IntegrityError as e:
|
except sqlite3.IntegrityError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to insert transaction {transaction.get('transactionId')}: {e}"
|
f"Failed to insert transaction {transaction.get('transactionId')}: {e}"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "leggen"
|
name = "leggen"
|
||||||
version = "2025.9.15"
|
version = "2025.9.17"
|
||||||
description = "An Open Banking CLI"
|
description = "An Open Banking CLI"
|
||||||
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
|
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
|
||||||
requires-python = "~=3.13.0"
|
requires-python = "~=3.13.0"
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ class TestDatabaseService:
|
|||||||
with patch("sqlite3.connect") as mock_connect:
|
with patch("sqlite3.connect") as mock_connect:
|
||||||
mock_conn = mock_connect.return_value
|
mock_conn = mock_connect.return_value
|
||||||
mock_cursor = mock_conn.cursor.return_value
|
mock_cursor = mock_conn.cursor.return_value
|
||||||
|
# Mock fetchone to return (0,) indicating transaction doesn't exist yet
|
||||||
|
mock_cursor.fetchone.return_value = (0,)
|
||||||
|
|
||||||
result = await database_service._persist_transactions_sqlite(
|
result = await database_service._persist_transactions_sqlite(
|
||||||
"test-account-123", sample_transactions_db_format
|
"test-account-123", sample_transactions_db_format
|
||||||
@@ -338,6 +340,29 @@ class TestDatabaseService:
|
|||||||
mock_conn.commit.assert_called_once()
|
mock_conn.commit.assert_called_once()
|
||||||
mock_conn.close.assert_called_once()
|
mock_conn.close.assert_called_once()
|
||||||
|
|
||||||
|
async def test_persist_transactions_sqlite_duplicate_detection(
|
||||||
|
self, database_service, sample_transactions_db_format
|
||||||
|
):
|
||||||
|
"""Test that existing transactions are not returned as new."""
|
||||||
|
with patch("sqlite3.connect") as mock_connect:
|
||||||
|
mock_conn = mock_connect.return_value
|
||||||
|
mock_cursor = mock_conn.cursor.return_value
|
||||||
|
# Mock fetchone to return (1,) indicating transaction already exists
|
||||||
|
mock_cursor.fetchone.return_value = (1,)
|
||||||
|
|
||||||
|
result = await database_service._persist_transactions_sqlite(
|
||||||
|
"test-account-123", sample_transactions_db_format
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return empty list since all transactions already exist
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
# Verify database operations still happened (INSERT OR REPLACE executed)
|
||||||
|
mock_connect.assert_called()
|
||||||
|
mock_cursor.execute.assert_called()
|
||||||
|
mock_conn.commit.assert_called_once()
|
||||||
|
mock_conn.close.assert_called_once()
|
||||||
|
|
||||||
async def test_persist_transactions_sqlite_error(self, database_service):
|
async def test_persist_transactions_sqlite_error(self, database_service):
|
||||||
"""Test handling error during transaction persistence."""
|
"""Test handling error during transaction persistence."""
|
||||||
with patch("sqlite3.connect") as mock_connect:
|
with patch("sqlite3.connect") as mock_connect:
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -220,7 +220,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leggen"
|
name = "leggen"
|
||||||
version = "2025.9.15"
|
version = "2025.9.17"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
|
|||||||
Reference in New Issue
Block a user