9 Commits

Author SHA1 Message Date
gru
bc6dcc5bb7 Update README.md 2025-11-29 09:56:22 +01:00
Mateusz Gruszczyński
6da7860b59 oci support 2025-11-24 14:17:20 +01:00
gru
7202459284 Update deploy/app/Dockerfile 2025-11-23 22:32:51 +01:00
gru
6cc430d422 Update deploy/app/Dockerfile 2025-11-23 22:26:45 +01:00
Mateusz Gruszczyński
4128d617a7 zakladka ustawien 2025-10-21 12:08:05 +02:00
Mateusz Gruszczyński
a51e44847e zakladka ustawien 2025-10-21 12:03:45 +02:00
Mateusz Gruszczyński
45a6ab7249 zakladka ustawien 2025-10-21 12:02:29 +02:00
Mateusz Gruszczyński
a363fb9ef8 zakladka ustawien 2025-10-21 11:57:53 +02:00
Mateusz Gruszczyński
2c246ac40a zakladka ustawien 2025-10-21 11:44:21 +02:00
8 changed files with 105 additions and 60 deletions

View File

@@ -16,7 +16,7 @@ Prosta aplikacja webowa do zarządzania listami zakupów z obsługą użytkownik
- Python 3.9+
- Docker (opcjonalnie dla produkcji)
## Instalacja lokalna
## Instalacja lokalna (deweloperska)
1. Sklonuj repozytorium:
@@ -41,14 +41,26 @@ Prosta aplikacja webowa do zarządzania listami zakupów z obsługą użytkownik
flask --app app.py run
```
## Deploy z Docker Compose
## Deploy z Docker Compose - stack (zalecana)
1. Skonfiguruj `.env`.
2. Uruchom:
2.1 Uruchom: (pgsql)
```bash
docker-compose up --build
bash deploy_docker.sh pgsql
```
2.2 Uruchom: (mysql)
```bash
bash deploy_docker.sh mysql
```
2.3 Uruchom: (sqlite)
```bash
bash deploy_docker.sh sqlite
```
Aplikacja będzie dostępna pod `http://localhost:8000`.

48
app.py
View File

@@ -147,39 +147,20 @@ WEBP_SAVE_PARAMS = {
"quality": 95, # tylko jeśli lossless=False
}
def read_commit_and_date(filename="version.txt", root_path=None):
def read_commit(filename="version.txt", root_path=None):
base = root_path or os.path.dirname(os.path.abspath(__file__))
path = os.path.join(base, filename)
if not os.path.exists(path):
return None, None
return None
try:
commit = open(path, "r", encoding="utf-8").read().strip()
if commit:
commit = commit[:12]
return commit[:12] if commit else None
except Exception:
commit = None
return None
try:
ts = os.path.getmtime(path)
date_str = datetime.fromtimestamp(ts).strftime("%Y.%m.%d")
except Exception:
date_str = None
commit = read_commit("version.txt", root_path=os.path.dirname(__file__)) or "dev"
APP_VERSION = commit
return date_str, commit
deploy_date, commit = read_commit_and_date(
"version.txt", root_path=os.path.dirname(__file__)
)
if not deploy_date:
deploy_date = datetime.now().strftime("%Y.%m.%d")
if not commit:
commit = "dev"
APP_VERSION = f"{deploy_date}+{commit}"
app.config["APP_VERSION"] = APP_VERSION
db = SQLAlchemy(app)
@@ -4134,33 +4115,28 @@ def admin_settings():
categories = Category.query.order_by(Category.name.asc()).all()
if request.method == "POST":
# OCR
ocr_raw = (request.form.get("ocr_keywords") or "").strip()
set_setting("ocr_keywords", ocr_raw)
# OCR sensitivity
ocr_sens = (request.form.get("ocr_sensitivity") or "").strip()
set_setting("ocr_sensitivity", ocr_sens)
# Bezpieczeństwo limit błędnych prób (system_auth)
max_attempts = (request.form.get("max_login_attempts") or "").strip()
set_setting("max_login_attempts", max_attempts)
# (opcjonalnie) okno blokady
login_window = (request.form.get("login_window_seconds") or "").strip()
if login_window: # pole możesz ukryć w UI zostawiam jako opcję
if login_window:
set_setting("login_window_seconds", login_window)
# Kolory kategorii
for c in categories:
field = f"color_{c.id}"
val = (request.form.get(field) or "").strip()
vals = request.form.getlist(field)
val = (vals[-1] if vals else "").strip()
existing = CategoryColorOverride.query.filter_by(category_id=c.id).first()
if val and re.fullmatch(r"^#[0-9A-Fa-f]{6}$", val):
if not existing:
db.session.add(
CategoryColorOverride(category_id=c.id, color_hex=val)
)
db.session.add(CategoryColorOverride(category_id=c.id, color_hex=val))
else:
existing.color_hex = val
else:
@@ -4171,7 +4147,6 @@ def admin_settings():
flash("Zapisano ustawienia.", "success")
return redirect(url_for("admin_settings"))
# --- GET ---
override_rows = CategoryColorOverride.query.filter(
CategoryColorOverride.category_id.in_([c.id for c in categories])
).all()
@@ -4183,7 +4158,6 @@ def admin_settings():
current_ocr = get_setting("ocr_keywords", "")
# nowe pola do szablonu
ocr_sensitivity = get_int_setting("ocr_sensitivity", 5)
max_login_attempts = get_int_setting("max_login_attempts", 10)
login_window_seconds = get_int_setting("login_window_seconds", 3600)

