Skip to content

Commit 522cf0d

Browse files
feat: Add stability indicators for broker integrations and tooltips for unstable portfolios (#241)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 4f16752 commit 522cf0d

File tree

6 files changed

+64
-9
lines changed

6 files changed

+64
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ _This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) a
2222

2323
### Changed
2424

25+
- Add stability indicators for broker integrations and tooltips for unstable portfolios
26+
2527
### Fixed
2628

2729
- **Portfolio**:

docs/Bitvavo.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Bitvavo Integration Guide
22

3+
> **⚠️ Bitvavo integration is not yet stable. Features and reliability may change in future releases. Use with caution.**
4+
35
[Bitvavo](https://bitvavo.com/en/) is a cryptocurrency exchange supported by **Stonks Overwatch**, allowing you to track your crypto investments, portfolio value, and growth alongside your traditional investments.
46

57
## Overview

docs/IBKR.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# IBKR (Interactive Brokers) Integration Guide
22

3+
> **⚠️ IBKR integration is not yet stable. Features and reliability may change in future releases. Use with caution.**
4+
35
Interactive Brokers (IBKR) is a global broker supported by **Stonks Overwatch**, providing access to international markets and securities.
46

57
## Overview

src/stonks_overwatch/services/models.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -348,28 +348,29 @@ def formatted_percentage_realized_gain(self) -> str:
348348

349349
class PortfolioId(Enum):
350350
ALL = ("all", "Portfolios", "/static/stonks_overwatch.svg")
351-
DEGIRO = ("degiro", "DeGiro", "/static/logos/degiro.svg")
352-
BITVAVO = ("bitvavo", "Bitvavo", "/static/logos/bitvavo.svg")
353-
IBKR = ("ibkr", "IBKR", "/static/logos/ibkr.svg")
351+
DEGIRO = ("degiro", "DEGIRO", "/static/logos/degiro.svg")
352+
BITVAVO = ("bitvavo", "Bitvavo", "/static/logos/bitvavo.svg", False)
353+
IBKR = ("ibkr", "IBKR", "/static/logos/ibkr.svg", False)
354354

355-
def __init__(self, id: str, long_name: str, logo: str):
355+
def __init__(self, id: str, long_name: str, logo: str, stable: bool = True):
356356
self.id = id
357357
self.long_name = long_name
358358
self.logo = logo
359+
self.stable = stable
359360

360361
@classmethod
361362
def values(cls) -> list["PortfolioId"]:
362363
return list(cls)
363364

364365
@classmethod
365-
def from_id(cls, id: str):
366+
def from_id(cls, broker_id: str):
366367
for portfolio in cls:
367-
if portfolio.id == id:
368+
if portfolio.id == broker_id:
368369
return portfolio
369370
return cls.ALL
370371

371372
def to_dict(self):
372-
return {"id": self.id, "name": self.long_name, "logo": self.logo}
373+
return {"id": self.id, "name": self.long_name, "logo": self.logo, "stable": self.stable}
373374

374375

375376
@dataclass

src/stonks_overwatch/templates/base.html

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@
165165
<script src="{% static 'bootstrap-table/dist/themes/bootstrap-table/bootstrap-table.min.js' %}"></script>
166166

167167
<script type="text/javascript">
168+
// Tooltip message for unstable portfolios
169+
const IN_DEVELOPMENT_MSG = 'is in development';
170+
// Wrench emoji HTML for unstable portfolios (no custom class, just Bootstrap)
171+
const WRENCH_HTML = '<span class="ms-2 float-end">🧪️</span>';
172+
168173
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
169174
let tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
170175
const tooltip = new bootstrap.Tooltip(tooltipTriggerEl);
@@ -185,21 +190,55 @@
185190
.then(response => response.json())
186191
.then(data => {
187192
const selectedPortfolio = document.getElementById("selectPortfolio");
188-
selectedPortfolio.innerHTML = `<img src="${data.selected_portfolio.logo}" alt="${data.selected_portfolio.id}" width="25" height="25" class="rounded-circle me-2 sidebar-icon flex-shrink-0"><span class="sidebar-text">${data.selected_portfolio.name}</span>`;
193+
// Always keep dropdown functionality
194+
selectedPortfolio.setAttribute("data-bs-toggle", "dropdown");
195+
selectedPortfolio.setAttribute("aria-expanded", "false");
196+
selectedPortfolio.classList.add("dropdown-toggle");
197+
selectedPortfolio.classList.remove("non-clickable");
198+
199+
// Consistent stability check: treat as stable unless stable is explicitly false
200+
const selectedStable = data.selected_portfolio.stable !== false;
201+
let selectedPortfolioHTML = `<img src="${data.selected_portfolio.logo}" alt="${data.selected_portfolio.id}" width="25" height="25" class="rounded-circle me-2 sidebar-icon flex-shrink-0"><span class="sidebar-text">${data.selected_portfolio.name}</span>`;
202+
if (!selectedStable) {
203+
selectedPortfolioHTML += WRENCH_HTML;
204+
selectedPortfolio.setAttribute("title", `${data.selected_portfolio.name} ${IN_DEVELOPMENT_MSG}`);
205+
// Initialize tooltip for selectPortfolio
206+
if (!bootstrap.Tooltip.getInstance(selectedPortfolio)) {
207+
new bootstrap.Tooltip(selectedPortfolio);
208+
}
209+
} else {
210+
selectedPortfolio.removeAttribute("title");
211+
const tooltipInstance = bootstrap.Tooltip.getInstance(selectedPortfolio);
212+
if (tooltipInstance) tooltipInstance.dispose();
213+
}
214+
selectedPortfolio.innerHTML = selectedPortfolioHTML;
189215

190216
const portfolioMenu = document.getElementById("sidebar-portfolio-menu");
191217
portfolioMenu.innerHTML = ""; // Clear existing menu items
192218

193219
if (data.available_portfolios.length === 1) {
194220
// Remove the dropdown toggle if there is only one portfolio
195221
selectedPortfolio.removeAttribute("data-bs-toggle");
222+
selectedPortfolio.removeAttribute("aria-expanded");
196223
selectedPortfolio.classList.remove("dropdown-toggle");
197224
selectedPortfolio.classList.add("non-clickable");
198225
} else {
199226
// Show the portfolio selector dropdown only when there are multiple portfolios
200227
data.available_portfolios.forEach(portfolio => {
201228
const menuItem = document.createElement("li");
202-
menuItem.innerHTML = `<a class="dropdown-item portfolio-link-filter" href="#" data-value="${portfolio.id}"><img src="${portfolio.logo}" alt="${portfolio.id}" width="25" height="25" class="rounded-circle me-2">${portfolio.name}</a>`;
229+
// Consistent stability check for each portfolio
230+
const isStable = portfolio.stable !== false;
231+
const tooltipAttrs = !isStable ? ` data-bs-toggle=\"tooltip\" title=\"${portfolio.name} ${IN_DEVELOPMENT_MSG}\"` : '';
232+
const menuItemHTML = `
233+
<a class="dropdown-item portfolio-link-filter d-flex align-items-center justify-content-between" href="#" data-value="${portfolio.id}"${tooltipAttrs}>
234+
<span>
235+
<img src="${portfolio.logo}" alt="${portfolio.id}" width="25" height="25" class="rounded-circle me-2">
236+
${portfolio.name}
237+
</span>
238+
${!isStable ? WRENCH_HTML : ''}
239+
</a>
240+
`;
241+
menuItem.innerHTML = menuItemHTML;
203242

204243
// Add click event listener to menuItem
205244
menuItem.querySelector("a").addEventListener("click", function(event) {
@@ -225,6 +264,14 @@
225264
});
226265
}
227266

267+
// Initialize tooltips for new elements
268+
const newTooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
269+
newTooltipTriggerList.forEach(function (tooltipTriggerEl) {
270+
if (!bootstrap.Tooltip.getInstance(tooltipTriggerEl)) {
271+
new bootstrap.Tooltip(tooltipTriggerEl);
272+
}
273+
});
274+
228275
// Restore sidebar state after portfolio data is loaded
229276
restoreSidebarState();
230277
});

tests/stonks_overwatch/services/test_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_portfolio_ids():
2727
assert as_dict["id"] == portfolio_id.id
2828
assert as_dict["name"] == portfolio_id.long_name
2929
assert as_dict["logo"] == portfolio_id.logo
30+
assert as_dict["stable"] == portfolio_id.stable
3031

3132
assert PortfolioId.from_id("XXX") == PortfolioId.ALL
3233

0 commit comments

Comments
 (0)