Tłumaczenia - poprawki.
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user