From 5980a61a7e54a40324f04ab114f4c4555e2eb17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 14 Jul 2026 13:18:35 +0200 Subject: [PATCH] soma improvements --- app/__init__.py | 6 +++--- app/config.py | 2 +- app/database.py | 3 --- app/database_init.py | 2 -- app/database_migrations.py | 3 --- app/services.py | 1 - app/static/js/ajax.js | 2 +- app/templates/orlen.html | 2 +- app/templates/stations.html | 31 ++++++++++++++++++++++++++++++- app/vat.py | 1 - dev.sh | 36 ++++++++++++++++++++++++++---------- 11 files changed, 62 insertions(+), 27 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index e9c6652..093a92d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -53,11 +53,11 @@ def create_app(test_config=None): if request.endpoint == "static": filename = request.view_args.get("filename", "") if request.view_args else "" if filename.endswith(".css"): - response.headers["Cache-Control"] = "public, max-age=2592000, immutable" + response.headers["Cache-Control"] = "public, max-age=1800, immutable" else: - response.headers["Cache-Control"] = "public, max-age=2592000, immutable" + response.headers["Cache-Control"] = "public, max-age=1800, immutable" elif response.mimetype == "text/html": - response.headers["Cache-Control"] = "no-store, private, must-revalidate" + response.headers["Cache-Control"] = "private, no-store" return response @login_manager.user_loader diff --git a/app/config.py b/app/config.py index 40ea272..545f59f 100644 --- a/app/config.py +++ b/app/config.py @@ -35,7 +35,7 @@ class Config: SECRET_KEY = env("SECRET_KEY", "dev-secret") SQLALCHEMY_DATABASE_URI = env("DATABASE_URL", "sqlite:////app/instance/fueltrack.db") SQLALCHEMY_TRACK_MODIFICATIONS = False - API_TOKEN_MAX_AGE_SECONDS = env_int("API_TOKEN_MAX_AGE_SECONDS", 28800) + API_TOKEN_MAX_AGE_SECONDS = env_int("API_TOKEN_MAX_AGE_SECONDS", 928800) HTTP_TIMEOUT_SECONDS = env_float("HTTP_TIMEOUT_SECONDS", 30) ORLEN_TIMEOUT_SECONDS = env_float("ORLEN_TIMEOUT_SECONDS", 15) diff --git a/app/database.py b/app/database.py index b2b5301..fdba4b5 100644 --- a/app/database.py +++ b/app/database.py @@ -1,11 +1,8 @@ from __future__ import annotations - import fcntl from contextlib import contextmanager from pathlib import Path - from flask import Flask - from .database_init import initialize_database from .database_migrations import migrate_database diff --git a/app/database_init.py b/app/database_init.py index c507dfa..d017c19 100644 --- a/app/database_init.py +++ b/app/database_init.py @@ -1,7 +1,5 @@ from __future__ import annotations - from sqlalchemy import inspect - from .extensions import db from .models import AppSetting, CompanySettings, User diff --git a/app/database_migrations.py b/app/database_migrations.py index 30e3a5b..e1f32cd 100644 --- a/app/database_migrations.py +++ b/app/database_migrations.py @@ -1,10 +1,7 @@ from __future__ import annotations - from collections.abc import Callable from dataclasses import dataclass - from sqlalchemy import inspect, text - from .extensions import db diff --git a/app/services.py b/app/services.py index d40a6dd..4adfcbe 100644 --- a/app/services.py +++ b/app/services.py @@ -1,4 +1,3 @@ -"""Compatibility facade. New code lives in domain/ and integrations/.""" from .domain.costs import calculate_costs, money from .domain.invoices import invoice_period from .integrations.orlen import fetch_orlen_price, fetch_orlen_range, POLISH_REGIONS, PRODUCT_IDS diff --git a/app/static/js/ajax.js b/app/static/js/ajax.js index 5398b2e..563cfa6 100644 --- a/app/static/js/ajax.js +++ b/app/static/js/ajax.js @@ -1 +1 @@ -(()=>{const F=FuelTrack;F.replaceMain=async(url=location.href,{push=false}={})=>{const r=await fetch(url,{headers:{'X-Requested-With':'XMLHttpRequest'}});if(!r.ok)throw new Error('Nie udało się odświeżyć widoku.');const doc=new DOMParser().parseFromString(await r.text(),'text/html');const incoming=F.qs('main',doc),current=F.qs('main');if(!incoming||!current)return;current.replaceWith(incoming);document.documentElement.dataset.bsTheme=doc.documentElement.dataset.bsTheme||'light';const next=F.qs('#bootstrap-theme',doc),theme=F.qs('#bootstrap-theme');if(next&&theme)theme.href=next.href;if(push)history.pushState({},'',url);F.initPage();F.qsa('script:not([src])',doc).forEach(x=>{if(x.textContent.includes('FuelTrack.'))try{Function(x.textContent)()}catch(e){console.error(e)}})};F.submitAjax=async form=>{const b=F.qs('[type=submit]',form),old=b?.innerHTML;if(b){b.disabled=true;b.innerHTML='Zapisywanie…'}try{const r=await fetch(form.action||location.href,{method:(form.dataset.method||form.getAttribute('method')||'POST').toUpperCase(),body:new FormData(form),headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}});const d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error||d.message||'Operacja nie powiodła się.');F.notify(d.message||'Zapisano');bootstrap.Modal.getInstance(form.closest('.modal'))?.hide();await F.replaceMain((d.data||{}).redirect||location.href,{push:Boolean((d.data||{}).redirect)})}catch(e){F.notify(e.message,'danger')}finally{if(b){b.disabled=false;b.innerHTML=old}}};F.bindAjaxForms=()=>{F.qsa('form.ajax-form').forEach(f=>{if(f.dataset.bound)return;f.dataset.bound='1';f.addEventListener('submit',e=>{e.preventDefault();F.submitAjax(f)})});F.qsa('form.ajax-nav-form').forEach(f=>{if(f.dataset.bound)return;f.dataset.bound='1';f.addEventListener('submit',e=>{e.preventDefault();F.replaceMain(`${f.action||location.pathname}?${new URLSearchParams(new FormData(f))}`,{push:true}).catch(x=>F.notify(x.message,'danger'))})});F.qsa('a.ajax-nav-link').forEach(a=>{if(a.dataset.bound)return;a.dataset.bound='1';a.addEventListener('click',e=>{e.preventDefault();F.replaceMain(a.href,{push:true}).catch(x=>F.notify(x.message,'danger'))})})}})(); +(()=>{const F=FuelTrack;F.replaceMain=async(url=location.href,{push=false}={})=>{const r=await fetch(url,{headers:{'X-Requested-With':'XMLHttpRequest'}});if(!r.ok)throw new Error('Nie udało się odświeżyć widoku.');const doc=new DOMParser().parseFromString(await r.text(),'text/html');const incoming=F.qs('main',doc),current=F.qs('main');if(!incoming||!current)return;current.replaceWith(incoming);document.documentElement.dataset.bsTheme=doc.documentElement.dataset.bsTheme||'light';const next=F.qs('#bootstrap-theme',doc),theme=F.qs('#bootstrap-theme');if(next&&theme)theme.href=next.href;if(push)history.pushState({},'',url);F.initPage();F.qsa('script:not([src])',doc).forEach(x=>{if(x.textContent.includes('FuelTrack.'))try{Function(x.textContent)()}catch(e){console.error(e)}})};F.submitAjax=async form=>{const b=F.qs('[type=submit]',form),old=b?.innerHTML;if(b){b.disabled=true;b.innerHTML='Zapisywanie…'}try{const r=await fetch(form.action||location.href,{method:(form.dataset.method||form.getAttribute('method')||'POST').toUpperCase(),body:new FormData(form),headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json'}});const d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error||d.message||'Operacja nie powiodła się.');F.notify(d.message||'Zapisano');bootstrap.Modal.getInstance(form.closest('.modal'))?.hide();await F.replaceMain((d.data||{}).redirect||location.href,{push:Boolean((d.data||{}).redirect)})}catch(e){F.notify(e.message,'danger')}finally{if(b){b.disabled=false;b.innerHTML=old}}};F.bindAjaxForms=()=>{F.qsa('form.ajax-form').forEach(f=>{if(f.dataset.bound)return;f.dataset.bound='1';f.addEventListener('submit',e=>{e.preventDefault();F.submitAjax(f)})});F.qsa('form.ajax-nav-form').forEach(f=>{if(f.dataset.bound)return;f.dataset.bound='1';f.addEventListener('submit',e=>{e.preventDefault();(()=>{const url=new URL(f.getAttribute('action')||location.pathname,location.origin);url.search=new URLSearchParams(new FormData(f)).toString();return F.replaceMain(url.toString(),{push:true})})().catch(x=>F.notify(x.message,'danger'))})});F.qsa('a.ajax-nav-link').forEach(a=>{if(a.dataset.bound)return;a.dataset.bound='1';a.addEventListener('click',e=>{e.preventDefault();F.replaceMain(a.href,{push:true}).catch(x=>F.notify(x.message,'danger'))})})}})(); diff --git a/app/templates/orlen.html b/app/templates/orlen.html index 2571a8c..80fb6de 100644 --- a/app/templates/orlen.html +++ b/app/templates/orlen.html @@ -47,6 +47,6 @@

{% if mode == 'compare' %}Porównanie: {{compare_fuel}}{% else %}Trend ceny{% endif %} {% if include_vat %}brutto{% else %}netto{% endif %}

Cena za litr w złotych.

Brak danych dla wybranych filtrów.
-
{% for r in rows|reverse %}{% else %}{% endfor %}
DataPaliwoCena netto zł/lRegionPobrano
{{r.effective_date}}{{r.product_name}}{{'%.4f'|format(r.price_per_liter|float)}}{{r.region or '—'}}{{r.fetched_at.strftime('%Y-%m-%d %H:%M')}}
Brak danych w bazie.
+{% if mode == 'history' %}
{% for r in rows|reverse %}{% else %}{% endfor %}
DataPaliwoCena netto zł/lRegionPobrano
{{r.effective_date}}{{r.product_name}}{{'%.4f'|format(r.price_per_liter|float)}}{{r.region or '—'}}{{r.fetched_at.strftime('%Y-%m-%d %H:%M')}}
Brak danych w bazie.
{% endif %} {% endblock %} {% block scripts %}{% endblock %} diff --git a/app/templates/stations.html b/app/templates/stations.html index e80970e..270b66b 100644 --- a/app/templates/stations.html +++ b/app/templates/stations.html @@ -16,10 +16,39 @@ {{s.station_count}}
{% if s.has_petrol %}benzyna{% endif %}{% if s.has_diesel %}diesel{% endif %}{% if s.has_lpg %}LPG{% endif %}
-{% for card in fuel_cards %}{% set rule=card_rules.get((card.id,s.id)) %}
{{card.name}}
{% else %}Najpierw dodaj kartę w ustawieniach firmy.{% endfor %} +{% if fuel_cards %}
{{ fuel_cards|length }} {% if fuel_cards|length == 1 %}karta{% elif fuel_cards|length in [2,3,4] %}karty{% else %}kart{% endif %}
{% else %}Brak kart{% endif %} {% else %}Brak firm. Pobierz katalog z URE.{% endfor %} {% if station_page.pages > 1 %}{% endif %} +{% for s in station_page.items %}{% if fuel_cards %} + +{% endif %}{% endfor %} {% endblock %} diff --git a/app/vat.py b/app/vat.py index 9bd518a..efbd5e8 100644 --- a/app/vat.py +++ b/app/vat.py @@ -1,6 +1,5 @@ from datetime import date, datetime from decimal import Decimal, InvalidOperation - from .extensions import db from .models import AppSetting diff --git a/dev.sh b/dev.sh index 2569120..9d72ba0 100755 --- a/dev.sh +++ b/dev.sh @@ -1,31 +1,47 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$ROOT_DIR" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="${APP_DIR:-$SCRIPT_DIR}" +INSTANCE_DIR="${INSTANCE_DIR:-$APP_DIR/instance}" +ENV_FILE="${ENV_FILE:-$APP_DIR/.env}" +DB_FILE="${DB_FILE:-$INSTANCE_DIR/fueltrack.db}" -if [[ -f .env ]]; then +# Uruchamiaj aplikację zawsze z jej właściwego katalogu. +cd "$APP_DIR" + +# Wczytaj .env niezależnie od katalogu wywołania skryptu. +if [[ -f "$ENV_FILE" ]]; then set -a - # shellcheck disable=SC1091 - source .env + # shellcheck disable=SC1090 + source "$ENV_FILE" set +a fi -mkdir -p instance +mkdir -p "$INSTANCE_DIR" +mkdir -p "$(dirname -- "$DB_FILE")" export FLASK_ENV="${FLASK_ENV:-development}" -export DATABASE_URL="${DATABASE_URL:-sqlite:///$ROOT_DIR/instance/fueltrack.db}" export APP_HOST="${APP_HOST:-0.0.0.0}" export APP_PORT="${APP_PORT:-8000}" export GUNICORN_WORKERS="${GUNICORN_WORKERS:-1}" export GUNICORN_THREADS="${GUNICORN_THREADS:-8}" export GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-0}" +export GUNICORN_APP="${GUNICORN_APP:-wsgi:app}" + +# Jeśli DATABASE_URL nie jest ustawione w .env, użyj bezwzględnej +# ścieżki do pliku SQLite. +if [[ -z "${DATABASE_URL:-}" ]]; then + export DATABASE_URL="sqlite:///$DB_FILE" +fi echo "FuelTrack DEV" -echo "Adres: http://localhost:${APP_PORT}" -echo "Baza: ${DATABASE_URL}" +echo "Katalog aplikacji: $APP_DIR" +echo "Adres: http://localhost:${APP_PORT}" +echo "Baza: $DATABASE_URL" exec gunicorn \ + --chdir "$APP_DIR" \ --reload \ --access-logfile - \ --error-logfile - \ @@ -34,4 +50,4 @@ exec gunicorn \ --threads "$GUNICORN_THREADS" \ --timeout "$GUNICORN_TIMEOUT" \ --bind "$APP_HOST:$APP_PORT" \ - wsgi:app + "$GUNICORN_APP"