diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..eb2d0bb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger", + "type": "debugpy", + "request": "launch", + "program": "main.py", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}", + "env": { + "PRIVATECOFFEE_DEV": "1" + } + } + ] +} \ No newline at end of file diff --git a/assets/css/base.css b/assets/css/base.css index 5cc9970..4b50faa 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -136,7 +136,11 @@ h5 { } /* Responsive Styles */ -@media (max-width: 768px) { +@media (max-width: 991px) { + p.text-center.special-header { + font-size: 3rem; + } + .navbar .container { display: flex; flex-direction: column; @@ -177,29 +181,85 @@ h5 { padding: 0 1rem; } - .btn.btn-primary { - margin: 1rem auto; - display: block; - width: 80%; - } - - h2 .special-header { - font-size: 6rem !important; - } - - .special-header { - font-size: 2rem; - } - .navbar-btn { margin: 0 auto !important; } - .that-br { - display: none; - } + @media (max-width: 768px) { + .btn.btn-primary { + margin: 1rem auto; + display: block; + width: 80%; + } - .slogan { - display: none; + h2 .special-header { + font-size: 6rem !important; + } + + .special-header { + font-size: 2rem; + } + + .that-br { + display: none; + } + + .slogan { + display: none; + } } +} + +/* Dropdown Styles */ + +.dropdown { + position: relative; + display: inline-block; + width: 100%; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 100%; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; + text-align: left; +} + +.dropdown:hover .dropdown-content { + display: block; +} + +.dropdown-toggle-area { + display: inline-block; + cursor: pointer; + color: white; +} + +.btn-primary:has(.dropdown-toggle-area) { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.btn-primary .main-link { + flex-grow: 1; + text-align: center; + color: white; + text-decoration: none; +} + +.btn-primary .main-link:hover { + text-decoration: none; + color: black; } \ No newline at end of file diff --git a/data/finances.json b/data/finances.json index b047d7a..b03b816 100644 --- a/data/finances.json +++ b/data/finances.json @@ -4,15 +4,14 @@ "Membership Fees": { "EUR": 365 }, - "Donations": {}, "Server Costs": { "EUR": -216.57 }, "Domain Names": {}, - "Operating Expenses": { - "EUR": -36.10 - }, - "Conversions": {} + "Administrative Expenses": { + "EUR": -36.10, + "Notes": "Administrative fee for the formation of the association" + } }, "5": { "Membership Fees": { @@ -23,11 +22,20 @@ "XMR": 0.447661805527 }, "Server Costs": { - "EUR": -430.04 + "EUR": -430.04, + "Notes": "Includes setup costs and two monthly payments for new server" + } + }, + "6": { + "Membership Fees": { + "EUR": 382.42 }, - "Domain Names": {}, - "Operating Expenses": {}, - "Conversions": {} + "Server Costs": { + "EUR": -317.62 + }, + "Bank Fees": { + "EUR": -49.05 + } } } } \ No newline at end of file diff --git a/data/services.json b/data/services.json index 39e75d1..9250568 100644 --- a/data/services.json +++ b/data/services.json @@ -4,12 +4,18 @@ "name": "Matrix", "url": "https://element.private.coffee", "short_description": "Matrix is an open network for secure, decentralized communication.", - "long_description": "Private.coffee runs a Matrix server. You can use it to chat with other people at Private.coffee or around the world. Use it with a client of your choice, with https://matrix.private.coffee as the homeserver, or use our web client.", + "long_description": "Private.coffee runs a Matrix server. You can use it to chat with other people at Private.coffee or around the world. Use it with a client of your choice, or use our Element web client.", "status": "OK", "links": [ { - "name": "Go to Element (Web client)", - "url": "https://element.private.coffee" + "name": "Go to Element", + "url": "https://element.private.coffee", + "alternatives": [ + { + "name": "Tor", + "url": "http://element.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion" + } + ] } ], "icon": "matrix-logo", @@ -33,15 +39,25 @@ "exclude_from_simple": false }, { - "name": "Piped", + "name": "Piped / Invidious", "url": "https://piped.private.coffee", "short_description": "Watch YouTube videos without Google tracking.", - "long_description": "Piped is another alternative front-end to YouTube. It allows you to watch YouTube videos without Google tracking you.", + "long_description": "Piped and Invidious are alternative front-ends to YouTube. They allow you to watch YouTube videos without Google tracking you.", "status": "OK", "links": [ { "name": "Go to Piped", "url": "https://piped.private.coffee" + }, + { + "name": "Go to Invidious", + "url": "https://invidious.private.coffee", + "alternatives": [ + { + "name": "Tor", + "url": "http://invidious.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion" + } + ] } ], "icon": "video", @@ -80,22 +96,6 @@ "exclude_from_index": false, "exclude_from_simple": false }, - { - "name": "Invidious", - "url": "https://invidious.private.coffee", - "short_description": "Watch YouTube videos without Google tracking.", - "long_description": "Invidious is an alternative front-end to YouTube. It allows you to watch YouTube videos without Google tracking you.", - "status": "OK", - "links": [ - { - "name": "Go to Invidious", - "url": "https://invidious.private.coffee" - } - ], - "icon": "video", - "exclude_from_index": false, - "exclude_from_simple": false - }, { "name": "Mastodon", "url": "https://cuddly.space", diff --git a/helpers/finances.py b/helpers/finances.py index e4a0b32..001ab35 100644 --- a/helpers/finances.py +++ b/helpers/finances.py @@ -1,14 +1,27 @@ from decimal import Decimal +from datetime import datetime -def get_latest_month(data): +def get_latest_month(data, allow_current=False): years = sorted(data.keys()) latest_year = years[-1] months = sorted(data[latest_year].keys()) latest_month = months[-1] + if ( + not allow_current + and latest_year == str(datetime.now().year) + and latest_month == str(datetime.now().month) + ): + try: + latest_month = months[-2] + except IndexError: + latest_year = years[-2] + 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()) @@ -23,6 +36,7 @@ def get_transparency_data(data, year=None, month=None): balances = {} incomes = {} expenses = {} + notes = {} start_balance = {} end_balance = {} @@ -37,41 +51,51 @@ def get_transparency_data(data, year=None, month=None): # 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()} + start_balance = { + k: Decimal(v) for k, v in balances.items() if k != "Notes" + } 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)) + if currency == "Notes": + if int(y) == int(year) and int(m) == int(month): + notes[category] = amount + else: + 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)) + # 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()} + end_balance = { + k: Decimal(v) for k, v in balances.items() if k != "Notes" + } # 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 + if currency != "Notes" } accumulated_expenses = { currency: sum(expenses[cat].get(currency, Decimal(0)) for cat in expenses) for currency in balances + if currency != "Notes" } return { @@ -81,6 +105,7 @@ def get_transparency_data(data, year=None, month=None): "expenses": expenses, "accumulated_incomes": accumulated_incomes, "accumulated_expenses": accumulated_expenses, + "notes": notes, } @@ -94,7 +119,7 @@ def generate_transparency_table(result, currencies=None): + list(data["accumulated_incomes"].keys()) + list(data["accumulated_expenses"].keys()) ) - - {"EUR"} + - {"EUR", "Notes"} ) ) @@ -152,7 +177,8 @@ def generate_transparency_table(result, currencies=None): # Add income rows for category, transactions in result["incomes"].items(): - html += f"{category}" + has_notes = result["notes"].get(category) + html += f"{category}{'*' if has_notes else ''}" for currency in currencies: value = transactions.get(currency, "") if value != "": @@ -163,7 +189,8 @@ def generate_transparency_table(result, currencies=None): # Add expense rows for category, transactions in result["expenses"].items(): - html += f"{category}" + has_notes = result["notes"].get(category) + html += f"{category}{'*' if has_notes else ''}" for currency in currencies: value = transactions.get(currency, "") if value != "": @@ -198,4 +225,11 @@ def generate_transparency_table(result, currencies=None): """ + if result["notes"]: + html += "

