Compare commits
9 Commits
43b7b93ffa
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bc6dcc5bb7 | |||
|
|
6da7860b59 | ||
| 7202459284 | |||
| 6cc430d422 | |||
|
|
4128d617a7 | ||
|
|
a51e44847e | ||
|
|
45a6ab7249 | ||
|
|
a363fb9ef8 | ||
|
|
2c246ac40a |
20
README.md
20
README.md
@@ -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
48
app.py
@@ -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)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
FROM python:3.14-rc-trixie
|
||||
FROM python:3.14-trixie
|
||||
#FROM python:3.13-slim
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
static,tar.gz
BIN
static,tar.gz
Binary file not shown.
@@ -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");
|
||||
|
||||
BIN
templates.tar.gz
BIN
templates.tar.gz
Binary file not shown.
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user