diff --git a/app.py b/app.py index 01d37ff..b6ff207 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,11 @@ from shopping_app import app, socketio, APP_PORT, DEBUG_MODE from shopping_app.app_setup import logging +from shopping_app.startup_info import print_startup_info + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG if DEBUG_MODE else logging.INFO) + + print_startup_info(app) + socketio.run(app, host="0.0.0.0", port=APP_PORT, debug=False) diff --git a/shopping_app.zip b/shopping_app.zip new file mode 100644 index 0000000..ee69d35 Binary files /dev/null and b/shopping_app.zip differ diff --git a/shopping_app/app_setup.py b/shopping_app/app_setup.py index b141970..a267e78 100644 --- a/shopping_app/app_setup.py +++ b/shopping_app/app_setup.py @@ -94,6 +94,18 @@ def read_commit(filename="version.txt", root_path=None): except Exception: return None + +def get_file_md5(path): + try: + digest = hashlib.md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + digest.update(chunk) + return digest.hexdigest()[:12] + except Exception: + return "dev" + + commit = read_commit("version.txt", root_path=os.path.dirname(os.path.dirname(__file__))) or "dev" APP_VERSION = commit app.config["APP_VERSION"] = APP_VERSION diff --git a/shopping_app/startup_info.py b/shopping_app/startup_info.py new file mode 100644 index 0000000..44e02a4 --- /dev/null +++ b/shopping_app/startup_info.py @@ -0,0 +1,95 @@ +import os +import sys +import platform +import socket +from datetime import datetime + +import psutil + +try: + from sqlalchemy import text +except Exception: + text = None + +def mb(x): + return int(x / 1024 / 1024) + + +def get_db_type(app): + uri = app.config.get("SQLALCHEMY_DATABASE_URI") or app.config.get("DATABASE_URL", "") + + if not uri: + return "NONE" + + if uri.startswith("sqlite"): + return "SQLite" + if uri.startswith("mysql"): + return "MySQL" + if uri.startswith("postgresql"): + return "PostgreSQL" + + return "OTHER" + +def print_startup_info(app): + host = os.getenv("HOST", "127.0.0.1") + port = int(os.getenv("PORT", "8000")) + + rules = list(app.url_map.iter_rules()) + + cpu = psutil.cpu_percent(interval=0.2) + ram = psutil.virtual_memory() + proc = psutil.Process(os.getpid()) + + db_type = get_db_type(app) + + print("\n" + "="*52) + print(" APP START") + print("="*52) + + # SYSTEM + print("\n[ SYSTEM ]") + print(f"Time : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"OS : {platform.system()} {platform.release()} ({platform.machine()})") + print(f"Python : {sys.version.split()[0]}") + print(f"Host : {socket.gethostname()}") + + # SERVER + print("\n[ SERVER ]") + print(f"Bind : {host}:{port}") + print(f"URL : http://127.0.0.1:{port}") + + # APP + print("\n[ APP ]") + print(f"Name : {app.name}") + print(f"Mode : {'DEV' if app.debug else 'PROD'}") + print(f"Debug : {app.debug}") + + # RESOURCES + print("\n[ RESOURCES ]") + print(f"CPU : {cpu:>5.1f}%") + print(f"RAM : {ram.percent:>5.1f}% ({mb(ram.used)} / {mb(ram.total)} MB)") + print(f"PROC : {mb(proc.memory_info().rss)} MB") + + # DATABASE + print("\n[ DATABASE ]") + print(f"Type : {db_type}") + + # SECURITY + print("\n[ SECURITY ]") + print(f"Secret : {'OK' if app.config.get('SECRET_KEY') else 'MISSING'}") + print(f"Talis : {'OFF' if app.config.get('TALISMAN_DISABLED') else 'ON'}") + + # HEALTH + print("\n[ HEALTH ]") + print(f"Uploads: {'OK' if os.path.exists('uploads') else 'MISS'}") + print(f"Static : {'OK' if os.path.exists(app.static_folder) else 'MISS'}") + + # ROUTES + print("\n[ ROUTES ]") + print(f"Total : {len(rules)}") + + # STATUS + print("\n[ STATUS ]") + print("READY") + + print("="*52 + "\n") \ No newline at end of file diff --git a/shopping_app/static/css/style.css b/shopping_app/static/css/style.css index a2eeeee..78a32b9 100644 --- a/shopping_app/static/css/style.css +++ b/shopping_app/static/css/style.css @@ -5259,3 +5259,93 @@ body:not(.sorting-active) .drag-handle { min-width: 44px !important; } } + + +/* wyróżnienie pola dodawania produktu */ +.endpoint-list .shopping-entry-card, +.endpoint-list_share .shopping-entry-card, +.endpoint-shared_list .shopping-entry-card, +.endpoint-view_list .shopping-entry-card { + background: linear-gradient(180deg, rgba(25, 135, 84, 0.16), rgba(13, 17, 23, 0.92)); + border: 1px solid rgba(25, 135, 84, 0.42); + border-radius: 1rem; + padding: .9rem; + box-shadow: 0 .5rem 1.2rem rgba(0, 0, 0, 0.18); +} + +.endpoint-list .shopping-entry-card__label, +.endpoint-list_share .shopping-entry-card__label, +.endpoint-shared_list .shopping-entry-card__label, +.endpoint-view_list .shopping-entry-card__label { + display: inline-flex; + align-items: center; + gap: .4rem; + margin-bottom: .2rem; + font-size: .95rem; + font-weight: 700; + color: #d1f7df; +} + +.endpoint-list .shopping-entry-card__hint, +.endpoint-list_share .shopping-entry-card__hint, +.endpoint-shared_list .shopping-entry-card__hint, +.endpoint-view_list .shopping-entry-card__hint { + margin-bottom: .75rem; + color: rgba(255, 255, 255, 0.72); + font-size: .82rem; + line-height: 1.35; +} + +.endpoint-list .shopping-entry-card .shopping-product-input-group, +.endpoint-list_share .shopping-entry-card .shopping-product-input-group, +.endpoint-shared_list .shopping-entry-card .shopping-product-input-group, +.endpoint-view_list .shopping-entry-card .shopping-product-input-group { + margin-bottom: 0 !important; +} + +.endpoint-list .shopping-entry-card .shopping-product-input-group > .form-control, +.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .form-control, +.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .form-control, +.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-control { + border-color: rgba(25, 135, 84, 0.55) !important; + background: rgba(17, 24, 39, 0.95) !important; +} + +.endpoint-list .shopping-entry-card .shopping-product-input-group > .form-control::placeholder, +.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .form-control::placeholder, +.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .form-control::placeholder, +.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-control::placeholder { + color: rgba(255, 255, 255, 0.62); +} + +.endpoint-list .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus, +.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus, +.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus, +.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus { + box-shadow: inset 0 0 0 1px rgba(25, 135, 84, 0.25), 0 0 0 .2rem rgba(25, 135, 84, 0.18); +} + +@media (max-width: 767.98px) { + .endpoint-list .shopping-entry-card, + .endpoint-list_share .shopping-entry-card, + .endpoint-shared_list .shopping-entry-card, + .endpoint-view_list .shopping-entry-card { + padding: .8rem; + border-radius: .95rem; + } + + .endpoint-list .shopping-entry-card__label, + .endpoint-list_share .shopping-entry-card__label, + .endpoint-shared_list .shopping-entry-card__label, + .endpoint-view_list .shopping-entry-card__label { + font-size: .92rem; + } + + .endpoint-list .shopping-entry-card__hint, + .endpoint-list_share .shopping-entry-card__hint, + .endpoint-shared_list .shopping-entry-card__hint, + .endpoint-view_list .shopping-entry-card__hint { + font-size: .78rem; + margin-bottom: .65rem; + } +} diff --git a/shopping_app/static/js/live.js b/shopping_app/static/js/live.js index e134d5e..4538a12 100644 --- a/shopping_app/static/js/live.js +++ b/shopping_app/static/js/live.js @@ -13,7 +13,7 @@ function toggleEmptyPlaceholder() { const li = document.createElement('li'); li.id = 'empty-placeholder'; li.className = 'list-group-item bg-dark text-secondary text-center w-100'; - li.textContent = 'Brak produktów w tej liście.'; + li.textContent = 'Brak produktów w tej liście.'; list.appendChild(li); } else if (hasRealItems && placeholder) { placeholder.remove(); @@ -206,7 +206,7 @@ function setupList(listId, username) { const progressTitle = document.getElementById('progress-title'); if (progressTitle) { - progressTitle.textContent = `📊 Postęp listy — ${data.purchased_count}/${data.total_count} kupionych (${Math.round(data.percent)}%)`; + progressTitle.textContent = `Postęp listy — ${data.purchased_count}/${data.total_count} kupionych (${Math.round(data.percent)}%)`; } }); diff --git a/shopping_app/templates/admin/admin_panel.html b/shopping_app/templates/admin/admin_panel.html index f735f5f..441b36b 100644 --- a/shopping_app/templates/admin/admin_panel.html +++ b/shopping_app/templates/admin/admin_panel.html @@ -341,7 +341,7 @@ checkboxes.forEach(cb => cb.checked = this.checked); }); - + {% endblock %} {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/admin/edit_categories.html b/shopping_app/templates/admin/edit_categories.html index c9dfad7..5466d26 100644 --- a/shopping_app/templates/admin/edit_categories.html +++ b/shopping_app/templates/admin/edit_categories.html @@ -146,6 +146,6 @@ {% endblock %} {% block scripts %} - - + + {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/admin/edit_list.html b/shopping_app/templates/admin/edit_list.html index 7845906..ff8b1e8 100644 --- a/shopping_app/templates/admin/edit_list.html +++ b/shopping_app/templates/admin/edit_list.html @@ -303,5 +303,5 @@ {% endblock %} {% block scripts %} - + {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/admin/list_products.html b/shopping_app/templates/admin/list_products.html index b669026..89f2328 100644 --- a/shopping_app/templates/admin/list_products.html +++ b/shopping_app/templates/admin/list_products.html @@ -170,8 +170,8 @@ {% block scripts %} - - + + {% endblock %} {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/admin/lists_access.html b/shopping_app/templates/admin/lists_access.html index 3dd2198..c10326e 100644 --- a/shopping_app/templates/admin/lists_access.html +++ b/shopping_app/templates/admin/lists_access.html @@ -181,7 +181,7 @@ {% endblock %} {% block scripts %} - - + + {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/admin/receipts.html b/shopping_app/templates/admin/receipts.html index 1b4f5e2..a5ccd0a 100644 --- a/shopping_app/templates/admin/receipts.html +++ b/shopping_app/templates/admin/receipts.html @@ -224,8 +224,8 @@ endpoint: "/admin/crop_receipt" }; - - + + {% endblock %} {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/admin/settings.html b/shopping_app/templates/admin/settings.html index b9b7671..b9c7cba 100644 --- a/shopping_app/templates/admin/settings.html +++ b/shopping_app/templates/admin/settings.html @@ -153,6 +153,6 @@ {% endblock %} {% block scripts %} - - + + {% endblock %} diff --git a/shopping_app/templates/admin/user_management.html b/shopping_app/templates/admin/user_management.html index 4581550..8dc1265 100644 --- a/shopping_app/templates/admin/user_management.html +++ b/shopping_app/templates/admin/user_management.html @@ -121,7 +121,7 @@ {% block scripts %} - + {% endblock %} diff --git a/shopping_app/templates/base.html b/shopping_app/templates/base.html index 740e09a..5651049 100644 --- a/shopping_app/templates/base.html +++ b/shopping_app/templates/base.html @@ -6,25 +6,25 @@ {% block title %}Live Lista Zakupów{% endblock %} - - + + {% set exclude_paths = ['/system-auth'] %} {% if (exclude_paths | select("in", request.path) | list | length == 0) and has_authorized_cookie and not is_blocked %} - - + + {% endif %} {% set substrings_cropper = ['/admin/receipts', '/edit_my_list'] %} {% if substrings_cropper | select("in", request.path) | list | length > 0 %} - + {% endif %} {% set substrings_tomselect = ['/edit_my_list', '/admin/edit_list', '/admin/edit_categories'] %} {% if substrings_tomselect | select("in", request.path) | list | length > 0 %} - + {% endif %} @@ -123,7 +123,7 @@ - + {% if not is_blocked %} {% if request.endpoint != 'system_auth' %} - - - - - - + + + + + + {% endif %} - - + + + {% endif %} {% set substrings = ['/edit_my_list', '/admin/edit_list', '/admin/edit_categories'] %} {% if substrings | select("in", request.path) | list | length > 0 %} - + {% endif %} {% endif %} diff --git a/shopping_app/templates/edit_my_list.html b/shopping_app/templates/edit_my_list.html index 9131305..f54e766 100644 --- a/shopping_app/templates/edit_my_list.html +++ b/shopping_app/templates/edit_my_list.html @@ -260,9 +260,9 @@ endpoint: "/user_crop_receipt" }; - - - - - + + + + + {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/expenses.html b/shopping_app/templates/expenses.html index e02eb58..902df05 100644 --- a/shopping_app/templates/expenses.html +++ b/shopping_app/templates/expenses.html @@ -213,13 +213,13 @@ {% endblock %} {% block scripts %} - - - - - - - - - + + + + + + + + + {% endblock %} \ No newline at end of file diff --git a/shopping_app/templates/list.html b/shopping_app/templates/list.html index 428b952..706175d 100644 --- a/shopping_app/templates/list.html +++ b/shopping_app/templates/list.html @@ -196,21 +196,25 @@ -
- +
+
➕ Dodaj produkt
+
Wpisz nazwę produktu i ilość, potem kliknij Dodaj.
+
+ - + - + +
{% endif %} @@ -394,7 +398,7 @@
{% block scripts %} - + - - - - - + + + + + - - - - - - + + + + + + diff --git a/shopping_app/templates/main.html b/shopping_app/templates/main.html index 84338b1..80b41b4 100644 --- a/shopping_app/templates/main.html +++ b/shopping_app/templates/main.html @@ -289,8 +289,8 @@ {% block scripts %} - - + + {% endblock %} {% endblock %} \ No newline at end of file diff --git a/shopping_app/web.py b/shopping_app/web.py index 04877fc..11a06c4 100644 --- a/shopping_app/web.py +++ b/shopping_app/web.py @@ -10,7 +10,24 @@ def load_user(user_id): @app.context_processor def inject_version(): - return {"APP_VERSION": app.config["APP_VERSION"]} + def static_asset_url(endpoint, filename): + directory_map = { + "static_bp.serve_js": "static/js", + "static_bp.serve_css": "static/css", + "static_bp.serve_js_lib": "static/lib/js", + "static_bp.serve_css_lib": "static/lib/css", + } + relative_dir = directory_map.get(endpoint) + version = app.config["APP_VERSION"] + if relative_dir: + file_path = os.path.join(app.root_path, relative_dir, filename) + version = get_file_md5(file_path) + return url_for(endpoint, filename=filename, v=version) + + return { + "APP_VERSION": app.config["APP_VERSION"], + "static_asset_url": static_asset_url, + } @app.context_processor diff --git a/tests/test_refactor.py b/tests/test_refactor.py deleted file mode 100644 index c02dcb9..0000000 --- a/tests/test_refactor.py +++ /dev/null @@ -1,70 +0,0 @@ -import unittest -from pathlib import Path - -from shopping_app import app - - -class RefactorSmokeTests(unittest.TestCase): - @classmethod - def setUpClass(cls): - app.config.update(TESTING=True) - cls.client = app.test_client() - - def test_undefined_path_returns_not_500(self): - response = self.client.get('/undefined') - self.assertNotEqual(response.status_code, 500) - self.assertEqual(response.status_code, 404) - - def test_login_page_renders(self): - response = self.client.get('/login') - self.assertEqual(response.status_code, 200) - html = response.get_data(as_text=True) - self.assertIn('name="password"', html) - self.assertIn('app_ui.js', html) - - -class TemplateContractTests(unittest.TestCase): - def test_main_template_uses_single_action_group_on_mobile(self): - main_html = Path('shopping_app/templates/main.html').read_text(encoding='utf-8') - self.assertIn('mobile-list-heading', main_html) - self.assertIn('list-main-title__link', main_html) - self.assertNotIn('d-flex d-sm-none" role="group"', main_html) - - def test_list_templates_use_compact_mobile_action_layout(self): - list_html = Path('shopping_app/templates/list.html').read_text(encoding='utf-8') - shared_html = Path('shopping_app/templates/list_share.html').read_text(encoding='utf-8') - for html in (list_html, shared_html): - self.assertIn('shopping-item-row', html) - self.assertIn('shopping-item-actions', html) - self.assertIn('shopping-compact-input-group', html) - self.assertIn('shopping-item-head', html) - - def test_css_contains_mobile_ux_overrides(self): - css = Path('shopping_app/static/css/style.css').read_text(encoding='utf-8') - self.assertIn('.shopping-item-actions', css) - self.assertIn('.shopping-compact-input-group', css) - self.assertIn('.ui-password-group > .ui-password-toggle', css) - self.assertIn('.hide-purchased-switch--minimal', css) - self.assertIn('.shopping-item-head', css) - self.assertIn('UX tweak 2026-03-14 c: hamburger with full labels', css) - - -if __name__ == '__main__': - unittest.main() - - -class NavbarContractTests(unittest.TestCase): - def test_base_template_uses_mobile_collapse_nav(self): - base_html = Path('shopping_app/templates/base.html').read_text(encoding='utf-8') - self.assertIn('navbar-toggler', base_html) - self.assertIn('appNavbarMenu', base_html) - - - def test_base_template_mobile_nav_has_full_labels(self): - base_html = Path('shopping_app/templates/base.html').read_text(encoding='utf-8') - self.assertIn('>📊 Wydatki<', base_html) - self.assertIn('>🚪 Wyloguj<', base_html) - - def test_main_template_temp_toggle_is_integrated(self): - main_html = Path('shopping_app/templates/main.html').read_text(encoding='utf-8') - self.assertIn('create-list-temp-toggle', main_html)