View File

@@ -1,5 +1,4 @@
FROM python:3.14-rc-trixie
FROM python:3.14-trixie
#FROM python:3.13-slim
WORKDIR /app

View File

@@ -1,10 +1,16 @@
#!/bin/sh
# Czekaj na bazę w Pythonie
python _tools/wait_for_db.py
# Jeśli nie przekazano zmiennej środowiskowej DB_ENGINE, ustaw na sqlite
DB_ENGINE=${DB_ENGINE:-sqlite}
# Jak baza gotowa, to migruj li daj informacje
echo "Starting app with database engine: $DB_ENGINE"
# Czekaj na bazę, jeśli jest inna niż sqlite (np. PostgreSQL)
if [ "$DB_ENGINE" != "sqlite" ]; then
python _tools/wait_for_db.py --engine "$DB_ENGINE"
fi
# Migracje i start aplikacji
flask db upgrade 2>/dev/null || flask db_info
# Start aplikacji
exec python app.py

Binary file not shown.

View File

@@ -31,9 +31,25 @@
if (barEff) barEff.style.backgroundColor = effHex;
if (hexEffEl) hexEffEl.textContent = effHex;
if (!raw) ensureHiddenClear(input); else removeHiddenClear(input);
if (!raw) {
ensureHiddenClear(input);
input.disabled = true;
} else {
removeHiddenClear(input);
input.disabled = false;
}
}
form.querySelectorAll(".use-default").forEach(btn => {
btn.addEventListener("click", () => {
const name = btn.getAttribute("data-target");
const input = form.querySelector(`input[name="${name}"]`);
if (!input) return;
input.value = "";
updatePreview(input);
});
});
form.querySelectorAll(".reset-one").forEach(btn => {
btn.addEventListener("click", () => {
const name = btn.getAttribute("data-target");
@@ -43,6 +59,7 @@
updatePreview(input);
});
});
resetAllBtn?.addEventListener("click", () => {
form.querySelectorAll('input[type="color"].category-color').forEach(input => {
input.value = "";
@@ -56,6 +73,32 @@
input.addEventListener("change", () => updatePreview(input));
});
form.addEventListener("submit", () => {
form.querySelectorAll('input[type="color"].category-color').forEach(updatePreview);
});
form.querySelectorAll(".use-default").forEach(btn => {
btn.addEventListener("click", () => {
const name = btn.getAttribute("data-target");
const input = form.querySelector(`input[name="${name}"]`);
if (!input) return;
const card = input.closest(".col-12, .col-md-6, .col-lg-4") || input.closest(".col-12");
let autoHex = (input.dataset.auto || "").trim();
if (!autoHex && card) {
autoHex = (card.querySelector(".hex-auto")?.textContent || "").trim();
}
if (autoHex && !autoHex.startsWith("#")) autoHex = `#${autoHex}`;
if (autoHex) {
input.disabled = false;
removeHiddenClear(input);
input.value = autoHex;
updatePreview(input);
}
});
});
(function () {
const slider = document.getElementById("ocr_sensitivity");
const badge = document.getElementById("ocr_sens_badge");

Binary file not shown.

View File

@@ -66,19 +66,30 @@
<div class="col-12 col-md-6 col-lg-4">
<label class="form-label d-block mb-2">{{ c.name }}</label>
<div class="input-group">
<input
type="color"
class="form-control form-control-color category-color"
name="color_{{ c.id }}"
value="{{ hex_override or '' }}"
{% if not hex_override %}data-empty="1"{% endif %}
aria-label="Kolor kategorii {{ c.name }}"
>
<button type="button" class="btn btn-outline-light btn-sm reset-one" data-target="color_{{ c.id }}">
🔄 Reset
<div class="input-group">
<input
type="color"
class="form-control form-control-color category-color"
name="color_{{ c.id }}"
value="{{ hex_override or '' }}"
data-auto="{{ hex_auto }}"
{% if not hex_override %}data-empty="1"{% endif %}
aria-label="Kolor kategorii {{ c.name }}"
>
<div class="btn-group" role="group" aria-label="Akcje koloru">
<button type="button"
class="btn btn-outline-light btn-sm reset-one"
data-target="color_{{ c.id }}">
🔄 Reset
</button>
</div>
<button type="button"
class="btn btn-outline-light btn-sm use-default"
data-target="color_{{ c.id }}">
🎯 Przywróć domyślny
</button>
</div>
</div>
<div class="color-indicators mt-2">
<div class="indicator">