Tłumaczenia - poprawki.

This commit is contained in:
2026-09-25 18:16:53 +02:00
parent 4fde92d323
commit 80e1aa12ed
6 changed files with 178 additions and 49 deletions
+47
View File
@@ -49,6 +49,53 @@ Z powodów bezpieczeństwa, aby klienci w restauracji nie podglądali zamówień
4. Upewnij się, że folder `api/cache` posiada prawa zapisu (`chmod 777` na Linuksie / Pełna kontrola na Windowsie) by skrypt mógł wygenerować `tables_cache.json`.
5. Dla ułatwienia pracy, na roocie znajduje się `index.php` (Dev Portal), z którego możesz klikać we wszystkie moduły (do usunięcia na produkcji).
## 🍽 Odświeżanie cache menu (CLI)
Menu dla gości (PL/EN/DE + tłumaczenia AI ES/IT/KO) **nie odświeża się automatycznie**.
Cache leży w `api/cache/menu_*.json` i aktualizujesz go ręcznie z terminala.
### Wymagania
1. **PHP CLI** (nie `php-fpm`) — np. `/usr/bin/php8` albo `php` w PATH.
2. Katalog `api/cache` zapisywalny przez użytkownika, który odpala skrypt:
```bash
# na serwerze Linux (dostosuj użytkownika do FPM / SSH)
chown -R www-data:www-data api/cache
chmod 775 api/cache
```
3. Klucz OpenAI w `config/menu_ai.local.php` (plik lokalny z `api_key` i modelem — nie commituj klucza).
### Komendy
Z katalogu głównego projektu (`karczma-stoliki/`):
```bash
# Tylko upstream PL/EN/DE + AI gdy zmienił się fingerprint PL
php scripts/refresh_menu_cache.php
# Wymuś ponowne tłumaczenie ES/IT/KO przez OpenAI (zużywa tokeny)
php scripts/refresh_menu_cache.php --force-ai
```
Na produkcji, jeśli `php` wskazuje na FPM albo złą wersję:
```bash
cd /var/www/html/public/app # ścieżka do projektu na serwerze
/usr/bin/php8 scripts/refresh_menu_cache.php --force-ai
```
Skrypt wypisuje log linia po linii. Exit `0` = sukces, `1` = błąd (np. brak praw zapisu, błąd AI).
### Co robi skrypt?
1. Pobiera menu z upstreamu Karczmy (PL/EN/DE) → `api/cache/menu_pl.json` itd.
2. Dla ES/IT/KO tłumaczy zmienione pozycje z PL przez OpenAI (`gpt-4o` wg konfiguracji).
3. Zapisuje fingerprint PL, żeby kolejne uruchomienia bez `--force-ai` pomijały AI, gdy menu się nie zmieniło.
---
## 📊 Analityka (MVP)
W projekcie działa eventowa analityka oparta o MySQL:
+9 -21
View File
@@ -31,30 +31,18 @@ if ($lang === '' || !isset($languages[$lang])) {
$langConfig = $languages[$lang];
$source = (string) ($langConfig['source'] ?? 'upstream');
$ttl = max(60, (int) ($config['ttl_seconds'] ?? 86400));
$timeout = max(3, (int) ($config['timeout_seconds'] ?? 12));
$cacheFile = menuCachePath($lang);
$cached = readMenuCache($cacheFile);
$now = time();
$cachedAtTs = 0;
if ($cached && !empty($cached['cachedAt'])) {
$cachedAtTs = strtotime((string) $cached['cachedAt']) ?: 0;
}
$isFresh = $cached && $cachedAtTs > 0 && ($now - $cachedAtTs) < $ttl;
if ($isFresh) {
// Cache odświeżamy tylko ręcznie (panel admina / skrypt CLI) — nie fetchujemy upstreamu przy każdym TTL.
if ($cached) {
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if ($source === 'ai') {
if ($cached) {
// Stary AI-cache jest OK dłużej niż TTL — cron i tak odświeża raz/dzień.
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$from = strtolower((string) ($langConfig['from'] ?? 'pl'));
$fallback = readMenuCache(menuCachePath($from));
if ($fallback) {
@@ -67,7 +55,7 @@ if ($source === 'ai') {
http_response_code(503);
echo json_encode([
'status' => 'error',
'message' => 'AI menu not available yet — run scripts/refresh_menu_cache.php',
'message' => 'AI menu not available yet — uruchom: php scripts/refresh_menu_cache.php --force-ai',
'lang' => $lang,
], JSON_UNESCAPED_UNICODE);
exit;
@@ -78,20 +66,20 @@ $url = $base . '/' . rawurlencode($lang);
$raw = fetchUpstreamMenu($url, $timeout);
if ($raw === null) {
if ($cached) {
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
http_response_code(502);
echo json_encode([
'status' => 'error',
'message' => 'Failed to fetch menu',
'message' => 'Failed to fetch menu (brak cache — uruchom odświeżenie w panelu)',
'lang' => $lang,
], JSON_UNESCAPED_UNICODE);
exit;
}
$mapped = mapUpstreamMenu($raw, $lang, 'upstream');
writeMenuCache($cacheFile, $mapped);
try {
writeMenuCache($cacheFile, $mapped);
} catch (Throwable $e) {
// Serwuj świeże dane nawet gdy cache nie da się zapisać
}
echo json_encode($mapped, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+37 -5
View File
@@ -6,6 +6,21 @@ declare(strict_types=1);
* Wspólne funkcje cache/upstream menu (api/menu.php + scripts/refresh_menu_cache.php).
*/
if (!function_exists('array_is_list')) {
/** Polyfill PHP < 8.1 */
function array_is_list(array $array): bool
{
$i = 0;
foreach ($array as $k => $_) {
if ($k !== $i++) {
return false;
}
}
return true;
}
}
function menuCacheDir(): string
{
$dir = __DIR__ . '/cache';
@@ -16,6 +31,20 @@ function menuCacheDir(): string
return $dir;
}
function assertMenuCacheWritable(): void
{
$dir = menuCacheDir();
if (!is_dir($dir) || !is_writable($dir)) {
$user = function_exists('posix_geteuid')
? ((string) (@posix_getpwuid(posix_geteuid())['name'] ?? posix_geteuid()))
: get_current_user();
throw new RuntimeException(
"Katalog cache nie jest zapisywalny: {$dir} (proces jako: {$user}). "
. 'Na serwerze: chown -R www-data:www-data api/cache && chmod 775 api/cache'
);
}
}
function menuCachePath(string $lang): string
{
$safe = preg_replace('/[^a-z0-9_-]/i', '', strtolower($lang)) ?: 'pl';
@@ -46,11 +75,14 @@ function readMenuCache(string $cacheFile): ?array
function writeMenuCache(string $cacheFile, array $payload): void
{
file_put_contents(
$cacheFile,
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
LOCK_EX
);
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
throw new RuntimeException('Nie udało się zserializować cache: ' . $cacheFile);
}
$ok = @file_put_contents($cacheFile, $json, LOCK_EX);
if ($ok === false) {
throw new RuntimeException('Permission denied przy zapisie: ' . $cacheFile);
}
}
function fetchUpstreamMenu(string $url, int $timeout): ?array
+1
View File
@@ -10,6 +10,7 @@ declare(strict_types=1);
*/
return [
'upstream_base' => 'https://www.karczmabiesiada.eu/restaurant_menu/export',
// Nieużywane do auto-odświeżania — cache odświeża się tylko ręcznie (panel / CLI).
'ttl_seconds' => 86400,
'timeout_seconds' => 12,
'languages' => [
+53 -10
View File
@@ -15,17 +15,60 @@ requireAdminAuth(true);
body { margin: 0; font-family: Inter, Arial, sans-serif; background: var(--bg); color: var(--text); }
.wrap { width: min(1200px, 94vw); margin: 24px auto; }
.top { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 16px; }
.title { font-size: 1.5rem; font-weight: 800; }
.btn { display: inline-block; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; color: var(--text); text-decoration: none; background: #0b1220; }
.title { font-size: 1.5rem; font-weight: 800; color: var(--text); }
.btn {
display: inline-block;
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 12px;
color: var(--text);
text-decoration: none;
background: #0b1220;
cursor: pointer;
font: inherit;
font-size: 0.9rem;
line-height: 1.2;
}
.btn:hover { filter: brightness(1.08); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: white; }
.admin-tooltip {
position: relative;
}
.admin-tooltip::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
top: calc(100% + 8px);
transform: translateX(-50%);
background: #020617;
color: #e2e8f0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px 10px;
font-size: 12px;
font-weight: 500;
line-height: 1.35;
white-space: normal;
width: max-content;
max-width: 260px;
z-index: 50;
opacity: 0;
pointer-events: none;
transition: opacity 0.12s ease;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.admin-tooltip:hover::after,
.admin-tooltip:focus-visible::after {
opacity: 1;
}
.grid { display: grid; gap: 14px; grid-template-columns: repeat(12,1fr); }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px; }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px; color: var(--text); }
.card h3 { margin: 0 0 8px; font-size: 1rem; color: #cbd5e1; }
.kpi { font-size: 1.8rem; font-weight: 800; margin-top: 4px; }
.kpi { font-size: 1.8rem; font-weight: 800; margin-top: 4px; color: var(--text); }
.muted { color: var(--muted); font-size: .88rem; }
.col-3 { grid-column: span 3; } .col-4 { grid-column: span 4; } .col-6 { grid-column: span 6; } .col-8 { grid-column: span 8; } .col-12 { grid-column: span 12; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #263446; padding: 8px 6px; text-align: left; font-size: .9rem; }
table { width: 100%; border-collapse: collapse; color: var(--text); }
th, td { border-bottom: 1px solid #263446; padding: 8px 6px; text-align: left; font-size: .9rem; color: var(--text); }
th { color: #cbd5e1; }
#recentOpensBody tr.recent-open-row {
border-left: 4px solid transparent;
@@ -79,10 +122,10 @@ requireAdminAuth(true);
<div class="title">Panel Admina</div>
<div class="muted">KDS, generator QR i analityka operacyjna.</div>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a class="btn" href="kds.php">Otwórz KDS</a>
<a class="btn" href="generator.php">Generator QR</a>
<a class="btn" href="logout.php">Wyloguj</a>
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<a class="btn admin-tooltip" href="kds.php" data-tooltip="Panel kuchni / kelnera (KDS)">Otwórz KDS</a>
<a class="btn admin-tooltip" href="generator.php" data-tooltip="Generator kodów QR do stolików">Generator QR</a>
<a class="btn admin-tooltip" href="logout.php" data-tooltip="Wyloguj z panelu admina">Wyloguj</a>
</div>
</div>
+31 -13
View File
@@ -3,25 +3,24 @@
declare(strict_types=1);
/**
* Odświeża cache menu raz na dobę (cron) lub ręcznie.
* Odświeża cache menu ręcznie (tylko CLI / terminal).
*
* 1) Upstream PL/EN/DE → api/cache/menu_{lang}.json
* 2) Języki source=ai (ES/IT/KO): tłumaczenie OpenAI ChatGPT z PL
* tylko gdy fingerprint PL się zmienił (lub brak cache AI).
* (z --force-ai zawsze; bez flagi tylko gdy zmienił się fingerprint PL)
*
* Użycie:
* php scripts/refresh_menu_cache.php
* php scripts/refresh_menu_cache.php --force-ai
*
* Cron (raz dziennie, np. 04:15):
* 15 4 * * * php /path/to/scripts/refresh_menu_cache.php >> /var/log/menu_refresh.log 2>&1
*
* Klucz OpenAI: skopiuj config/menu_ai.local.php.example → config/menu_ai.local.php
* Klucz OpenAI: config/menu_ai.local.php
* Instrukcja: README.md → „Odświeżanie cache menu”
*/
require_once __DIR__ . '/../api/menu_helpers.php';
$forceAi = in_array('--force-ai', $argv ?? [], true);
$hadError = false;
$config = require __DIR__ . '/../config/menu.php';
$languages = is_array($config['languages'] ?? null) ? $config['languages'] : [];
@@ -33,6 +32,13 @@ function logLine(string $msg): void
echo '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
}
try {
assertMenuCacheWritable();
} catch (Throwable $e) {
logLine('ERROR: ' . $e->getMessage());
exit(1);
}
function targetLanguageName(string $code): string
{
return match ($code) {
@@ -325,11 +331,17 @@ foreach ($languages as $code => $meta) {
$raw = fetchUpstreamMenu($url, $timeout);
if ($raw === null) {
logLine("Upstream {$code}: FAILED — keeping old cache if any");
$hadError = true;
continue;
}
$mapped = mapUpstreamMenu($raw, $code, 'upstream');
writeMenuCache(menuCachePath($code), $mapped);
logLine("Upstream {$code}: OK (" . count($mapped['sections'] ?? []) . ' sections)');
try {
$mapped = mapUpstreamMenu($raw, $code, 'upstream');
writeMenuCache(menuCachePath($code), $mapped);
logLine("Upstream {$code}: OK (" . count($mapped['sections'] ?? []) . ' sections)');
} catch (Throwable $e) {
logLine("Upstream {$code}: ERROR " . $e->getMessage());
$hadError = true;
}
}
$plMenu = readMenuCache(menuCachePath('pl'));
@@ -343,13 +355,17 @@ $fpFile = menuFingerprintPath();
$prevFp = is_file($fpFile) ? trim((string) file_get_contents($fpFile)) : '';
$plChanged = $forceAi || $prevFp === '' || !hash_equals($prevFp, $fp);
file_put_contents($fpFile, $fp, LOCK_EX);
logLine('PL fingerprint: ' . substr($fp, 0, 12) . '… changed=' . ($plChanged ? 'yes' : 'no'));
if (@file_put_contents($fpFile, $fp, LOCK_EX) === false) {
logLine('ERROR: nie zapisano fingerprint: ' . $fpFile);
$hadError = true;
} else {
logLine('PL fingerprint: ' . substr($fp, 0, 12) . '… changed=' . ($plChanged ? 'yes' : 'no'));
}
$aiCfg = loadMenuAiConfig();
if ($aiCfg === null) {
logLine('No config/menu_ai.local.php — skip AI languages (copy from .example)');
exit(0);
logLine('No config/menu_ai.local.php — skip AI languages');
exit($hadError ? 1 : 0);
}
foreach ($languages as $code => $meta) {
@@ -371,7 +387,9 @@ foreach ($languages as $code => $meta) {
logLine("AI {$code}: wrote cache");
} catch (Throwable $e) {
logLine('AI ' . $code . ': ERROR ' . $e->getMessage());
$hadError = true;
}
}
logLine('Done.');
exit($hadError ? 1 : 0);