avios-cli 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {avios_cli-0.2.1 → avios_cli-0.3.0}/CHANGELOG.md +28 -1
  2. {avios_cli-0.2.1 → avios_cli-0.3.0}/PKG-INFO +34 -17
  3. {avios_cli-0.2.1 → avios_cli-0.3.0}/README.md +33 -16
  4. {avios_cli-0.2.1 → avios_cli-0.3.0}/SECURITY.md +9 -10
  5. avios_cli-0.3.0/docs/dashboard.svg +190 -0
  6. avios_cli-0.3.0/scripts/screenshot.py +84 -0
  7. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/__init__.py +1 -1
  8. avios_cli-0.3.0/src/avios/accounts.py +174 -0
  9. avios_cli-0.3.0/src/avios/aggregate.py +118 -0
  10. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/auth.py +37 -35
  11. avios_cli-0.3.0/src/avios/cli.py +420 -0
  12. avios_cli-0.3.0/src/avios/finnair.py +236 -0
  13. avios_cli-0.3.0/src/avios/programmes.py +66 -0
  14. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/session.py +28 -7
  15. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/tui/app.py +32 -33
  16. avios_cli-0.3.0/src/avios/tui/widgets.py +51 -0
  17. avios_cli-0.3.0/tests/test_accounts.py +136 -0
  18. avios_cli-0.3.0/tests/test_aggregate.py +140 -0
  19. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/test_auth.py +22 -24
  20. avios_cli-0.3.0/tests/test_cli.py +409 -0
  21. avios_cli-0.3.0/tests/test_finnair.py +220 -0
  22. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/test_session.py +19 -0
  23. avios_cli-0.3.0/tests/test_tui.py +137 -0
  24. avios_cli-0.2.1/docs/dashboard.svg +0 -190
  25. avios_cli-0.2.1/scripts/screenshot.py +0 -52
  26. avios_cli-0.2.1/src/avios/cli.py +0 -263
  27. avios_cli-0.2.1/src/avios/tui/widgets.py +0 -30
  28. avios_cli-0.2.1/tests/test_cli.py +0 -197
  29. avios_cli-0.2.1/tests/test_tui.py +0 -88
  30. {avios_cli-0.2.1 → avios_cli-0.3.0}/.editorconfig +0 -0
  31. {avios_cli-0.2.1 → avios_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  32. {avios_cli-0.2.1 → avios_cli-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  33. {avios_cli-0.2.1 → avios_cli-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  34. {avios_cli-0.2.1 → avios_cli-0.3.0}/.github/dependabot.yml +0 -0
  35. {avios_cli-0.2.1 → avios_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  36. {avios_cli-0.2.1 → avios_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  37. {avios_cli-0.2.1 → avios_cli-0.3.0}/.gitignore +0 -0
  38. {avios_cli-0.2.1 → avios_cli-0.3.0}/.pre-commit-config.yaml +0 -0
  39. {avios_cli-0.2.1 → avios_cli-0.3.0}/.python-version +0 -0
  40. {avios_cli-0.2.1 → avios_cli-0.3.0}/CONTRIBUTING.md +0 -0
  41. {avios_cli-0.2.1 → avios_cli-0.3.0}/LICENSE +0 -0
  42. {avios_cli-0.2.1 → avios_cli-0.3.0}/pyproject.toml +0 -0
  43. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/client.py +0 -0
  44. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/config.py +0 -0
  45. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/endpoints.py +0 -0
  46. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/models.py +0 -0
  47. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/tui/__init__.py +0 -0
  48. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/tui/art.py +0 -0
  49. {avios_cli-0.2.1 → avios_cli-0.3.0}/src/avios/tui/styles.tcss +0 -0
  50. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/conftest.py +0 -0
  51. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/fixtures/balance.json +0 -0
  52. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/fixtures/transactions.json +0 -0
  53. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/test_client.py +0 -0
  54. {avios_cli-0.2.1 → avios_cli-0.3.0}/tests/test_models.py +0 -0
  55. {avios_cli-0.2.1 → avios_cli-0.3.0}/uv.lock +0 -0
@@ -6,6 +6,32 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-07-27
10
+
11
+ ### Added
12
+ - **Multiple accounts, one per programme.** Log into several Avios programmes and
13
+ keep their sessions side by side: `avios login [programme]` (default `ba`),
14
+ `avios logout [programme]` (or `avios logout` for all), and a new `avios accounts`
15
+ roster showing each balance and status. Programmes: British Airways (`ba`),
16
+ Iberia (`iberia`), Aer Lingus (`aerlingus`), Finnair Plus (`finnair`).
17
+ - **Combined views.** `balance` sums every account into a combined total (a single
18
+ account keeps the individual/household detail); `transactions`/`pending` merge
19
+ across accounts newest-first with a Programme column. `--account/-a` filters any
20
+ data command to one programme.
21
+ - **Multi-account TUI.** `avios tui` shows a combined balance header (per-programme
22
+ breakdown) and a Programme column, loading every account concurrently.
23
+ - **Finnair Plus** via Finnair's CAS/OAuth browser login + loyalty API (balance,
24
+ profile, transactions); token-backed account storage alongside cookie accounts.
25
+ - Programme-aware browser login (per-programme login URL + isolated Chrome profile)
26
+ and a shared, backend-aware aggregation layer (`aggregate.py`) used by the CLI
27
+ and TUI. Per-account failures are captured, so one expired session never hides
28
+ the others.
29
+
30
+ ### Changed
31
+ - Sessions are now stored per programme at `~/.config/avios/accounts/<programme>.json`
32
+ (mode `600`); the old single `state.json` is migrated to `accounts/ba.json` on
33
+ first run.
34
+
9
35
  ## [0.2.1] - 2026-07-27
10
36
 
11
37
  ### Fixed
@@ -127,7 +153,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
127
153
  - Reward-flight **availability** search is not yet implemented (needs a British
128
154
  Airways capture).
129
155
 
130
- [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.2.1...HEAD
156
+ [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.3.0...HEAD
157
+ [0.3.0]: https://github.com/alexechoi/avios-cli/compare/v0.2.1...v0.3.0
131
158
  [0.2.1]: https://github.com/alexechoi/avios-cli/compare/v0.2.0...v0.2.1
132
159
  [0.2.0]: https://github.com/alexechoi/avios-cli/compare/v0.1.6...v0.2.0
133
160
  [0.1.6]: https://github.com/alexechoi/avios-cli/compare/v0.1.5...v0.1.6
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avios-cli
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: A CLI and TUI for avios.com — view your Avios balance and transactions from the terminal.
5
5
  Project-URL: Homepage, https://github.com/alexechoi/avios-cli
6
6
  Project-URL: Repository, https://github.com/alexechoi/avios-cli
@@ -39,13 +39,14 @@ Description-Content-Type: text/markdown
39
39
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
40
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
41
41
 
42
- A **CLI and TUI for [avios.com](https://www.avios.com)** — check your Avios balance,
43
- browse your transactions and manage your account without leaving the terminal.
42
+ A **CLI and TUI for your Avios programmes** — check balances and browse transactions
43
+ without leaving the terminal. British Airways and Iberia use avios.com; Finnair Plus
44
+ uses Finnair's own OAuth and loyalty API.
44
45
 
45
46
  > ⚠️ **Unofficial.** This project is not affiliated with, authorised by, or endorsed by
46
- > Avios, British Airways or IAG Loyalty. It drives the same private endpoints the
47
- > avios.com website uses, with your own logged-in session. Use at your own risk; it may
48
- > break at any time and may be against the provider's terms of service.
47
+ > Avios, British Airways, Finnair or IAG Loyalty. It drives the same private endpoints
48
+ > the providers' websites use, with your own logged-in session. Use at your own risk;
49
+ > it may break at any time and may be against the provider's terms of service.
49
50
 
50
51
  ![avios TUI dashboard](docs/dashboard.svg)
51
52
 
@@ -84,6 +85,7 @@ on the dashboard. It needs the **`login` extra**:
84
85
 
85
86
  ```bash
86
87
  uvx --from 'avios-cli[login]' avios login
88
+ uvx --from 'avios-cli[login]' avios login finnair
87
89
  ```
88
90
 
89
91
  It launches your installed **Google Chrome** (no extra download). If you don't have
@@ -113,22 +115,34 @@ browser. Use `--from-browser` instead: log into avios.com in your normal Chrome
113
115
  (you'll get one normal captcha), then run
114
116
  `uvx --from 'avios-cli[login]' avios login --from-browser`.
115
117
 
116
- Your session cookie is stored at `~/.config/avios/state.json` (mode `600`). It
117
- expires after ~a day; just run `avios login` again. Run `avios logout` to remove it.
118
+ Finnair uses a different CAS/OAuth flow. `avios login finnair` opens the Finnair Plus
119
+ balance page, lets you complete password and MFA in the real browser, and captures the
120
+ OAuth session from the authenticated loyalty request. `--from-browser` is not available
121
+ for Finnair because browser-cookie import cannot read that token.
122
+
123
+ Each programme session is stored at `~/.config/avios/accounts/<programme>.json`
124
+ (mode `600`). Sessions expire; just log in to that programme again. Use
125
+ `avios logout <programme>` for one account or `avios logout` for all accounts.
118
126
 
119
127
  ## Usage
120
128
 
129
+ Commands span **all logged-in accounts** by default and show a combined total; add
130
+ `--account/-a <programme>` to focus on one.
131
+
121
132
  ```bash
122
- avios balance # total / individual / household Avios
123
- avios transactions --limit 20 # recent transactions (date, description, Avios, type)
124
- avios pending # pending Avios
133
+ avios accounts # every logged-in account: balance + status
134
+ avios balance # per-account balances + combined total
135
+ avios transactions --limit 20 # recent transactions, merged across accounts
136
+ avios pending # pending Avios, merged across accounts
137
+ avios balance --account iberia # just one programme
125
138
  avios overview # dashboard summary
126
139
  avios whoami # name, tier, membership, email
127
140
  avios raw /shell/api/users/current/accounts # hit any endpoint directly
128
141
  ```
129
142
 
130
- Add `--json` to `balance`, `transactions`, `pending` or `whoami` for scriptable
131
- output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
143
+ Add `--json` to `accounts`, `balance`, `transactions`, `pending` or `whoami` for
144
+ scriptable output. With a single account, `balance` keeps the individual/household
145
+ breakdown.
132
146
 
133
147
  ## TUI
134
148
 
@@ -136,8 +150,9 @@ output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
136
150
  avios tui
137
151
  ```
138
152
 
139
- A full-screen dashboard: your balance in the header and a scrollable transactions
140
- table. Press `r` to refresh, `q` to quit.
153
+ A full-screen dashboard across all your accounts: a combined balance header (with a
154
+ per-programme breakdown) and a scrollable transactions table with a Programme column,
155
+ merged newest-first. Press `r` to refresh, `q` to quit.
141
156
 
142
157
  ## Roadmap
143
158
 
@@ -147,6 +162,7 @@ table. Press `r` to refresh, `q` to quit.
147
162
  - [x] Browser-assisted `avios login`
148
163
  - [x] CLI commands
149
164
  - [x] Textual TUI dashboard
165
+ - [x] Multiple accounts (BA, Iberia, Aer Lingus, Finnair) with combined views
150
166
  - [ ] Reward-flight **availability** search _(coming soon — needs a British Airways capture)_
151
167
 
152
168
  ## Development
@@ -163,8 +179,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow and
163
179
 
164
180
  ## Security
165
181
 
166
- No passwords are handled or stored — only your session cookie, kept locally at
167
- `~/.config/avios/state.json` (mode `600`). See [SECURITY.md](SECURITY.md).
182
+ No passwords are handled or stored — only programme session cookies or OAuth tokens,
183
+ kept locally under `~/.config/avios/accounts/` (mode `600`). See
184
+ [SECURITY.md](SECURITY.md).
168
185
 
169
186
  ## License
170
187
 
@@ -4,13 +4,14 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
6
6
 
7
- A **CLI and TUI for [avios.com](https://www.avios.com)** — check your Avios balance,
8
- browse your transactions and manage your account without leaving the terminal.
7
+ A **CLI and TUI for your Avios programmes** — check balances and browse transactions
8
+ without leaving the terminal. British Airways and Iberia use avios.com; Finnair Plus
9
+ uses Finnair's own OAuth and loyalty API.
9
10
 
10
11
  > ⚠️ **Unofficial.** This project is not affiliated with, authorised by, or endorsed by
11
- > Avios, British Airways or IAG Loyalty. It drives the same private endpoints the
12
- > avios.com website uses, with your own logged-in session. Use at your own risk; it may
13
- > break at any time and may be against the provider's terms of service.
12
+ > Avios, British Airways, Finnair or IAG Loyalty. It drives the same private endpoints
13
+ > the providers' websites use, with your own logged-in session. Use at your own risk;
14
+ > it may break at any time and may be against the provider's terms of service.
14
15
 
15
16
  ![avios TUI dashboard](docs/dashboard.svg)
16
17
 
@@ -49,6 +50,7 @@ on the dashboard. It needs the **`login` extra**:
49
50
 
50
51
  ```bash
51
52
  uvx --from 'avios-cli[login]' avios login
53
+ uvx --from 'avios-cli[login]' avios login finnair
52
54
  ```
53
55
 
54
56
  It launches your installed **Google Chrome** (no extra download). If you don't have
@@ -78,22 +80,34 @@ browser. Use `--from-browser` instead: log into avios.com in your normal Chrome
78
80
  (you'll get one normal captcha), then run
79
81
  `uvx --from 'avios-cli[login]' avios login --from-browser`.
80
82
 
81
- Your session cookie is stored at `~/.config/avios/state.json` (mode `600`). It
82
- expires after ~a day; just run `avios login` again. Run `avios logout` to remove it.
83
+ Finnair uses a different CAS/OAuth flow. `avios login finnair` opens the Finnair Plus
84
+ balance page, lets you complete password and MFA in the real browser, and captures the
85
+ OAuth session from the authenticated loyalty request. `--from-browser` is not available
86
+ for Finnair because browser-cookie import cannot read that token.
87
+
88
+ Each programme session is stored at `~/.config/avios/accounts/<programme>.json`
89
+ (mode `600`). Sessions expire; just log in to that programme again. Use
90
+ `avios logout <programme>` for one account or `avios logout` for all accounts.
83
91
 
84
92
  ## Usage
85
93
 
94
+ Commands span **all logged-in accounts** by default and show a combined total; add
95
+ `--account/-a <programme>` to focus on one.
96
+
86
97
  ```bash
87
- avios balance # total / individual / household Avios
88
- avios transactions --limit 20 # recent transactions (date, description, Avios, type)
89
- avios pending # pending Avios
98
+ avios accounts # every logged-in account: balance + status
99
+ avios balance # per-account balances + combined total
100
+ avios transactions --limit 20 # recent transactions, merged across accounts
101
+ avios pending # pending Avios, merged across accounts
102
+ avios balance --account iberia # just one programme
90
103
  avios overview # dashboard summary
91
104
  avios whoami # name, tier, membership, email
92
105
  avios raw /shell/api/users/current/accounts # hit any endpoint directly
93
106
  ```
94
107
 
95
- Add `--json` to `balance`, `transactions`, `pending` or `whoami` for scriptable
96
- output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
108
+ Add `--json` to `accounts`, `balance`, `transactions`, `pending` or `whoami` for
109
+ scriptable output. With a single account, `balance` keeps the individual/household
110
+ breakdown.
97
111
 
98
112
  ## TUI
99
113
 
@@ -101,8 +115,9 @@ output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
101
115
  avios tui
102
116
  ```
103
117
 
104
- A full-screen dashboard: your balance in the header and a scrollable transactions
105
- table. Press `r` to refresh, `q` to quit.
118
+ A full-screen dashboard across all your accounts: a combined balance header (with a
119
+ per-programme breakdown) and a scrollable transactions table with a Programme column,
120
+ merged newest-first. Press `r` to refresh, `q` to quit.
106
121
 
107
122
  ## Roadmap
108
123
 
@@ -112,6 +127,7 @@ table. Press `r` to refresh, `q` to quit.
112
127
  - [x] Browser-assisted `avios login`
113
128
  - [x] CLI commands
114
129
  - [x] Textual TUI dashboard
130
+ - [x] Multiple accounts (BA, Iberia, Aer Lingus, Finnair) with combined views
115
131
  - [ ] Reward-flight **availability** search _(coming soon — needs a British Airways capture)_
116
132
 
117
133
  ## Development
@@ -128,8 +144,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow and
128
144
 
129
145
  ## Security
130
146
 
131
- No passwords are handled or stored — only your session cookie, kept locally at
132
- `~/.config/avios/state.json` (mode `600`). See [SECURITY.md](SECURITY.md).
147
+ No passwords are handled or stored — only programme session cookies or OAuth tokens,
148
+ kept locally under `~/.config/avios/accounts/` (mode `600`). See
149
+ [SECURITY.md](SECURITY.md).
133
150
 
134
151
  ## License
135
152
 
@@ -3,21 +3,20 @@
3
3
  ## How avios-cli handles your credentials
4
4
 
5
5
  - **No passwords are ever handled or stored.** Login happens in a real browser
6
- (Auth0 Universal Login + hCaptcha + MFA); avios-cli only captures the resulting
7
- **session cookie**.
8
- - The cookie is stored locally at `~/.config/avios/state.json` with file mode
9
- `600` (owner read/write only). It is never transmitted anywhere except to
10
- avios.com in the requests you make.
11
- - `avios logout` deletes the stored session. The cookie also expires on the
12
- server side after roughly a day.
6
+ (the programme's normal login and MFA); avios-cli only captures the resulting
7
+ **session cookie or OAuth token**.
8
+ - Sessions are stored locally under `~/.config/avios/accounts/` with file mode
9
+ `600` (owner read/write only). Each credential is sent only to its programme's
10
+ API in the requests you make.
11
+ - `avios logout` deletes stored sessions. They also expire on the server side.
13
12
  - `AVIOS_COOKIE` may be set to supply a cookie via the environment instead of the
14
13
  stored file — avoid committing it or storing it in shell history.
15
14
 
16
15
  ## Unofficial software
17
16
 
18
- avios-cli is **not affiliated with Avios, British Airways or IAG Loyalty** and
19
- uses private, undocumented endpoints. Using it may breach the provider's terms of
20
- service and could affect your account. Use at your own risk.
17
+ avios-cli is **not affiliated with Avios, British Airways, Finnair or IAG
18
+ Loyalty** and uses private, undocumented endpoints. Using it may breach the
19
+ provider's terms of service and could affect your account. Use at your own risk.
21
20
 
22
21
  ## Reporting a vulnerability
23
22
 
@@ -0,0 +1,190 @@
1
+ <svg class="rich-terminal" viewBox="0 0 1190 782.0" xmlns="http://www.w3.org/2000/svg">
2
+ <!-- Generated with Rich https://www.textualize.io -->
3
+ <style>
4
+
5
+ @font-face {
6
+ font-family: "Fira Code";
7
+ src: local("FiraCode-Regular"),
8
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
9
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
10
+ font-style: normal;
11
+ font-weight: 400;
12
+ }
13
+ @font-face {
14
+ font-family: "Fira Code";
15
+ src: local("FiraCode-Bold"),
16
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
17
+ url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
18
+ font-style: bold;
19
+ font-weight: 700;
20
+ }
21
+
22
+ .terminal-1749240985-matrix {
23
+ font-family: Fira Code, monospace;
24
+ font-size: 20px;
25
+ line-height: 24.4px;
26
+ font-variant-east-asian: full-width;
27
+ }
28
+
29
+ .terminal-1749240985-title {
30
+ font-size: 18px;
31
+ font-weight: bold;
32
+ font-family: arial;
33
+ }
34
+
35
+ .terminal-1749240985-r1 { fill: #c5c8c6 }
36
+ .terminal-1749240985-r2 { fill: #e0e0e0 }
37
+ .terminal-1749240985-r3 { fill: #a0a3a6 }
38
+ .terminal-1749240985-r4 { fill: #c5c6c7 }
39
+ .terminal-1749240985-r5 { fill: #00d7af;font-weight: bold }
40
+ .terminal-1749240985-r6 { fill: #00d7d7;font-weight: bold }
41
+ .terminal-1749240985-r7 { fill: #00afd7;font-weight: bold }
42
+ .terminal-1749240985-r8 { fill: #0087d7;font-weight: bold }
43
+ .terminal-1749240985-r9 { fill: #005fd7;font-weight: bold }
44
+ .terminal-1749240985-r10 { fill: #5f5fd7;font-weight: bold }
45
+ .terminal-1749240985-r11 { fill: #9e9e9e;font-style: italic; }
46
+ .terminal-1749240985-r12 { fill: #e0e0e0;font-weight: bold }
47
+ .terminal-1749240985-r13 { fill: #58d1eb;font-weight: bold }
48
+ .terminal-1749240985-r14 { fill: #9e9e9e }
49
+ .terminal-1749240985-r15 { fill: #0178d4 }
50
+ .terminal-1749240985-r16 { fill: #ddedf9;font-weight: bold }
51
+ .terminal-1749240985-r17 { fill: #ffa62b;font-weight: bold }
52
+ .terminal-1749240985-r18 { fill: #495259 }
53
+ </style>
54
+
55
+ <defs>
56
+ <clipPath id="terminal-1749240985-clip-terminal">
57
+ <rect x="0" y="0" width="1170.1999999999998" height="731.0" />
58
+ </clipPath>
59
+ <clipPath id="terminal-1749240985-line-0">
60
+ <rect x="0" y="1.5" width="1171.2" height="24.65"/>
61
+ </clipPath>
62
+ <clipPath id="terminal-1749240985-line-1">
63
+ <rect x="0" y="25.9" width="1171.2" height="24.65"/>
64
+ </clipPath>
65
+ <clipPath id="terminal-1749240985-line-2">
66
+ <rect x="0" y="50.3" width="1171.2" height="24.65"/>
67
+ </clipPath>
68
+ <clipPath id="terminal-1749240985-line-3">
69
+ <rect x="0" y="74.7" width="1171.2" height="24.65"/>
70
+ </clipPath>
71
+ <clipPath id="terminal-1749240985-line-4">
72
+ <rect x="0" y="99.1" width="1171.2" height="24.65"/>
73
+ </clipPath>
74
+ <clipPath id="terminal-1749240985-line-5">
75
+ <rect x="0" y="123.5" width="1171.2" height="24.65"/>
76
+ </clipPath>
77
+ <clipPath id="terminal-1749240985-line-6">
78
+ <rect x="0" y="147.9" width="1171.2" height="24.65"/>
79
+ </clipPath>
80
+ <clipPath id="terminal-1749240985-line-7">
81
+ <rect x="0" y="172.3" width="1171.2" height="24.65"/>
82
+ </clipPath>
83
+ <clipPath id="terminal-1749240985-line-8">
84
+ <rect x="0" y="196.7" width="1171.2" height="24.65"/>
85
+ </clipPath>
86
+ <clipPath id="terminal-1749240985-line-9">
87
+ <rect x="0" y="221.1" width="1171.2" height="24.65"/>
88
+ </clipPath>
89
+ <clipPath id="terminal-1749240985-line-10">
90
+ <rect x="0" y="245.5" width="1171.2" height="24.65"/>
91
+ </clipPath>
92
+ <clipPath id="terminal-1749240985-line-11">
93
+ <rect x="0" y="269.9" width="1171.2" height="24.65"/>
94
+ </clipPath>
95
+ <clipPath id="terminal-1749240985-line-12">
96
+ <rect x="0" y="294.3" width="1171.2" height="24.65"/>
97
+ </clipPath>
98
+ <clipPath id="terminal-1749240985-line-13">
99
+ <rect x="0" y="318.7" width="1171.2" height="24.65"/>
100
+ </clipPath>
101
+ <clipPath id="terminal-1749240985-line-14">
102
+ <rect x="0" y="343.1" width="1171.2" height="24.65"/>
103
+ </clipPath>
104
+ <clipPath id="terminal-1749240985-line-15">
105
+ <rect x="0" y="367.5" width="1171.2" height="24.65"/>
106
+ </clipPath>
107
+ <clipPath id="terminal-1749240985-line-16">
108
+ <rect x="0" y="391.9" width="1171.2" height="24.65"/>
109
+ </clipPath>
110
+ <clipPath id="terminal-1749240985-line-17">
111
+ <rect x="0" y="416.3" width="1171.2" height="24.65"/>
112
+ </clipPath>
113
+ <clipPath id="terminal-1749240985-line-18">
114
+ <rect x="0" y="440.7" width="1171.2" height="24.65"/>
115
+ </clipPath>
116
+ <clipPath id="terminal-1749240985-line-19">
117
+ <rect x="0" y="465.1" width="1171.2" height="24.65"/>
118
+ </clipPath>
119
+ <clipPath id="terminal-1749240985-line-20">
120
+ <rect x="0" y="489.5" width="1171.2" height="24.65"/>
121
+ </clipPath>
122
+ <clipPath id="terminal-1749240985-line-21">
123
+ <rect x="0" y="513.9" width="1171.2" height="24.65"/>
124
+ </clipPath>
125
+ <clipPath id="terminal-1749240985-line-22">
126
+ <rect x="0" y="538.3" width="1171.2" height="24.65"/>
127
+ </clipPath>
128
+ <clipPath id="terminal-1749240985-line-23">
129
+ <rect x="0" y="562.7" width="1171.2" height="24.65"/>
130
+ </clipPath>
131
+ <clipPath id="terminal-1749240985-line-24">
132
+ <rect x="0" y="587.1" width="1171.2" height="24.65"/>
133
+ </clipPath>
134
+ <clipPath id="terminal-1749240985-line-25">
135
+ <rect x="0" y="611.5" width="1171.2" height="24.65"/>
136
+ </clipPath>
137
+ <clipPath id="terminal-1749240985-line-26">
138
+ <rect x="0" y="635.9" width="1171.2" height="24.65"/>
139
+ </clipPath>
140
+ <clipPath id="terminal-1749240985-line-27">
141
+ <rect x="0" y="660.3" width="1171.2" height="24.65"/>
142
+ </clipPath>
143
+ <clipPath id="terminal-1749240985-line-28">
144
+ <rect x="0" y="684.7" width="1171.2" height="24.65"/>
145
+ </clipPath>
146
+ </defs>
147
+
148
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1188" height="780" rx="8"/><text class="terminal-1749240985-title" fill="#c5c8c6" text-anchor="middle" x="594" y="27">avios</text>
149
+ <g transform="translate(26,22)">
150
+ <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
151
+ <circle cx="22" cy="0" r="7" fill="#febc2e"/>
152
+ <circle cx="44" cy="0" r="7" fill="#28c840"/>
153
+ </g>
154
+
155
+ <g transform="translate(9, 41)" clip-path="url(#terminal-1749240985-clip-terminal)">
156
+ <rect fill="#242f38" x="0" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="12.2" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="24.4" y="1.5" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="85.4" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="97.6" y="1.5" width="256.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="353.8" y="1.5" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="414.8" y="1.5" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="451.4" y="1.5" width="329.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="780.8" y="1.5" width="268.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#2c363f" x="1049.2" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#2c363f" x="1061.4" y="1.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#2c363f" x="1159" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="353.8" y="50.3" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="805.2" y="50.3" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="353.8" y="74.7" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="805.2" y="74.7" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="353.8" y="99.1" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="805.2" y="99.1" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="353.8" y="123.5" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="805.2" y="123.5" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="353.8" y="147.9" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="805.2" y="147.9" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="353.8" y="172.3" width="451.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="805.2" y="172.3" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="402.6" y="196.7" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="768.6" y="196.7" width="402.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="24.4" y="221.1" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="146.4" y="221.1" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="219.6" y="221.1" width="573.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="793" y="221.1" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="1146.8" y="221.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="24.4" y="245.5" width="1122.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="1146.8" y="245.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#2d3740" x="0" y="294.3" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#2d3740" x="207.4" y="294.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#2d3740" x="353.8" y="294.3" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#2d3740" x="634.4" y="294.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#2d3740" x="744.2" y="294.3" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#2d3740" x="951.6" y="294.3" width="219.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="0" y="318.7" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="207.4" y="318.7" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="353.8" y="318.7" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="634.4" y="318.7" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="744.2" y="318.7" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="951.6" y="318.7" width="219.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="343.1" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="207.4" y="343.1" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="353.8" y="343.1" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="634.4" y="343.1" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="744.2" y="343.1" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="951.6" y="343.1" width="219.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="0" y="367.5" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="207.4" y="367.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="353.8" y="367.5" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="634.4" y="367.5" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="744.2" y="367.5" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="951.6" y="367.5" width="219.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="391.9" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="207.4" y="391.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="353.8" y="391.9" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="634.4" y="391.9" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="744.2" y="391.9" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="951.6" y="391.9" width="219.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="0" y="416.3" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="207.4" y="416.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="353.8" y="416.3" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="634.4" y="416.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="744.2" y="416.3" width="207.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1c1c1c" x="951.6" y="416.3" width="219.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="440.7" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="465.1" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="489.5" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="513.9" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="538.3" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="562.7" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="587.1" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="611.5" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="635.9" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="660.3" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#272727" x="0" y="684.7" width="1171.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="0" y="709.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="36.6" y="709.1" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="134.2" y="709.1" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="170.8" y="709.1" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="231.8" y="709.1" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="1024.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="1037" y="709.1" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="1061.4" y="709.1" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#242f38" x="1159" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/>
157
+ <g class="terminal-1749240985-matrix">
158
+ <text class="terminal-1749240985-r2" x="12.2" y="20" textLength="12.2" clip-path="url(#terminal-1749240985-line-0)">⭘</text><text class="terminal-1749240985-r2" x="353.8" y="20" textLength="61" clip-path="url(#terminal-1749240985-line-0)">avios</text><text class="terminal-1749240985-r3" x="414.8" y="20" textLength="36.6" clip-path="url(#terminal-1749240985-line-0)">&#160;—&#160;</text><text class="terminal-1749240985-r3" x="451.4" y="20" textLength="329.4" clip-path="url(#terminal-1749240985-line-0)">your&#160;Avios,&#160;in&#160;the&#160;terminal</text><text class="terminal-1749240985-r4" x="1061.4" y="20" textLength="97.6" clip-path="url(#terminal-1749240985-line-0)">14:28:47</text><text class="terminal-1749240985-r1" x="1171.2" y="20" textLength="12.2" clip-path="url(#terminal-1749240985-line-0)">
159
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1749240985-line-1)">
160
+ </text><text class="terminal-1749240985-r5" x="353.8" y="68.8" textLength="451.4" clip-path="url(#terminal-1749240985-line-2)">&#160;█████╗&#160;██╗&#160;&#160;&#160;██╗██╗&#160;██████╗&#160;███████╗</text><text class="terminal-1749240985-r1" x="1171.2" y="68.8" textLength="12.2" clip-path="url(#terminal-1749240985-line-2)">
161
+ </text><text class="terminal-1749240985-r6" x="353.8" y="93.2" textLength="451.4" clip-path="url(#terminal-1749240985-line-3)">██╔══██╗██║&#160;&#160;&#160;██║██║██╔═══██╗██╔════╝</text><text class="terminal-1749240985-r1" x="1171.2" y="93.2" textLength="12.2" clip-path="url(#terminal-1749240985-line-3)">
162
+ </text><text class="terminal-1749240985-r7" x="353.8" y="117.6" textLength="451.4" clip-path="url(#terminal-1749240985-line-4)">███████║██║&#160;&#160;&#160;██║██║██║&#160;&#160;&#160;██║███████╗</text><text class="terminal-1749240985-r1" x="1171.2" y="117.6" textLength="12.2" clip-path="url(#terminal-1749240985-line-4)">
163
+ </text><text class="terminal-1749240985-r8" x="353.8" y="142" textLength="451.4" clip-path="url(#terminal-1749240985-line-5)">██╔══██║╚██╗&#160;██╔╝██║██║&#160;&#160;&#160;██║╚════██║</text><text class="terminal-1749240985-r1" x="1171.2" y="142" textLength="12.2" clip-path="url(#terminal-1749240985-line-5)">
164
+ </text><text class="terminal-1749240985-r9" x="353.8" y="166.4" textLength="451.4" clip-path="url(#terminal-1749240985-line-6)">██║&#160;&#160;██║&#160;╚████╔╝&#160;██║╚██████╔╝███████║</text><text class="terminal-1749240985-r1" x="1171.2" y="166.4" textLength="12.2" clip-path="url(#terminal-1749240985-line-6)">
165
+ </text><text class="terminal-1749240985-r10" x="353.8" y="190.8" textLength="451.4" clip-path="url(#terminal-1749240985-line-7)">╚═╝&#160;&#160;╚═╝&#160;&#160;╚═══╝&#160;&#160;╚═╝&#160;╚═════╝&#160;╚══════╝</text><text class="terminal-1749240985-r1" x="1171.2" y="190.8" textLength="12.2" clip-path="url(#terminal-1749240985-line-7)">
166
+ </text><text class="terminal-1749240985-r11" x="402.6" y="215.2" textLength="366" clip-path="url(#terminal-1749240985-line-8)">✈&#160;&#160;your&#160;Avios,&#160;in&#160;the&#160;terminal</text><text class="terminal-1749240985-r1" x="1171.2" y="215.2" textLength="12.2" clip-path="url(#terminal-1749240985-line-8)">
167
+ </text><text class="terminal-1749240985-r12" x="24.4" y="239.6" textLength="122" clip-path="url(#terminal-1749240985-line-9)">Combined&#160;&#160;</text><text class="terminal-1749240985-r13" x="146.4" y="239.6" textLength="73.2" clip-path="url(#terminal-1749240985-line-9)">97,791</text><text class="terminal-1749240985-r14" x="219.6" y="239.6" textLength="573.4" clip-path="url(#terminal-1749240985-line-9)">&#160;&#160;&#160;&#160;&#160;British&#160;Airways&#160;75,751&#160;&#160;&#160;·&#160;&#160;&#160;Iberia&#160;22,040</text><text class="terminal-1749240985-r1" x="1171.2" y="239.6" textLength="12.2" clip-path="url(#terminal-1749240985-line-9)">
168
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="264" textLength="12.2" clip-path="url(#terminal-1749240985-line-10)">
169
+ </text><text class="terminal-1749240985-r15" x="0" y="288.4" textLength="1171.2" clip-path="url(#terminal-1749240985-line-11)">────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-1749240985-r1" x="1171.2" y="288.4" textLength="12.2" clip-path="url(#terminal-1749240985-line-11)">
170
+ </text><text class="terminal-1749240985-r12" x="0" y="312.8" textLength="207.4" clip-path="url(#terminal-1749240985-line-12)">&#160;Programme&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r12" x="207.4" y="312.8" textLength="146.4" clip-path="url(#terminal-1749240985-line-12)">&#160;Date&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r12" x="353.8" y="312.8" textLength="280.6" clip-path="url(#terminal-1749240985-line-12)">&#160;Description&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r12" x="634.4" y="312.8" textLength="109.8" clip-path="url(#terminal-1749240985-line-12)">&#160;Avios&#160;&#160;&#160;</text><text class="terminal-1749240985-r12" x="744.2" y="312.8" textLength="207.4" clip-path="url(#terminal-1749240985-line-12)">&#160;Type&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r1" x="1171.2" y="312.8" textLength="12.2" clip-path="url(#terminal-1749240985-line-12)">
171
+ </text><text class="terminal-1749240985-r16" x="0" y="337.2" textLength="207.4" clip-path="url(#terminal-1749240985-line-13)">&#160;British&#160;Airways&#160;</text><text class="terminal-1749240985-r16" x="207.4" y="337.2" textLength="146.4" clip-path="url(#terminal-1749240985-line-13)">&#160;2026-07-18&#160;</text><text class="terminal-1749240985-r16" x="353.8" y="337.2" textLength="280.6" clip-path="url(#terminal-1749240985-line-13)">&#160;Flight&#160;BA286&#160;SFO-LHR&#160;&#160;</text><text class="terminal-1749240985-r16" x="634.4" y="337.2" textLength="109.8" clip-path="url(#terminal-1749240985-line-13)">&#160;+8,500&#160;&#160;</text><text class="terminal-1749240985-r16" x="744.2" y="337.2" textLength="207.4" clip-path="url(#terminal-1749240985-line-13)">&#160;Collection&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r1" x="1171.2" y="337.2" textLength="12.2" clip-path="url(#terminal-1749240985-line-13)">
172
+ </text><text class="terminal-1749240985-r2" x="0" y="361.6" textLength="207.4" clip-path="url(#terminal-1749240985-line-14)">&#160;Iberia&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r2" x="207.4" y="361.6" textLength="146.4" clip-path="url(#terminal-1749240985-line-14)">&#160;2026-07-12&#160;</text><text class="terminal-1749240985-r2" x="353.8" y="361.6" textLength="280.6" clip-path="url(#terminal-1749240985-line-14)">&#160;Iberia&#160;IB3170&#160;MAD-LHR&#160;</text><text class="terminal-1749240985-r2" x="634.4" y="361.6" textLength="109.8" clip-path="url(#terminal-1749240985-line-14)">&#160;+4,200&#160;&#160;</text><text class="terminal-1749240985-r2" x="744.2" y="361.6" textLength="207.4" clip-path="url(#terminal-1749240985-line-14)">&#160;Reward&#160;Flight&#160;&#160;&#160;</text><text class="terminal-1749240985-r1" x="1171.2" y="361.6" textLength="12.2" clip-path="url(#terminal-1749240985-line-14)">
173
+ </text><text class="terminal-1749240985-r2" x="0" y="386" textLength="207.4" clip-path="url(#terminal-1749240985-line-15)">&#160;British&#160;Airways&#160;</text><text class="terminal-1749240985-r2" x="207.4" y="386" textLength="146.4" clip-path="url(#terminal-1749240985-line-15)">&#160;2026-07-02&#160;</text><text class="terminal-1749240985-r2" x="353.8" y="386" textLength="280.6" clip-path="url(#terminal-1749240985-line-15)">&#160;Tesco&#160;groceries&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r2" x="634.4" y="386" textLength="109.8" clip-path="url(#terminal-1749240985-line-15)">&#160;+240&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r2" x="744.2" y="386" textLength="207.4" clip-path="url(#terminal-1749240985-line-15)">&#160;BA&#160;Avios&#160;eStore&#160;</text><text class="terminal-1749240985-r1" x="1171.2" y="386" textLength="12.2" clip-path="url(#terminal-1749240985-line-15)">
174
+ </text><text class="terminal-1749240985-r2" x="0" y="410.4" textLength="207.4" clip-path="url(#terminal-1749240985-line-16)">&#160;Iberia&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r2" x="207.4" y="410.4" textLength="146.4" clip-path="url(#terminal-1749240985-line-16)">&#160;2026-06-21&#160;</text><text class="terminal-1749240985-r2" x="353.8" y="410.4" textLength="280.6" clip-path="url(#terminal-1749240985-line-16)">&#160;NH&#160;Hotels&#160;Madrid&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r2" x="634.4" y="410.4" textLength="109.8" clip-path="url(#terminal-1749240985-line-16)">&#160;+1,600&#160;&#160;</text><text class="terminal-1749240985-r2" x="744.2" y="410.4" textLength="207.4" clip-path="url(#terminal-1749240985-line-16)">&#160;Hotel&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r1" x="1171.2" y="410.4" textLength="12.2" clip-path="url(#terminal-1749240985-line-16)">
175
+ </text><text class="terminal-1749240985-r2" x="0" y="434.8" textLength="207.4" clip-path="url(#terminal-1749240985-line-17)">&#160;British&#160;Airways&#160;</text><text class="terminal-1749240985-r2" x="207.4" y="434.8" textLength="146.4" clip-path="url(#terminal-1749240985-line-17)">&#160;2026-06-09&#160;</text><text class="terminal-1749240985-r2" x="353.8" y="434.8" textLength="280.6" clip-path="url(#terminal-1749240985-line-17)">&#160;Redemption&#160;LHR-JFK&#160;&#160;&#160;&#160;</text><text class="terminal-1749240985-r2" x="634.4" y="434.8" textLength="109.8" clip-path="url(#terminal-1749240985-line-17)">&#160;-50,000&#160;</text><text class="terminal-1749240985-r2" x="744.2" y="434.8" textLength="207.4" clip-path="url(#terminal-1749240985-line-17)">&#160;Reward&#160;Flight&#160;&#160;&#160;</text><text class="terminal-1749240985-r1" x="1171.2" y="434.8" textLength="12.2" clip-path="url(#terminal-1749240985-line-17)">
176
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="459.2" textLength="12.2" clip-path="url(#terminal-1749240985-line-18)">
177
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="483.6" textLength="12.2" clip-path="url(#terminal-1749240985-line-19)">
178
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="508" textLength="12.2" clip-path="url(#terminal-1749240985-line-20)">
179
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="532.4" textLength="12.2" clip-path="url(#terminal-1749240985-line-21)">
180
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="556.8" textLength="12.2" clip-path="url(#terminal-1749240985-line-22)">
181
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="581.2" textLength="12.2" clip-path="url(#terminal-1749240985-line-23)">
182
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="605.6" textLength="12.2" clip-path="url(#terminal-1749240985-line-24)">
183
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="630" textLength="12.2" clip-path="url(#terminal-1749240985-line-25)">
184
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="654.4" textLength="12.2" clip-path="url(#terminal-1749240985-line-26)">
185
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="678.8" textLength="12.2" clip-path="url(#terminal-1749240985-line-27)">
186
+ </text><text class="terminal-1749240985-r1" x="1171.2" y="703.2" textLength="12.2" clip-path="url(#terminal-1749240985-line-28)">
187
+ </text><text class="terminal-1749240985-r17" x="0" y="727.6" textLength="36.6" clip-path="url(#terminal-1749240985-line-29)">&#160;r&#160;</text><text class="terminal-1749240985-r2" x="36.6" y="727.6" textLength="97.6" clip-path="url(#terminal-1749240985-line-29)">Refresh&#160;</text><text class="terminal-1749240985-r17" x="134.2" y="727.6" textLength="36.6" clip-path="url(#terminal-1749240985-line-29)">&#160;q&#160;</text><text class="terminal-1749240985-r2" x="170.8" y="727.6" textLength="61" clip-path="url(#terminal-1749240985-line-29)">Quit&#160;</text><text class="terminal-1749240985-r18" x="1024.8" y="727.6" textLength="12.2" clip-path="url(#terminal-1749240985-line-29)">▏</text><text class="terminal-1749240985-r17" x="1037" y="727.6" textLength="24.4" clip-path="url(#terminal-1749240985-line-29)">^p</text><text class="terminal-1749240985-r2" x="1061.4" y="727.6" textLength="97.6" clip-path="url(#terminal-1749240985-line-29)">&#160;palette</text>
188
+ </g>
189
+ </g>
190
+ </svg>
@@ -0,0 +1,84 @@
1
+ """Generate the TUI screenshot (SVG) used in the README.
2
+
3
+ Runs the multi-account dashboard headlessly with demo data (two programmes) and
4
+ exports an SVG.
5
+
6
+ uv run python scripts/screenshot.py
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from avios import aggregate
16
+ from avios.accounts import Account
17
+ from avios.aggregate import AccountBalance, TaggedTransaction
18
+ from avios.models import Balance, Transaction
19
+ from avios.programmes import get_programme
20
+ from avios.tui.app import AviosApp
21
+
22
+ DOCS = Path(__file__).resolve().parent.parent / "docs"
23
+
24
+ _BA = Account.from_programme(get_programme("ba"), cookies=[{"name": "s", "value": "ba"}])
25
+ _IB = Account.from_programme(get_programme("iberia"), cookies=[{"name": "s", "value": "ib"}])
26
+
27
+ _BALANCES = {"ba": 75751, "iberia": 22040}
28
+ _DEMO = [
29
+ (_BA, "2026-07-18", "Flight BA286 SFO-LHR", 8500, "Collection"),
30
+ (_IB, "2026-07-12", "Iberia IB3170 MAD-LHR", 4200, "Reward Flight"),
31
+ (_BA, "2026-07-02", "Tesco groceries", 240, "BA Avios eStore"),
32
+ (_IB, "2026-06-21", "NH Hotels Madrid", 1600, "Hotel"),
33
+ (_BA, "2026-06-09", "Redemption LHR-JFK", -50000, "Reward Flight"),
34
+ ]
35
+
36
+
37
+ def _demo_balances(accounts: list[Account], **_: Any) -> list[AccountBalance]:
38
+ return [
39
+ AccountBalance(
40
+ a,
41
+ Balance(
42
+ balance=_BALANCES[a.slug],
43
+ individual=_BALANCES[a.slug],
44
+ household=112430 if a.slug == "ba" else None,
45
+ ),
46
+ )
47
+ for a in accounts
48
+ ]
49
+
50
+
51
+ def _demo_transactions(accounts: list[Account], **_: Any) -> list[TaggedTransaction]:
52
+ items = [
53
+ TaggedTransaction(
54
+ acc,
55
+ Transaction.model_validate(
56
+ {
57
+ "dateProcessed": f"{date}T09:00:00Z",
58
+ "description": desc,
59
+ "amount": amount,
60
+ "type": {"value": kind},
61
+ }
62
+ ),
63
+ )
64
+ for acc, date, desc, amount, kind in _DEMO
65
+ ]
66
+ items.sort(key=lambda t: t.transaction.date_processed or "", reverse=True)
67
+ return items
68
+
69
+
70
+ async def main() -> None:
71
+ DOCS.mkdir(exist_ok=True)
72
+ # Swap the aggregation layer for demo data (no network) before the app loads.
73
+ aggregate.all_balances = _demo_balances # type: ignore[assignment]
74
+ aggregate.merged_transactions = _demo_transactions # type: ignore[assignment]
75
+ app = AviosApp(accounts=[_BA, _IB])
76
+ async with app.run_test(size=(96, 30)) as pilot:
77
+ await app._load()
78
+ await pilot.pause()
79
+ app.save_screenshot(filename="dashboard.svg", path=str(DOCS))
80
+ print(f"wrote {DOCS / 'dashboard.svg'}")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ asyncio.run(main())
@@ -3,4 +3,4 @@
3
3
  View your Avios balance, transactions and account details from the terminal.
4
4
  """
5
5
 
6
- __version__ = "0.2.1"
6
+ __version__ = "0.3.0"