mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-28 21:59:14 +00:00
Compare commits
27 Commits
2025.9.3
...
52898b2aa7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52898b2aa7 | ||
|
|
893b070ac5 | ||
|
|
4e63940aeb | ||
|
|
6f90a02696 | ||
|
|
b5a2e793cf | ||
|
|
67b7f73d40 | ||
|
|
e24060bc4b | ||
|
|
3bb957a470 | ||
|
|
bb3f5712e9 | ||
|
|
d435e7beb6 | ||
|
|
ef6e8ea846 | ||
|
|
fc65e08c36 | ||
|
|
500d05438d | ||
|
|
8cfad8a853 | ||
|
|
4293d45ce4 | ||
|
|
3ed79f9508 | ||
|
|
beb44f8563 | ||
|
|
1bc259bd3e | ||
|
|
fad426aee8 | ||
|
|
7f59e634a1 | ||
|
|
011c792e89 | ||
|
|
4e4c80be48 | ||
|
|
4c10307e12 | ||
|
|
de67ce9ec0 | ||
|
|
d99b1c73db | ||
|
|
69e381fffe | ||
|
|
f6ebb98de3 |
164
.github/workflows/release.yml
vendored
164
.github/workflows/release.yml
vendored
@@ -45,91 +45,87 @@ jobs:
|
||||
push-docker-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: elisiariocouto
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta backend
|
||||
id: meta-backend
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elisiariocouto/leggen
|
||||
ghcr.io/elisiariocouto/leggen
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
- name: Build and push backend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: elisiariocouto
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta backend
|
||||
id: meta-backend
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elisiariocouto/leggen
|
||||
ghcr.io/elisiariocouto/leggen
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
- name: Build and push backend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||
|
||||
push-docker-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: elisiariocouto
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta frontend
|
||||
id: meta-frontend
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
latest=false
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elisiariocouto/leggen
|
||||
ghcr.io/elisiariocouto/leggen
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=tag,suffix=-frontend
|
||||
type=semver,pattern={{version}},suffix=-frontend
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-frontend
|
||||
type=raw,value=latest-frontend
|
||||
- name: Build and push frontend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./frontend
|
||||
file: ./frontend/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: elisiariocouto
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta frontend
|
||||
id: meta-frontend
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
elisiariocouto/leggen
|
||||
ghcr.io/elisiariocouto/leggen
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=tag,suffix=-frontend
|
||||
type=semver,pattern={{version}},suffix=-frontend
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-frontend
|
||||
type=raw,value=latest-frontend
|
||||
- name: Build and push frontend
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./frontend
|
||||
file: ./frontend/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||
|
||||
@@ -38,5 +38,4 @@
|
||||
### General
|
||||
- **Formatting**: ruff for Python, ESLint for TypeScript
|
||||
- **Commits**: Use conventional commits, run pre-commit hooks before pushing
|
||||
- Avoid including specific numbers, counts, or data-dependent information that may become outdated
|
||||
- **Security**: Never log sensitive data, use environment variables for secrets
|
||||
|
||||
166
CHANGELOG.md
166
CHANGELOG.md
@@ -1,170 +1,4 @@
|
||||
|
||||
## 2025.9.3 (2025/09/10)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- **ci:** Fix GitHub Actions syntax. ([90e58734](https://github.com/elisiariocouto/leggen/commit/90e58734adb9638efd695719321874658529561d))
|
||||
|
||||
|
||||
|
||||
## 2025.9.3 (2025/09/10)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- **ci:** Fix GitHub Actions syntax. ([90e58734](https://github.com/elisiariocouto/leggen/commit/90e58734adb9638efd695719321874658529561d))
|
||||
|
||||
|
||||
|
||||
## 2025.9.2 (2025/09/10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **ci:** Prevent duplicate Docker tags in GitHub Actions ([53e08e8e](https://github.com/elisiariocouto/leggen/commit/53e08e8e4b909b4895b5a447cfbce515893d31a5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- **docker:** Add Docker containerization for React frontend ([84fe79b3](https://github.com/elisiariocouto/leggen/commit/84fe79b37b4f154fa0758f8d037cdba0d166dd3b))
|
||||
|
||||
|
||||
|
||||
## 2025.9.2 (2025/09/10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **ci:** Prevent duplicate Docker tags in GitHub Actions ([53e08e8e](https://github.com/elisiariocouto/leggen/commit/53e08e8e4b909b4895b5a447cfbce515893d31a5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- **docker:** Add Docker containerization for React frontend ([84fe79b3](https://github.com/elisiariocouto/leggen/commit/84fe79b37b4f154fa0758f8d037cdba0d166dd3b))
|
||||
|
||||
|
||||
|
||||
## 2025.9.1 (2025/09/09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Handle duplicate transactionId values in migration ([8fabaf7b](https://github.com/elisiariocouto/leggen/commit/8fabaf7b86fde921c61266568ecb0403d3102671))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Improve AGENTS.md. ([3270dc45](https://github.com/elisiariocouto/leggen/commit/3270dc4585e6b33d55aef0deecd849753d36fa74))
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused hide_missing_ids functionality ([8006e5e1](https://github.com/elisiariocouto/leggen/commit/8006e5e1f6373aae39d3c38068d694e142bc85a5))
|
||||
|
||||
|
||||
|
||||
## 2025.9.1 (2025/09/09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Handle duplicate transactionId values in migration ([8fabaf7b](https://github.com/elisiariocouto/leggen/commit/8fabaf7b86fde921c61266568ecb0403d3102671))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Improve AGENTS.md. ([3270dc45](https://github.com/elisiariocouto/leggen/commit/3270dc4585e6b33d55aef0deecd849753d36fa74))
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused hide_missing_ids functionality ([8006e5e1](https://github.com/elisiariocouto/leggen/commit/8006e5e1f6373aae39d3c38068d694e142bc85a5))
|
||||
|
||||
|
||||
|
||||
## 2025.9.0 (2025/09/09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **cli:** Show transactions without internal ID when using --full. ([46f3f5c4](https://github.com/elisiariocouto/leggen/commit/46f3f5c4984224c3f4b421e1a06dcf44d4f211e0))
|
||||
- Do not install development dependencies. ([73d6bd32](https://github.com/elisiariocouto/leggen/commit/73d6bd32dbc59608ef1472dc65d9e18450f00896))
|
||||
- Implement proper GoCardless authentication and add dev features ([f0fee4fd](https://github.com/elisiariocouto/leggen/commit/f0fee4fd82e1c788614d73fcd0075f5e16976650))
|
||||
- Make internal transcation ID optional. ([6bce7eb6](https://github.com/elisiariocouto/leggen/commit/6bce7eb6be5f9a5286eb27e777fbf83a6b1c5f8d))
|
||||
- Resolve 404 balances endpoint and currency formatting errors ([417b7753](https://github.com/elisiariocouto/leggen/commit/417b77539fc275493d55efb29f92abcea666b210))
|
||||
- Merge account details into balance data to prevent unknown/N/A values ([eaaea6e4](https://github.com/elisiariocouto/leggen/commit/eaaea6e4598e9c81997573e19f4ef1c58ebe320f))
|
||||
- Use account status for balance records instead of hardcoded 'active' ([541cb262](https://github.com/elisiariocouto/leggen/commit/541cb262ee5783eedf2b154c148c28ec89845da5))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update README for new web architecture ([4018b263](https://github.com/elisiariocouto/leggen/commit/4018b263f27c2b59af31428d7a0878280a291c85))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- Transform to web architecture with FastAPI backend ([91f53b35](https://github.com/elisiariocouto/leggen/commit/91f53b35b18740869ee9cebfac394db2e12db099))
|
||||
- Add comprehensive test suite with 46 passing tests ([34e793c7](https://github.com/elisiariocouto/leggen/commit/34e793c75c8df1e57ea240b92ccf0843a80c2a14))
|
||||
- Add mypy to pre-commit. ([ec8ef834](https://github.com/elisiariocouto/leggen/commit/ec8ef8346add878f3ff4e8ed928b952d9b5dd584))
|
||||
- Implement database-first architecture to minimize GoCardless API calls ([155c3055](https://github.com/elisiariocouto/leggen/commit/155c30559f4cacd76ef01e50ec29ee436d3f9d56))
|
||||
- Implement dynamic API connection status ([cb2e70e4](https://github.com/elisiariocouto/leggen/commit/cb2e70e42d1122e9c2e5420b095aeb1e55454c24))
|
||||
- Add automatic balance timestamp migration mechanism ([34501f5f](https://github.com/elisiariocouto/leggen/commit/34501f5f0d3b3dff68364b60be77bfb99071b269))
|
||||
- Improve notification filters configuration format ([2191fe90](https://github.com/elisiariocouto/leggen/commit/2191fe906659f4fd22c25b6cb9fbb95c03472f00))
|
||||
- Add notifications view and update branding ([abf39abe](https://github.com/elisiariocouto/leggen/commit/abf39abe74b75d8cb980109fbcbdd940066cc90b))
|
||||
- Make API URL configurable and improve code quality ([37949a4e](https://github.com/elisiariocouto/leggen/commit/37949a4e1f25a2656f6abef75ba942f7b205c130))
|
||||
- Change versioning scheme to calver. ([f2e05484](https://github.com/elisiariocouto/leggen/commit/f2e05484dc688409b6db6bd16858b066d3a16976))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Implement code review suggestions and format code. ([de3da84d](https://github.com/elisiariocouto/leggen/commit/de3da84dffd83e0b232cf76836935a66eb704aee))
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove MongoDB support, simplify to SQLite-only architecture ([47164e85](https://github.com/elisiariocouto/leggen/commit/47164e854600dfcac482449769b1d2e55c842570))
|
||||
- Remove unused amount_threshold and keywords from notification filters ([95709978](https://github.com/elisiariocouto/leggen/commit/957099786cb0e48c9ffbda11b3172ec9fae9ac37))
|
||||
|
||||
|
||||
|
||||
## 2025.9.0 (2025/09/09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **cli:** Show transactions without internal ID when using --full. ([46f3f5c4](https://github.com/elisiariocouto/leggen/commit/46f3f5c4984224c3f4b421e1a06dcf44d4f211e0))
|
||||
- Do not install development dependencies. ([73d6bd32](https://github.com/elisiariocouto/leggen/commit/73d6bd32dbc59608ef1472dc65d9e18450f00896))
|
||||
- Implement proper GoCardless authentication and add dev features ([f0fee4fd](https://github.com/elisiariocouto/leggen/commit/f0fee4fd82e1c788614d73fcd0075f5e16976650))
|
||||
- Make internal transcation ID optional. ([6bce7eb6](https://github.com/elisiariocouto/leggen/commit/6bce7eb6be5f9a5286eb27e777fbf83a6b1c5f8d))
|
||||
- Resolve 404 balances endpoint and currency formatting errors ([417b7753](https://github.com/elisiariocouto/leggen/commit/417b77539fc275493d55efb29f92abcea666b210))
|
||||
- Merge account details into balance data to prevent unknown/N/A values ([eaaea6e4](https://github.com/elisiariocouto/leggen/commit/eaaea6e4598e9c81997573e19f4ef1c58ebe320f))
|
||||
- Use account status for balance records instead of hardcoded 'active' ([541cb262](https://github.com/elisiariocouto/leggen/commit/541cb262ee5783eedf2b154c148c28ec89845da5))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update README for new web architecture ([4018b263](https://github.com/elisiariocouto/leggen/commit/4018b263f27c2b59af31428d7a0878280a291c85))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- Transform to web architecture with FastAPI backend ([91f53b35](https://github.com/elisiariocouto/leggen/commit/91f53b35b18740869ee9cebfac394db2e12db099))
|
||||
- Add comprehensive test suite with 46 passing tests ([34e793c7](https://github.com/elisiariocouto/leggen/commit/34e793c75c8df1e57ea240b92ccf0843a80c2a14))
|
||||
- Add mypy to pre-commit. ([ec8ef834](https://github.com/elisiariocouto/leggen/commit/ec8ef8346add878f3ff4e8ed928b952d9b5dd584))
|
||||
- Implement database-first architecture to minimize GoCardless API calls ([155c3055](https://github.com/elisiariocouto/leggen/commit/155c30559f4cacd76ef01e50ec29ee436d3f9d56))
|
||||
- Implement dynamic API connection status ([cb2e70e4](https://github.com/elisiariocouto/leggen/commit/cb2e70e42d1122e9c2e5420b095aeb1e55454c24))
|
||||
- Add automatic balance timestamp migration mechanism ([34501f5f](https://github.com/elisiariocouto/leggen/commit/34501f5f0d3b3dff68364b60be77bfb99071b269))
|
||||
- Improve notification filters configuration format ([2191fe90](https://github.com/elisiariocouto/leggen/commit/2191fe906659f4fd22c25b6cb9fbb95c03472f00))
|
||||
- Add notifications view and update branding ([abf39abe](https://github.com/elisiariocouto/leggen/commit/abf39abe74b75d8cb980109fbcbdd940066cc90b))
|
||||
- Make API URL configurable and improve code quality ([37949a4e](https://github.com/elisiariocouto/leggen/commit/37949a4e1f25a2656f6abef75ba942f7b205c130))
|
||||
- Change versioning scheme to calver. ([f2e05484](https://github.com/elisiariocouto/leggen/commit/f2e05484dc688409b6db6bd16858b066d3a16976))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Implement code review suggestions and format code. ([de3da84d](https://github.com/elisiariocouto/leggen/commit/de3da84dffd83e0b232cf76836935a66eb704aee))
|
||||
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove MongoDB support, simplify to SQLite-only architecture ([47164e85](https://github.com/elisiariocouto/leggen/commit/47164e854600dfcac482449769b1d2e55c842570))
|
||||
- Remove unused amount_threshold and keywords from notification filters ([95709978](https://github.com/elisiariocouto/leggen/commit/957099786cb0e48c9ffbda11b3172ec9fae9ac37))
|
||||
|
||||
|
||||
|
||||
## 0.6.11 (2025/02/23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
52
README.md
52
README.md
@@ -64,8 +64,8 @@ git clone https://github.com/elisiariocouto/leggen.git
|
||||
cd leggen
|
||||
|
||||
# Create your configuration
|
||||
mkdir -p data && cp config.example.toml data/config.toml
|
||||
# Edit data/config.toml with your GoCardless credentials
|
||||
mkdir -p leggen && cp config.example.toml leggen/config.toml
|
||||
# Edit leggen/config.toml with your GoCardless credentials
|
||||
|
||||
# Start all services (frontend + backend)
|
||||
docker compose up -d
|
||||
@@ -74,31 +74,6 @@ docker compose up -d
|
||||
# API is available at http://localhost:8000
|
||||
```
|
||||
|
||||
#### Production Deployment
|
||||
|
||||
For production deployment using published Docker images:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/elisiariocouto/leggen.git
|
||||
cd leggen
|
||||
|
||||
# Create your configuration
|
||||
mkdir -p data && cp config.example.toml data/config.toml
|
||||
# Edit data/config.toml with your GoCardless credentials
|
||||
|
||||
# Start production services
|
||||
docker compose up -d
|
||||
|
||||
# Access the web interface at http://localhost:3000
|
||||
# API is available at http://localhost:8000
|
||||
```
|
||||
|
||||
### Development vs Production
|
||||
|
||||
- **Development**: Use `docker compose -f compose.dev.yml up -d` (builds from source)
|
||||
- **Production**: Use `docker compose up -d` (uses published images)
|
||||
|
||||
#### Option 2: Local Development
|
||||
For development or local installation:
|
||||
|
||||
@@ -115,7 +90,7 @@ uv run leggen --help
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a configuration file at `./data/config.toml` (for Docker) or `~/.config/leggen/config.toml` (for local development):
|
||||
Create a configuration file at `~/.config/leggen/config.toml`:
|
||||
|
||||
```toml
|
||||
[gocardless]
|
||||
@@ -213,25 +188,8 @@ leggen status
|
||||
|
||||
### Docker Usage
|
||||
|
||||
#### Development (build from source)
|
||||
```bash
|
||||
# Start development services
|
||||
docker compose -f compose.dev.yml up -d
|
||||
|
||||
# View service status
|
||||
docker compose -f compose.dev.yml ps
|
||||
|
||||
# Check logs
|
||||
docker compose -f compose.dev.yml logs frontend
|
||||
docker compose -f compose.dev.yml logs leggend
|
||||
|
||||
# Stop development services
|
||||
docker compose -f compose.dev.yml down
|
||||
```
|
||||
|
||||
#### Production (use published images)
|
||||
```bash
|
||||
# Start production services
|
||||
# Start all services (frontend + backend)
|
||||
docker compose up -d
|
||||
|
||||
# View service status
|
||||
@@ -244,7 +202,7 @@ docker compose logs leggend
|
||||
# Access the web interface at http://localhost:3000
|
||||
# API documentation at http://localhost:8000/docs
|
||||
|
||||
# Stop production services
|
||||
# Stop all services
|
||||
docker compose down
|
||||
```
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
services:
|
||||
# React frontend service
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- "127.0.0.1:3000:80"
|
||||
environment:
|
||||
- API_BACKEND_URL=${API_BACKEND_URL:-http://leggend:8000}
|
||||
depends_on:
|
||||
leggend:
|
||||
condition: service_healthy
|
||||
|
||||
# FastAPI backend service
|
||||
leggend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
volumes:
|
||||
- "./data:/root/.config/leggen"
|
||||
12
compose.yml
12
compose.yml
@@ -1,19 +1,25 @@
|
||||
services:
|
||||
# React frontend service
|
||||
frontend:
|
||||
image: ghcr.io/elisiariocouto/leggen:latest-frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- "127.0.0.1:3000:80"
|
||||
environment:
|
||||
- API_BACKEND_URL=${API_BACKEND_URL:-http://leggend:8000}
|
||||
depends_on:
|
||||
leggend:
|
||||
condition: service_healthy
|
||||
|
||||
# FastAPI backend service
|
||||
leggend:
|
||||
image: ghcr.io/elisiariocouto/leggen:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
volumes:
|
||||
- "./data:/root/.config/leggen" # Configuration and database directory
|
||||
- "./data:/root/.config/leggen"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
[gocardless]
|
||||
key = "your-api-key"
|
||||
secret = "your-secret-key"
|
||||
url = "https://bankaccountdata.gocardless.com/api/v2"
|
||||
|
||||
[database]
|
||||
sqlite = true
|
||||
|
||||
# Optional: Background sync scheduling
|
||||
[scheduler.sync]
|
||||
enabled = true
|
||||
hour = 3 # 3 AM
|
||||
minute = 0
|
||||
# cron = "0 3 * * *" # Alternative: use cron expression
|
||||
|
||||
# Optional: Discord notifications
|
||||
[notifications.discord]
|
||||
webhook = "https://discord.com/api/webhooks/..."
|
||||
enabled = true
|
||||
|
||||
# Optional: Telegram notifications
|
||||
[notifications.telegram]
|
||||
token = "your-bot-token"
|
||||
chat_id = 12345
|
||||
enabled = true
|
||||
|
||||
# Optional: Transaction filters for notifications
|
||||
[filters]
|
||||
case-insensitive = ["salary", "utility"]
|
||||
case-sensitive = ["SpecificStore"]
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "VITE_API_URL=http://localhost:8000/api/v1 vite",
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
|
||||
@@ -210,6 +210,7 @@ def get_transactions(
|
||||
min_amount=None,
|
||||
max_amount=None,
|
||||
search=None,
|
||||
hide_missing_ids=True,
|
||||
):
|
||||
"""Get transactions from SQLite database with optional filtering"""
|
||||
from pathlib import Path
|
||||
@@ -249,6 +250,11 @@ def get_transactions(
|
||||
query += " AND description LIKE ?"
|
||||
params.append(f"%{search}%")
|
||||
|
||||
if hide_missing_ids:
|
||||
query += (
|
||||
" AND internalTransactionId IS NOT NULL AND internalTransactionId != ''"
|
||||
)
|
||||
|
||||
# Add ordering and pagination
|
||||
query += " ORDER BY transactionDate DESC"
|
||||
|
||||
@@ -397,6 +403,11 @@ def get_transaction_count(account_id=None, **filters):
|
||||
query += " AND description LIKE ?"
|
||||
params.append(f"%{filters['search']}%")
|
||||
|
||||
if filters.get("hide_missing_ids", True):
|
||||
query += (
|
||||
" AND internalTransactionId IS NOT NULL AND internalTransactionId != ''"
|
||||
)
|
||||
|
||||
try:
|
||||
cursor.execute(query, params)
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
@@ -69,13 +69,8 @@ def save_transactions(ctx: click.Context, account: str) -> list:
|
||||
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
|
||||
)
|
||||
|
||||
# Extract transaction ID, using transactionId as fallback when internalTransactionId is missing
|
||||
transaction_id = transaction.get("internalTransactionId") or transaction.get(
|
||||
"transactionId"
|
||||
)
|
||||
|
||||
t = {
|
||||
"internalTransactionId": transaction_id,
|
||||
"internalTransactionId": transaction.get("internalTransactionId"),
|
||||
"institutionId": account_info["institution_id"],
|
||||
"iban": account_info.get("iban", "N/A"),
|
||||
"transactionDate": min_date,
|
||||
@@ -110,13 +105,8 @@ def save_transactions(ctx: click.Context, account: str) -> list:
|
||||
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
|
||||
)
|
||||
|
||||
# Extract transaction ID, using transactionId as fallback when internalTransactionId is missing
|
||||
transaction_id = transaction.get("internalTransactionId") or transaction.get(
|
||||
"transactionId"
|
||||
)
|
||||
|
||||
t = {
|
||||
"internalTransactionId": transaction_id,
|
||||
"internalTransactionId": transaction.get("internalTransactionId"),
|
||||
"institutionId": account_info["institution_id"],
|
||||
"iban": account_info.get("iban", "N/A"),
|
||||
"transactionDate": min_date,
|
||||
|
||||
@@ -18,6 +18,9 @@ async def get_all_transactions(
|
||||
summary_only: bool = Query(
|
||||
default=True, description="Return transaction summaries only"
|
||||
),
|
||||
hide_missing_ids: bool = Query(
|
||||
default=True, description="Hide transactions without internalTransactionId"
|
||||
),
|
||||
date_from: Optional[str] = Query(
|
||||
default=None, description="Filter from date (YYYY-MM-DD)"
|
||||
),
|
||||
@@ -47,6 +50,7 @@ async def get_all_transactions(
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
search=search,
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
|
||||
# Get total count for pagination info (respecting the same filters)
|
||||
@@ -57,6 +61,7 @@ async def get_all_transactions(
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
search=search,
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
|
||||
# Get total count for pagination info
|
||||
@@ -121,6 +126,9 @@ async def get_all_transactions(
|
||||
async def get_transaction_stats(
|
||||
days: int = Query(default=30, description="Number of days to include in stats"),
|
||||
account_id: Optional[str] = Query(default=None, description="Filter by account ID"),
|
||||
hide_missing_ids: bool = Query(
|
||||
default=True, description="Hide transactions without internalTransactionId"
|
||||
),
|
||||
) -> APIResponse:
|
||||
"""Get transaction statistics for the last N days from database"""
|
||||
try:
|
||||
@@ -138,6 +146,7 @@ async def get_transaction_stats(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
limit=None, # Get all matching transactions for stats
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
|
||||
# Calculate stats
|
||||
|
||||
@@ -93,13 +93,8 @@ class DatabaseService:
|
||||
",".join(transaction.get("remittanceInformationUnstructuredArray", [])),
|
||||
)
|
||||
|
||||
# Extract transaction ID, using transactionId as fallback when internalTransactionId is missing
|
||||
transaction_id = transaction.get("internalTransactionId") or transaction.get(
|
||||
"transactionId"
|
||||
)
|
||||
|
||||
return {
|
||||
"internalTransactionId": transaction_id,
|
||||
"internalTransactionId": transaction.get("internalTransactionId"),
|
||||
"institutionId": account_info["institution_id"],
|
||||
"iban": account_info.get("iban", "N/A"),
|
||||
"transactionDate": min_date,
|
||||
@@ -121,6 +116,7 @@ class DatabaseService:
|
||||
min_amount: Optional[float] = None,
|
||||
max_amount: Optional[float] = None,
|
||||
search: Optional[str] = None,
|
||||
hide_missing_ids: bool = True,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get transactions from SQLite database"""
|
||||
if not self.sqlite_enabled:
|
||||
@@ -137,6 +133,7 @@ class DatabaseService:
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
search=search,
|
||||
hide_missing_ids=hide_missing_ids,
|
||||
)
|
||||
logger.debug(f"Retrieved {len(transactions)} transactions from database")
|
||||
return transactions
|
||||
@@ -152,6 +149,7 @@ class DatabaseService:
|
||||
min_amount: Optional[float] = None,
|
||||
max_amount: Optional[float] = None,
|
||||
search: Optional[str] = None,
|
||||
hide_missing_ids: bool = True,
|
||||
) -> int:
|
||||
"""Get total count of transactions from SQLite database"""
|
||||
if not self.sqlite_enabled:
|
||||
@@ -168,7 +166,9 @@ class DatabaseService:
|
||||
# Remove None values
|
||||
filters = {k: v for k, v in filters.items() if v is not None}
|
||||
|
||||
count = sqlite_db.get_transaction_count(account_id=account_id, **filters)
|
||||
count = sqlite_db.get_transaction_count(
|
||||
account_id=account_id, hide_missing_ids=hide_missing_ids, **filters
|
||||
)
|
||||
logger.debug(f"Total transaction count: {count}")
|
||||
return count
|
||||
except Exception as e:
|
||||
@@ -259,7 +259,6 @@ class DatabaseService:
|
||||
return
|
||||
|
||||
await self._migrate_balance_timestamps_if_needed()
|
||||
await self._migrate_null_transaction_ids_if_needed()
|
||||
|
||||
async def _migrate_balance_timestamps_if_needed(self):
|
||||
"""Check and migrate balance timestamps if needed"""
|
||||
@@ -380,145 +379,6 @@ class DatabaseService:
|
||||
logger.error(f"Balance timestamp migration failed: {e}")
|
||||
raise
|
||||
|
||||
async def _migrate_null_transaction_ids_if_needed(self):
|
||||
"""Check and migrate null transaction IDs if needed"""
|
||||
try:
|
||||
if await self._check_null_transaction_ids_migration_needed():
|
||||
logger.info("Null transaction IDs migration needed, starting...")
|
||||
await self._migrate_null_transaction_ids()
|
||||
logger.info("Null transaction IDs migration completed")
|
||||
else:
|
||||
logger.info("No null transaction IDs found to migrate")
|
||||
except Exception as e:
|
||||
logger.error(f"Null transaction IDs migration failed: {e}")
|
||||
raise
|
||||
|
||||
async def _check_null_transaction_ids_migration_needed(self) -> bool:
|
||||
"""Check if null transaction IDs need migration"""
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
if not db_path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check for transactions with null or empty internalTransactionId
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM transactions
|
||||
WHERE (internalTransactionId IS NULL OR internalTransactionId = '')
|
||||
AND json_extract(rawTransaction, '$.transactionId') IS NOT NULL
|
||||
""")
|
||||
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
return count > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check null transaction IDs migration status: {e}")
|
||||
return False
|
||||
|
||||
async def _migrate_null_transaction_ids(self):
|
||||
"""Populate null internalTransactionId fields using transactionId from raw data"""
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path.home() / ".config" / "leggen" / "leggen.db"
|
||||
if not db_path.exists():
|
||||
logger.warning("Database file not found, skipping migration")
|
||||
return
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all transactions with null/empty internalTransactionId but valid transactionId in raw data
|
||||
cursor.execute("""
|
||||
SELECT rowid, json_extract(rawTransaction, '$.transactionId') as transactionId
|
||||
FROM transactions
|
||||
WHERE (internalTransactionId IS NULL OR internalTransactionId = '')
|
||||
AND json_extract(rawTransaction, '$.transactionId') IS NOT NULL
|
||||
ORDER BY rowid
|
||||
""")
|
||||
|
||||
null_records = cursor.fetchall()
|
||||
total_records = len(null_records)
|
||||
|
||||
if total_records == 0:
|
||||
logger.info("No null transaction IDs found to migrate")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Migrating {total_records} transaction records with null internalTransactionId"
|
||||
)
|
||||
|
||||
# Update in batches
|
||||
batch_size = 100
|
||||
migrated_count = 0
|
||||
skipped_duplicates = 0
|
||||
|
||||
for i in range(0, total_records, batch_size):
|
||||
batch = null_records[i : i + batch_size]
|
||||
|
||||
for rowid, transaction_id in batch:
|
||||
try:
|
||||
# Check if this transactionId is already used by another record
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM transactions WHERE internalTransactionId = ?",
|
||||
(str(transaction_id),),
|
||||
)
|
||||
existing_count = cursor.fetchone()[0]
|
||||
|
||||
if existing_count > 0:
|
||||
# Generate a unique ID to avoid constraint violation
|
||||
unique_id = f"{str(transaction_id)}_{uuid.uuid4().hex[:8]}"
|
||||
logger.debug(
|
||||
f"Generated unique ID for duplicate transactionId: {unique_id}"
|
||||
)
|
||||
else:
|
||||
# Use the original transactionId
|
||||
unique_id = str(transaction_id)
|
||||
|
||||
# Update the record
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE transactions
|
||||
SET internalTransactionId = ?
|
||||
WHERE rowid = ?
|
||||
""",
|
||||
(unique_id, rowid),
|
||||
)
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
if migrated_count % 100 == 0:
|
||||
logger.info(
|
||||
f"Migrated {migrated_count}/{total_records} transaction records"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to migrate record {rowid}: {e}")
|
||||
continue
|
||||
|
||||
# Commit batch
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
logger.info(f"Successfully migrated {migrated_count} transaction records")
|
||||
if skipped_duplicates > 0:
|
||||
logger.info(
|
||||
f"Generated unique IDs for {skipped_duplicates} duplicate transactionIds"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Null transaction IDs migration failed: {e}")
|
||||
raise
|
||||
|
||||
def _unix_to_datetime_string(self, unix_timestamp: float) -> str:
|
||||
"""Convert Unix timestamp to datetime string"""
|
||||
dt = datetime.fromtimestamp(unix_timestamp)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "leggen"
|
||||
version = "2025.9.3"
|
||||
version = "0.6.11"
|
||||
description = "An Open Banking CLI"
|
||||
authors = [{ name = "Elisiário Couto", email = "elisiario@couto.io" }]
|
||||
requires-python = "~=3.13.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
set -ef -o pipefail
|
||||
|
||||
@@ -13,37 +13,23 @@ check_command git
|
||||
check_command git-cliff
|
||||
check_command uv
|
||||
|
||||
# Get current date components
|
||||
YEAR=$(date +%Y)
|
||||
MONTH=$(date +%-m) # %-m removes zero padding
|
||||
|
||||
# Get the latest version for current year and month
|
||||
LATEST_TAG=$(git tag -l "${YEAR}.${MONTH}.*" | sort -V | tail -n 1)
|
||||
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
# No version for current year/month exists, start at 0
|
||||
MICRO=0
|
||||
else
|
||||
# Extract micro version and increment
|
||||
MICRO=$(echo "$LATEST_TAG" | cut -d. -f3)
|
||||
MICRO=$((MICRO + 1))
|
||||
if [ -z "$1" ]; then
|
||||
echo " > No semver verb specified, run release with <major|minor|patch> parameter."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEXT_VERSION="${YEAR}.${MONTH}.${MICRO}"
|
||||
CURRENT_VERSION=$(uv version --short)
|
||||
|
||||
CURRENT_VERSION=$(uvx poetry version -s)
|
||||
echo " > Current version is $CURRENT_VERSION"
|
||||
echo " > Setting new version to $NEXT_VERSION"
|
||||
|
||||
# Manually update version in pyproject.toml
|
||||
sed -i '' "s/^version = .*/version = \"${NEXT_VERSION}\"/" pyproject.toml
|
||||
uvx poetry version "$1"
|
||||
NEXT_VERSION=$(uvx poetry version -s)
|
||||
|
||||
echo " > Version bumped to $NEXT_VERSION"
|
||||
echo " > leggen bumped to $NEXT_VERSION"
|
||||
echo "Updating CHANGELOG.md"
|
||||
git-cliff --unreleased --tag "$NEXT_VERSION" --prepend CHANGELOG.md > /dev/null
|
||||
|
||||
echo " > Commiting changes and adding git tag"
|
||||
git add pyproject.toml CHANGELOG.md uv.lock
|
||||
git add pyproject.toml CHANGELOG.md
|
||||
git commit -m "chore(ci): Bump version to $NEXT_VERSION"
|
||||
git tag -a "$NEXT_VERSION" -m "$NEXT_VERSION"
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ class TestTransactionsAPI:
|
||||
min_amount=-50.0,
|
||||
max_amount=0.0,
|
||||
search="Coffee",
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
def test_get_transactions_empty_result(
|
||||
|
||||
@@ -99,6 +99,7 @@ class TestDatabaseService:
|
||||
min_amount=None,
|
||||
max_amount=None,
|
||||
search=None,
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
async def test_get_transactions_from_db_with_filters(
|
||||
@@ -129,6 +130,7 @@ class TestDatabaseService:
|
||||
min_amount=-50.0,
|
||||
max_amount=0.0,
|
||||
search="Coffee",
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
async def test_get_transactions_from_db_sqlite_disabled(self, database_service):
|
||||
@@ -158,7 +160,9 @@ class TestDatabaseService:
|
||||
)
|
||||
|
||||
assert result == 42
|
||||
mock_get_count.assert_called_once_with(account_id="test-account-123")
|
||||
mock_get_count.assert_called_once_with(
|
||||
account_id="test-account-123", hide_missing_ids=True
|
||||
)
|
||||
|
||||
async def test_get_transaction_count_from_db_with_filters(self, database_service):
|
||||
"""Test getting transaction count with filters."""
|
||||
@@ -178,6 +182,7 @@ class TestDatabaseService:
|
||||
date_from="2025-09-01",
|
||||
min_amount=-100.0,
|
||||
search="Coffee",
|
||||
hide_missing_ids=True,
|
||||
)
|
||||
|
||||
async def test_get_transaction_count_from_db_sqlite_disabled(
|
||||
|
||||
Reference in New Issue
Block a user