Notes:

" + html += "" + return html diff --git a/main.py b/main.py index 6c9a279..3df6e1c 100644 --- a/main.py +++ b/main.py @@ -54,7 +54,9 @@ def catch_all(path): (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() ) - finances_month, finances_year = get_latest_month(finances) + allow_current = app.development_mode + + finances_month, finances_year = get_latest_month(finances, allow_current) finances_period = datetime.date(finances_year, finances_month, 1) finances_period_str = finances_period.strftime("%B %Y") @@ -69,12 +71,53 @@ def catch_all(path): } ) + if path == "transparency": + finances = json.loads( + (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() + ) + + finance_data = {} + + for year in sorted(finances.keys(), reverse=True): + for month in sorted(finances[year].keys(), reverse=True): + if year not in finance_data: + finance_data[year] = {} + print(get_transparency_data(finances, year, month)) + finance_data[year][month] = generate_transparency_table( + get_transparency_data(finances, year, month) + ) + + kwargs.update( + { + "finances": finance_data, + } + ) + return render_template(f"{path}.html", **kwargs) except TemplateNotFound: return "404 Not Found", 404 +@app.route("/_metrics/") +def metrics(): + finances = json.loads( + (pathlib.Path(__file__).parent / "data" / "finances.json").read_text() + ) + + balances = get_transparency_data(finances)["end_balance"] + + response = ( + "# HELP privatecoffee_balance The balance of the private.coffee account\n" + ) + response += "# TYPE privatecoffee_balance gauge\n" + + for currency, balance in balances.items(): + response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n' + + return response + + app.development_mode = False if os.environ.get("PRIVATECOFFEE_DEV"): diff --git a/templates/base.html b/templates/base.html index 902ba11..2b6ce88 100644 --- a/templates/base.html +++ b/templates/base.html @@ -17,7 +17,7 @@