diff --git a/README.md b/README.md index d117c6f..1b88198 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,8 @@ The website will be available at `http://localhost:9810`. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Attribution + +This website is built using the [Bootstrap](https://getbootstrap.com) framework +and [Phosphor Icons](https://phosphoricons.com). \ No newline at end of file diff --git a/assets/css/base.css b/assets/css/base.css index 94aa844..8481a04 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -85,6 +85,48 @@ h5 { margin-top: 10px; } +.currency-col { + width: 200px; + white-space: nowrap; + text-align: right; +} + +.table-transparency td:not(:first-child) { + text-align: right; +} + +.section { + padding: 20px 0; + border-bottom: 1px solid #e0e0e0; +} + +.alert-warning { + background-color: #fff3cd; + border-color: #ffeeba; + color: #856404; + padding: 15px; + margin-bottom: 20px; + border-radius: 4px; +} + +.alert-warning .alert-link { + color: #856404; + font-weight: bold; + text-decoration: underline; +} + +.alert-warning .alert-link:hover { + color: #604c2e; +} + +.bs-icon.bs-icon-primary svg { + fill: var(--bs-primary-bg-subtle); +} + +.bs-icon.bs-icon-lg svg { + fill: var(--bs-primary) +} + /* Responsive Styles */ @media (max-width: 768px) { .navbar .container { diff --git a/assets/dist/icons/article-ny-times.svg b/assets/dist/icons/article-ny-times.svg new file mode 100644 index 0000000..f4ed00a --- /dev/null +++ b/assets/dist/icons/article-ny-times.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/calendar.svg b/assets/dist/icons/calendar.svg new file mode 100644 index 0000000..c066f4a --- /dev/null +++ b/assets/dist/icons/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/chats.svg b/assets/dist/icons/chats.svg new file mode 100644 index 0000000..2d1e90a --- /dev/null +++ b/assets/dist/icons/chats.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/coffee.svg b/assets/dist/icons/coffee.svg new file mode 100644 index 0000000..4d4e30a --- /dev/null +++ b/assets/dist/icons/coffee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/database.svg b/assets/dist/icons/database.svg new file mode 100644 index 0000000..8d916c3 --- /dev/null +++ b/assets/dist/icons/database.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/envelope.svg b/assets/dist/icons/envelope.svg new file mode 100644 index 0000000..30b1cee --- /dev/null +++ b/assets/dist/icons/envelope.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/git-branch.svg b/assets/dist/icons/git-branch.svg new file mode 100644 index 0000000..1b917dd --- /dev/null +++ b/assets/dist/icons/git-branch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/images.svg b/assets/dist/icons/images.svg new file mode 100644 index 0000000..65a3f60 --- /dev/null +++ b/assets/dist/icons/images.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/leaf.svg b/assets/dist/icons/leaf.svg new file mode 100644 index 0000000..e9cfc46 --- /dev/null +++ b/assets/dist/icons/leaf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/lightbulb.svg b/assets/dist/icons/lightbulb.svg new file mode 100644 index 0000000..918dbe2 --- /dev/null +++ b/assets/dist/icons/lightbulb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/link.svg b/assets/dist/icons/link.svg new file mode 100644 index 0000000..da45a28 --- /dev/null +++ b/assets/dist/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/magnifying-glass.svg b/assets/dist/icons/magnifying-glass.svg new file mode 100644 index 0000000..bf4e505 --- /dev/null +++ b/assets/dist/icons/magnifying-glass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/markdown-logo.svg b/assets/dist/icons/markdown-logo.svg new file mode 100644 index 0000000..7a14e6b --- /dev/null +++ b/assets/dist/icons/markdown-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/mastodon-logo.svg b/assets/dist/icons/mastodon-logo.svg new file mode 100644 index 0000000..313c57c --- /dev/null +++ b/assets/dist/icons/mastodon-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/matrix-logo.svg b/assets/dist/icons/matrix-logo.svg new file mode 100644 index 0000000..3a0190d --- /dev/null +++ b/assets/dist/icons/matrix-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/network.svg b/assets/dist/icons/network.svg new file mode 100644 index 0000000..62b51c5 --- /dev/null +++ b/assets/dist/icons/network.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/pencil-ruler.svg b/assets/dist/icons/pencil-ruler.svg new file mode 100644 index 0000000..c66af52 --- /dev/null +++ b/assets/dist/icons/pencil-ruler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/video.svg b/assets/dist/icons/video.svg new file mode 100644 index 0000000..d711517 --- /dev/null +++ b/assets/dist/icons/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/dist/icons/webcam.svg b/assets/dist/icons/webcam.svg new file mode 100644 index 0000000..abd8965 --- /dev/null +++ b/assets/dist/icons/webcam.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/finances.json b/data/finances.json new file mode 100644 index 0000000..b047d7a --- /dev/null +++ b/data/finances.json @@ -0,0 +1,33 @@ +{ + "2024": { + "4": { + "Membership Fees": { + "EUR": 365 + }, + "Donations": {}, + "Server Costs": { + "EUR": -216.57 + }, + "Domain Names": {}, + "Operating Expenses": { + "EUR": -36.10 + }, + "Conversions": {} + }, + "5": { + "Membership Fees": { + "EUR": 390 + }, + "Donations": { + "BTC": 0.000434, + "XMR": 0.447661805527 + }, + "Server Costs": { + "EUR": -430.04 + }, + "Domain Names": {}, + "Operating Expenses": {}, + "Conversions": {} + } + } +} \ No newline at end of file diff --git a/services.json b/data/services.json similarity index 89% rename from services.json rename to data/services.json index 7a20116..2e4dc7d 100644 --- a/services.json +++ b/data/services.json @@ -12,6 +12,7 @@ "url": "https://element.private.coffee" } ], + "icon": "matrix-logo", "exclude_from_index": false, "exclude_from_simple": true }, @@ -27,6 +28,7 @@ "url": "https://cryptpad.private.coffee" } ], + "icon": "article-ny-times", "exclude_from_index": false, "exclude_from_simple": false }, @@ -42,6 +44,7 @@ "url": "https://piped.private.coffee" } ], + "icon": "video", "exclude_from_index": false, "exclude_from_simple": false }, @@ -57,6 +60,7 @@ "url": "https://myip.coffee" } ], + "icon": "network", "exclude_from_index": false, "exclude_from_simple": false }, @@ -72,6 +76,7 @@ "url": "https://invidious.private.coffee" } ], + "icon": "video", "exclude_from_index": false, "exclude_from_simple": false }, @@ -91,6 +96,7 @@ "url": "https://skrt.social" } ], + "icon": "mastodon-logo", "exclude_from_index": false, "exclude_from_simple": false }, @@ -106,6 +112,7 @@ "url": "https://git.private.coffee" } ], + "icon": "git-branch", "exclude_from_index": false, "exclude_from_simple": false }, @@ -121,6 +128,7 @@ "url": "https://bbb.private.coffee" } ], + "icon": "webcam", "exclude_from_index": false, "exclude_from_simple": false }, @@ -136,6 +144,7 @@ "url": "https://hedgedoc.private.coffee" } ], + "icon": "markdown-logo", "exclude_from_index": false, "exclude_from_simple": false }, @@ -151,6 +160,7 @@ "url": "https://gothub.private.coffee" } ], + "icon": "git-branch", "exclude_from_index": false, "exclude_from_simple": false }, @@ -166,6 +176,7 @@ "url": "https://redlib.private.coffee" } ], + "icon": "chats", "exclude_from_index": false, "exclude_from_simple": false }, @@ -181,6 +192,7 @@ "url": "https://alltube.private.coffee" } ], + "icon": "video", "exclude_from_index": false, "exclude_from_simple": false }, @@ -196,6 +208,7 @@ "url": "https://structables.private.coffee" } ], + "icon": "lightbulb", "exclude_from_index": false, "exclude_from_simple": false }, @@ -211,6 +224,7 @@ "url": "https://nocodb.private.coffee" } ], + "icon": "database", "exclude_from_index": true, "exclude_from_simple": true }, @@ -226,6 +240,7 @@ "url": "https://penpot.private.coffee" } ], + "icon": "pencil-ruler", "exclude_from_index": false, "exclude_from_simple": false }, @@ -241,6 +256,7 @@ "url": "https://pcof.fi" } ], + "icon": "link", "exclude_from_index": false, "exclude_from_simple": false }, @@ -256,6 +272,7 @@ "url": "https://rallly.private.coffee" } ], + "icon": "calendar", "exclude_from_index": true, "exclude_from_simple": true }, @@ -271,6 +288,7 @@ "url": "https://librey.private.coffee" } ], + "icon": "magnifying-glass", "exclude_from_index": false, "exclude_from_simple": false }, @@ -286,6 +304,7 @@ "url": "https://overleaf.private.coffee" } ], + "icon": "leaf", "exclude_from_index": false, "exclude_from_simple": false }, @@ -301,44 +320,9 @@ "url": "https://binternet.private.coffee" } ], + "icon": "images", "exclude_from_index": false, "exclude_from_simple": false - }, - { - "name": "Nitter", - "url": "https://nitter.private.coffee", - "short_description": "Nitter is a privacy-friendly alternative front-end to Twitter.", - "long_description": "Nitter is a privacy-friendly alternative front-end to Twitter. It allows you to browse Twitter without being tracked.", - "status": "NOK", - "links": [ - { - "name": "Go to Nitter", - "url": "https://nitter.private.coffee", - "alternatives": [ - { - "name": "Tor (.onion)", - "url": "http://nitter.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion/" - } - ] - } - ], - "exclude_from_index": true, - "exclude_from_simple": true - }, - { - "name": "Proxigram", - "url": "https://proxigram.private.coffee", - "short_description": "Proxigram is a privacy-friendly alternative front-end to Instagram.", - "long_description": "Proxigram is a privacy-friendly alternative front-end to Instagram. It allows you to browse Instagram without being tracked.", - "status": "NOK", - "links": [ - { - "name": "Go to Proxigram", - "url": "https://proxigram.private.coffee" - } - ], - "exclude_from_index": true, - "exclude_from_simple": true } ] } \ No newline at end of file diff --git a/helpers/finances.py b/helpers/finances.py new file mode 100644 index 0000000..e4a0b32 --- /dev/null +++ b/helpers/finances.py @@ -0,0 +1,201 @@ +from decimal import Decimal + + +def get_latest_month(data): + years = sorted(data.keys()) + latest_year = years[-1] + months = sorted(data[latest_year].keys()) + latest_month = months[-1] + + return int(latest_month), int(latest_year) + +def get_transparency_data(data, year=None, month=None): + if year is None: + year = max(data.keys()) + + if month is None: + month = max(data[year].keys()) + + assert year in data, f"Year {year} not found in data" + assert month in data[year], f"Month {month}-{year} not found in data" + + # Initialize balances + balances = {} + incomes = {} + expenses = {} + + start_balance = {} + end_balance = {} + + for y in sorted(data.keys()): + if int(y) > int(year): + break + + for m in sorted(data[y].keys()): + if int(y) == int(year) and int(m) > int(month): + break + + # If the month is the one we are interested in, capture the start balance + if int(y) == int(year) and int(m) == int(month): + start_balance = {k: Decimal(v) for k, v in balances.items()} + + for category in data[y][m]: + for currency, amount in data[y][m][category].items(): + if currency not in balances: + balances[currency] = Decimal(0) + balances[currency] += Decimal(str(amount)) + + # Track incomes and expenses + if int(y) == int(year) and int(m) == int(month): + if Decimal(str(amount)) > 0: + if category not in incomes: + incomes[category] = {} + if currency not in incomes[category]: + incomes[category][currency] = Decimal(0) + incomes[category][currency] += Decimal(str(amount)) + else: + if category not in expenses: + expenses[category] = {} + if currency not in expenses[category]: + expenses[category][currency] = Decimal(0) + expenses[category][currency] += Decimal(str(amount)) + + # If the month is the one we are interested in, capture the end balance + if int(y) == int(year) and int(m) == int(month): + end_balance = {k: Decimal(v) for k, v in balances.items()} + + # Calculate accumulated sums of incomes and expenses + accumulated_incomes = { + currency: sum(incomes[cat].get(currency, Decimal(0)) for cat in incomes) + for currency in balances + } + accumulated_expenses = { + currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses) + for currency in balances + } + + return { + "start_balance": start_balance, + "end_balance": end_balance, + "incomes": incomes, + "expenses": expenses, + "accumulated_incomes": accumulated_incomes, + "accumulated_expenses": accumulated_expenses, + } + + +def generate_transparency_table(result, currencies=None): + def extract_currencies(data): + return ["EUR"] + ( + list( + set( + list(data["start_balance"].keys()) + + list(data["end_balance"].keys()) + + list(data["accumulated_incomes"].keys()) + + list(data["accumulated_expenses"].keys()) + ) + - {"EUR"} + ) + ) + + def format_currency(value, currency): + if currency == "EUR": + return f"€{value:,.2f}" + elif currency in ["BTC", "ETH", "XMR"]: + return f"{value:,.9f} {currency}" + else: + return f"{value} {currency}" + + def format_value(value, currency): + if value == 0: + return f"{format_currency(value, currency)}" + elif value > 0: + return f"+ {format_currency(value, currency)}" + else: + return f"- {format_currency(abs(value), currency)}" + + html = """ +
Category | + """ + + if currencies is None: + currencies = extract_currencies(result) + + # Add currency headers + for currency in currencies: + if currency == "EUR": + html += 'Euros (€) | ' + elif currency == "BTC": + html += 'Bitcoin (BTC) | ' + elif currency == "ETH": + html += 'Ethereum (ETH) | ' + elif currency == "XMR": + html += 'Monero (XMR) | ' + else: + html += f'{currency} | ' + + html += """ +
---|---|---|---|---|---|
Account Balance (start of month) | " + for currency in currencies: + value = result["start_balance"].get(currency, Decimal(0)) + html += f"{format_value(value, currency)} | " + html += "||||
{category} | " + for currency in currencies: + value = transactions.get(currency, "") + if value != "": + html += f"{format_value(value, currency)} | " + else: + html += "" + html += " | |||
{category} | " + for currency in currencies: + value = transactions.get(currency, "") + if value != "": + html += f"{format_value(value, currency)} | " + else: + html += "" + html += " | |||
Total Income | ' + for currency in currencies: + value = result["accumulated_incomes"].get(currency, Decimal(0)) + html += f"{format_value(value, currency)} | " + html += "||||
Total Expenses | ' + for currency in currencies: + value = result["accumulated_expenses"].get(currency, Decimal(0)) + html += f"{format_value(value, currency)} | " + html += "||||
Account Balance (end of month) | ' + for currency in currencies: + value = result["end_balance"].get(currency, Decimal(0)) + html += f"{format_value(value, currency)} | " + html += "