# Web Admin Panel Minimal Redesign — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** رفع ۱۵ باگ وب‌پنل ادمین + بازطراحی مینیمال (۲ تم روشن/تاریک) با CSS خوانای بازنویسی‌شده.

**Architecture:** یک `panel/css/panel.css` جدید جایگزین `style.css`+`modern.css`؛ کلاس‌های موجود حفظ می‌شوند تا مارکاپ صفحات پایدار بماند. رفع باگ‌ها درجا در PHPها. کلیدهای i18n جدید به هر ۴ زبان.

**Tech Stack:** PHP خالص، vanilla JS، CSS توکن‌محور، Node assert برای تست استاتیک (PHP لوکال نداریم).

**Spec:** `docs/superpowers/specs/2026-08-23-web-panel-minimal-redesign-design.md`

## Global Constraints

- هیچ کلید موجود `panel` از lang حذف/تغییرنام نمی‌شود.
- نام کلاس‌های CSS مصرفی در صفحات تغییر نمی‌کند.
- همه state-changing POST ها: `csrf_check_post()`؛ GET های تغییردهنده: `csrf_check_get()`.
- خروجی کاربر: `htmlspecialchars(..., ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8')`.
- تست نهایی: `node tests/web_panel_static_test.mjs` باید PASS شود.
- این پروژه git repo نیست → مرحله commit ندارد؛ به‌جای آن پس از هر تسک تست استاتیک مرتبط اجرا می‌شود.

---

### Task 1: کلیدهای i18n وضعیت‌های جدید (۴ زبان)

**Files:**
- Modify: `lang/fa.php`, `lang/en.php`, `lang/ru.php`, `lang/zh.php` (بخش `panel`، کنار کلیدهای `invoiceStatus*` حدود fa.php:4265)

- [ ] **Step 1: افزودن دو کلید بعد از `'invoiceStatusUnpaid'` در هر فایل**

fa.php:
```php
'invoiceStatusDisabledByAdmin' => 'غیرفعال توسط ادمین',
'invoiceStatusRemovedByAdmin' => 'حذف‌شده توسط ادمین',
```
en.php:
```php
'invoiceStatusDisabledByAdmin' => 'Disabled by admin',
'invoiceStatusRemovedByAdmin' => 'Removed by admin',
```
ru.php:
```php
'invoiceStatusDisabledByAdmin' => 'Отключено админом',
'invoiceStatusRemovedByAdmin' => 'Удалено админом',
```
zh.php:
```php
'invoiceStatusDisabledByAdmin' => '管理员已停用',
'invoiceStatusRemovedByAdmin' => '管理员已删除',
```

- [ ] **Step 2: Verify** — `Select-String "invoiceStatusDisabledByAdmin" lang\*.php` → ۴ نتیجه.

### Task 2: رفع باگ invoice.php (#1 هدر جابجا، #7 نگاشت status، #14 escape LIKE)

**Files:** Modify: `panel/invoice.php:57-59, 98-107, 211-212`

- [ ] **Step 1: escape LIKE (خط 57-59)**
```php
if ($search !== '') {
  $like = '%' . addcslashes($search, '%_\\') . '%';
  $where[] = "(id_user LIKE ? OR id_invoice LIKE ? OR COALESCE(name_product,'') LIKE ? OR COALESCE(username,'') LIKE ?)";
  $params = [$like, $like, $like, $like];
}
```
- [ ] **Step 2: افزودن به `$statusMap` بعد از خط `'Unsuccessful' => …`:**
```php
  'disablebyadmin' => ['tag-no', $textbotlang['panel']['invoiceStatusDisabledByAdmin']],
  'removebyadmin' => ['tag-no', $textbotlang['panel']['invoiceStatusRemovedByAdmin']],
```
(بدون افزودن به bulk whitelist — فقط نمایش.)
- [ ] **Step 3: جابجایی هدرهای ستون (211↔212):** ستون status → `$textbotlang['panel']['invoiceColDate']` و ستون تاریخ → `$textbotlang['panel']['invoiceColStatus']`.
- [ ] **Verify:** grep نبود `%$search%` در invoice.php.

### Task 3: index.php — تکمیل statusMap

**Files:** Modify: `panel/index.php:151-157`

- [ ] **Step 1:** بعد از `'send_on_hold' => …` این دو خط:
```php
'disablebyadmin' => ['tag-no', $textbotlang['panel']['invoiceStatusDisabledByAdmin']],
'removebyadmin' => ['tag-no', $textbotlang['panel']['invoiceStatusRemovedByAdmin']],
```

### Task 4: user.php — #4 برچسب status، #5 SUM واقعی، #11 لیبل نقش، #12 نرخ پرداخت

**Files:** Modify: `panel/user.php:71-75, 88-93, 347-356, 546`

- [ ] **Step 1: SUM جدا برای totalSpent (بعد از کوئری invoices):**
```php
try {
    $totalSpent = (int) db_query($pdo, "SELECT COALESCE(SUM(price_product),0) FROM invoice WHERE id_user = ?", [$id])->fetchColumn();
} catch (Exception $e) {
    $totalSpent = array_sum(array_column($invoices, 'price_product'));
}
```
و حذف `$totalSpent = array_sum(array_column(...))` قدیمی (خط 89).
- [ ] **Step 2: نرخ پرداخت بدون شارژ دستی (خط 92-93):**
```php
$gatewayPayments = array_filter($payments, fn($p) => !in_array($p['Payment_Method'] ?? '', ['add balance by admin', 'low balance by admin'], true));
$paidCount = count(array_filter($gatewayPayments, fn($p) => in_array($p['payment_Status'] ?? '', ['paid', 'success'])));
$convRate = count($gatewayPayments) > 0 ? round($paidCount / count($gatewayPayments) * 100) : 0;
```
- [ ] **Step 3: هم‌ترازی statusMap با index.php (خط 349-351):**
```php
'end_of_time' => ['tag-warn', $textbotlang['panel']['dashStatusExpired']],
'end_of_volume' => ['tag-no', $textbotlang['panel']['dashStatusVolumeFinished']],
'sendedwarn' => ['tag-warn', $textbotlang['panel']['dashStatusWarning']],
```
و افزودن disablebyadmin/removebyadmin مثل Task 3.
- [ ] **Step 4 (خط 546):** `دسترسی کامل` → `<?= user_role_label('all') ?>`.

### Task 5: XSS inline-JS در admins.php و security.php

**Files:** Modify: `panel/admins.php:176-206`، `panel/security.php:161-171, 236-247`

- [ ] **Step 1 (admins.php):** حذف onclick؛ دکمه:
```html
<button type="button" class="btn btn-no btn-sm btn-icon" title="حذف ادمین"
    aria-label="حذف <?= htmlspecialchars($adminUsername) ?>"
    data-admin-delete-form="<?= htmlspecialchars($formId) ?>"
    data-admin-delete-name="<?= htmlspecialchars($adminUsername) ?>">
    <?= icon('trash', 13) ?>
</button>
```
اسکریپت پایین صفحه:
```html
<script>
document.querySelectorAll('[data-admin-delete-form]').forEach(function (btn) {
    btn.addEventListener('click', function () {
        showConfirm('ادمین «' + btn.dataset.adminDeleteName + '» حذف شود؟ این عملیات قابل بازگشت نیست.', function () {
            var form = document.getElementById(btn.dataset.adminDeleteForm);
            if (form) form.submit();
        }, 'حذف ادمین');
    });
});
</script>
```
- [ ] **Step 2 (security.php):** الگوی مشکل — دکمه با `data-session-revoke-form="<?= htmlspecialchars($sessionFormId) ?>" data-session-revoke-label="نشست ادمین <?= htmlspecialchars((string) ($session['admin_username'] ?? '—')) ?>"` و listener معادل (متن تأیید فعلی حفظ شود).

### Task 6: try/catch کوئری‌های محافظت‌نشده

**Files:** Modify: `panel/product.php:83`، `panel/categories.php:6`، `panel/panels.php:6`، `panel/keyboard.php:158`

- [ ] **product.php:**
```php
$products = [];
try {
  $products = db_fetchAll($pdo, "SELECT * FROM product ORDER BY sort_order ASC, id ASC");
} catch (Exception $e) {
  error_log('product.php list failed: ' . $e->getMessage());
}
```
- [ ] **categories.php / panels.php:** همان الگو (`$categories=[]` / `$panels=[]`) — empty-state موجود صفحه خالی را نشان می‌دهد.
- [ ] **keyboard.php:158:** wrap بلوک reset_menu با try/catch؛ در catch: `error_log` + `header('Location: keyboard.php?menu_reset=0&tab=' . urlencode($menuKey))`.

### Task 7: product.php TOCTOU + users.php/payment/search LIKE + لیبل hardcode + redirect compare

**Files:** Modify: `panel/product.php:28-44`، `panel/users.php:18-24, 80-83, 332, 373`، `panel/payment.php:13-17`، `panel/search.php:15`

- [ ] **product.php:** داخل catch INSERT، تشخیص duplicate:
```php
} catch (Exception $e) {
    if ($e instanceof PDOException && str_contains($e->getMessage(), 'Duplicate entry') || db_count($pdo, "SELECT COUNT(*) FROM product WHERE name_product = ?", [$name])) {
      flash('error', $textbotlang['panel']['productNameExists']);
    } else {
      error_log('product.php add failed: ' . $e->getMessage());
      flash('error', 'افزودن محصول انجام نشد. لطفاً دوباره تلاش کنید.');
    }
}
```
- [ ] **users.php:18-24:** مقایسه → `$redirectQuery !== 'q=&status=&role=&balance='`.
- [ ] **users.php:80-83:** الگوی addcslashes مثل Task 2 Step 1.
- [ ] **users.php:332 و 373:** `دسترسی کامل` → `user_role_label('all')` (با `<?=`).
- [ ] **payment.php:15:** قبلش `$like = '%' . addcslashes(trim((string)$q), '%_\\') . '%';` و جایگزینی پارامترها.
- [ ] **search.php:15:** `$like = '%' . addcslashes($query, '%_\\') . '%';`.

### Task 8: logout CSRF

**Files:** Modify: `panel/logout.php:1-8`، `panel/inc/layout_head.php:196,230`

- [ ] **logout.php:** بعد از require:
```php
require_once __DIR__ . '/inc/icons.php';
require_auth();
csrf_check_get();
```
(ترتیب: require_auth اول تا session ادمین چک شود.)
- [ ] **layout_head.php** هر دو لینک logout (سایدبار خط 196 و تاپ‌بار خط 230):
`href="logout.php?_csrf=<?= urlencode(csrf_token()) ?>"`.

### Task 9: audit.php — تکمیل رویدادها

**Files:** Modify: `panel/audit.php:12-35, 73-80`

- [ ] **Step 1:** افزودن به `$eventLabels`:
```php
'service_toggle_status' => 'تغییر وضعیت سرویس',
'service_reset_usage' => 'ریست مصرف سرویس',
'service_revoke_sub' => 'صدور ساب جدید',
'service_remove' => 'حذف سرویس',
'service_extend' => 'تمدید/افزایش حجم سرویس',
'reorder_product' => 'تغییر ترتیب محصولات',
'reorder_panel' => 'تغییر ترتیب پنل‌ها',
'reorder_category' => 'تغییر ترتیب دسته‌بندی‌ها',
'user_quickapprove_toggled' => 'تغییر تأیید سریع کاربر',
'user_cardpayment_toggled' => 'تغییر پرداخت کارت‌به‌کارت کاربر',
```
- [ ] **Step 2:** در `$eventClass`، `'service_remove', 'login_rate_limited'` به گروه tag-no اضافه شوند.

### Task 10: login.php secure cookie proxy-aware

**Files:** Modify: `panel/login.php:1-9`

- [ ] **Step 1:** انتقال session init به بعد از require config و استفاده از helper:
```php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/icons.php';

if (session_status() === PHP_SESSION_NONE) {
  session_set_cookie_params([
    'httponly' => true,
    'secure' => panel_is_https(),
    'samesite' => 'Lax',
  ]);
  session_start();
}
```
(حذف بلاک قدیمی خط 2-9.)

### Task 11: سیستم طراحی مینیمال — ایجاد `panel/css/panel.css`

**Files:** Create: `panel/css/panel.css`

- [ ] **Step 1: ساختار فایل** (کامنت‌بخشی‌شده، خوانا):
```
1. Design tokens (:root روشن + [data-theme="dark"])
2. Base/reset + typography
3. Layout: app / sidebar / topbar / content / bottom-nav
4. Components: card/stat/tag/btn/input/select/field/form-grid
5. Data: tbl-*/toolbar/bulk-*/pager/tbl-foot/skeleton/empty
6. Feedback: notice/toast/confirm/modal/load-bar/fade-up
7. Overlays: command-palette/auth(login)
8. Page-specific: profile/u-*/report-*/health-*/theme-picker/settings-tabs/totp-*
9. Utilities + responsive + reduced-motion + focus-visible
```
- [ ] **Step 2: توکن‌ها (دقیق):**
```css
:root {
  --bg:#FAFAF9; --sf:#FFFFFF; --sf2:#F5F5F4; --sf3:#EFEDEA; --hl:#F5F5F4;
  --bd:#E7E5E4; --bds:#D6D3D1; --ac:#4F46E5; --ach:#4338CA; --acs:rgba(79,70,229,.08); --acg:rgba(79,70,229,.16);
  --ok:#15803D; --oks:rgba(21,128,61,.09); --no:#DC2626; --nos:rgba(220,38,38,.08);
  --warn:#B45309; --warns:rgba(180,83,9,.09);
  --text:#1C1917; --text2:#44403C; --mute:#78716C; --dim:#A8A29E; --fade:#D6D3D1;
  color-scheme: light;
}
[data-theme="dark"] {
  --bg:#0F1014; --sf:#17181C; --sf2:#1C1D22; --sf3:#22242A; --hl:#22242A;
  --bd:#26272C; --bds:#33343B; --ac:#818CF8; --ach:#A5B4FC; --acs:rgba(129,140,248,.10); --acg:rgba(129,140,248,.18);
  --ok:#4ADE80; --oks:rgba(74,222,128,.10); --no:#F87171; --nos:rgba(248,113,113,.10);
  --warn:#FBBF24; --warns:rgba(251,191,36,.10);
  --text:#E7E5E4; --text2:#D6D3D1; --mute:#A8A29E; --dim:#78716C; --fade:#44403C;
  color-scheme: dark;
}
```
قواعد مینیمال: بدون gradient/glow/keyframes تزئینی (فقط fadeUp ملایم 160ms، spin برای لودر، loadbar)؛ سایه فقط overlay: `--shadow-overlay: 0 12px 40px rgba(0,0,0,.14)`؛ radius یکنواخت 10px کارت/8px دکمه/6px input؛ focus-visible: `outline:2px solid var(--ac); outline-offset:2px`.
- [ ] **Step 3: پوشش کامل کلاس‌ها** — لیست الزامی (از inventory): active, app, auth*, bad/bar/bar-fill, bnav-item, bottom-nav(-row), brand-mark/name, btn(-ghost/-icon/-link/-no/-ok/-primary/-sm), card(-body/-head/-subtitle/-title), cf/cm/cn/cs, chart-axis-label/chart-bar/chart-grid-line/chart-line(-orders,-users), closing, collapsed, confirm-box/-btns/-icon/-veil, content, crumb, cs, cur, d1-d4, dash-chart-row/dash-secondary, dashboard-actions/dashboard-hero, dis, dis/dn, dot, drag-grip/drag-handle, empty, fade-up, field/field-hint, form-grid/full, global-style-saved, health-*, ill, inline-form, input, keyboard-style-*, kv(-key/-list/-val), lede, legend-orders/legend-users, lb-end/lb-go, live-search-meta, main, menu-toggle, miniapp-color-field, modal(-body/-foot/-head/-veil/-x), nav-heading/nav-icon/nav-item/nav-label/nav-section, no, notice(-no/-ok/-warn), ok, open, page-head/page-kpis, pager, profile-avatar/grid/handle/head/name, purchase-buttons-copy/save, quick-card/quick-grid/quick-icon, report-*, row-move/row-num, sb-toggle, search-box/-btn/-clear, security-grid, select, session-client, settings-security-note/settings-tab(s), sidebar(-backdrop/-brand/-foot/-nav), skeleton, sr-only, stat(-label/-meta/-num)/stats, tag(-info/-no/-ok/-plain/-warn), tbl-foot/lg/md/skeleton/sm/wrap/xl, theme-card/desc/grid/name/preview, toolbar(-end/-title), totp-card/recovery-box/recovery-codes/secret, topbar(-left/-title/-tools), two-col, u-main-col/u-profile-grid/u-sidebar/u-stats/u-tab-bar, uname/up, up, urole, user-info/user-mono/user-pill, warn, w3, admin-create-form/admin-layout, audit-details, settings-* .
- [ ] **Step 4: responsive** — ≤768px: سایدبار off-canvas (`.sidebar.open`)، bottom-nav نمایان، stats دوستونه؛ `.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}`.

### Task 12: سوئیچ به ۲ تم — layout_head/login/theme JS/settings

**Files:**
- Modify: `panel/inc/layout_head.php:22-42, 196, 214-232`
- Modify: `panel/login.php:118-120` (+ریستایل auth مینیمال تک‌کارتی)
- Modify: `panel/js/app.js:165-180`
- Modify: `panel/js/theme-init.js` (فایل → ۲ تم + prefers-color-scheme)
- Modify: `panel/js/settings.js:1-…` (pickTheme)
- Modify: `panel/settings.php` ($themes + گالری‌ها)
- Delete: `panel/css/style.css`, `panel/css/modern.css`

- [ ] **Step 1 (layout_head):** فقط `<link rel="stylesheet" href="css/panel.css">`؛ اسکریپت init:
```js
(function () {
  var t = localStorage.getItem('panel-theme');
  if (t !== 'light' && t !== 'dark') t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  document.documentElement.setAttribute('data-theme', t);
  var mtc = document.getElementById('mtc');
  if (mtc) mtc.content = t === 'dark' ? '#0F1014' : '#FAFAF9';
  if (localStorage.getItem('panel-sb-collapsed') === '1' && innerWidth > 768)
    document.documentElement.classList.add('sb-pre-collapsed');
}());
```
تاپ‌بار: بین search و logout دکمه:
```html
<button type="button" class="icon-btn" id="themeToggle" title="تغییر تم" aria-label="تغییر تم روشن/تاریک">
  <span class="theme-icon-light">☀</span><span class="theme-icon-dark">☾</span>
</button>
```
(نمایش آیکون با CSS `[data-theme=dark] .theme-icon-light{display:none}` و بالعکس.)
- [ ] **Step 2 (app.js):** `_THEME_BG`/`applyTheme` ساده:
```js
window.applyTheme = function (t) {
  if (t !== 'light' && t !== 'dark') t = 'light';
  document.documentElement.setAttribute('data-theme', t);
  localStorage.setItem('panel-theme', t);
  document.documentElement.style.colorScheme = t === 'dark' ? 'dark' : 'light';
  var mtc = document.getElementById('mtc');
  if (mtc) mtc.content = t === 'dark' ? '#0F1014' : '#FAFAF9';
};
(function () {
  var btn = document.getElementById('themeToggle');
  if (!btn) return;
  btn.addEventListener('click', function () {
    var cur = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
    applyTheme(cur === 'dark' ? 'light' : 'dark');
  });
}());
```
- [ ] **Step 3 (theme-init.js):** همان منطق Step 1 (برای صفحات غیر layout اگر لازم شد).
- [ ] **Step 4 (settings.js pickTheme):**
```js
window.pickTheme = function (t) { window.applyTheme(t); };
```
+ sync UI (کارت انتخاب‌شده `.active`).
- [ ] **Step 5 (settings.php):** `$themes` فقط دو عضو:
```php
$themes = [
  'light' => ['name'=>'روشن','desc'=>'مینیمال روز','c'=>['#FAFAF9','#FFFFFF','#4F46E5','#15803D'],'dark'=>false],
  'dark'  => ['name'=>'تاریک','desc'=>'مینیمال شب','c'=>['#0F1014','#17181C','#818CF8','#4ADE80'],'dark'=>true],
];
```
دو گالری → یک بخش «نمایش» با همان markup theme-card.
- [ ] **Step 6 (login.php):** لینک فقط panel.css؛ اسکریپت init مثل Step 1؛ مارکاپ auth به تک‌کارت مینیمال (auth-aside حذف؛ brand + فرم وسط). متون/کلیدهای lang فعلی حفظ.
- [ ] **Step 7:** Delete `panel/css/style.css`, `panel/css/modern.css`.
- [ ] **Step 8 (keyboard.php:220):** اسکریپت inline تم آن صفحه هم به نسخه ۲ تمی ساده شود و لینک CSS فقط panel.css.

### Task 13: تست استاتیک + وریفای نهایی

**Files:** Create: `tests/web_panel_static_test.mjs`

- [ ] **Step 1: نوشتن تست** شامل:
  - استخراج همه `$textbotlang['panel']['KEY']` از `panel/**.php` (regex) و assert وجود KEY در fa.php (parse آرایه با regex `'KEY' =>`).
  - fa.php مقدار `productConfirmDeleteProduct` نباید شامل `htmlspecialchars` باشد.
  - invoice.php نباید `%$search%` داشته باشد؛ باید `addcslashes` داشته باشد.
  - invoice.php: ستون status از `invoiceColDate` استفاده کند (ترتیب خطوط: invoiceColPrice سپس invoiceColDate سپس invoiceColStatus).
  - users.php شامل `'q=&status=&role=&balance='`.
  - admins.php/security.php نباشد: `onclick="confirmAdminDelete(` / `onclick="confirmSessionRevoke(`؛ باشد: `data-admin-delete-form` / `data-session-revoke-form`.
  - logout.php شامل `csrf_check_get()`.
  - login.php شامل `panel_is_https()`.
  - user.php شامل `COALESCE(SUM(price_product),0)` و نباشد `array_sum(array_column($invoices`.
  - panel.css باشد و شامل `[data-theme="dark"]`؛ style.css/modern.css نباشند؛ هیچ PHP به style.css/modern.css اشاره نکند.
  - هر ۴ زبان شامل `invoiceStatusDisabledByAdmin`.
  - همه POST handlerها (regex `REQUEST_METHOD.*POST`) در صفحات دارای `csrf_check_post`.
- [ ] **Step 2: اجرا:** `node tests/web_panel_static_test.mjs` → PASS.
- [ ] **Step 3:** اجرای تست‌های Node دیگر پروژه (tests/*.mjs) → بدون رگرسیون.
- [ ] **Step 4:** بازبینی دستی diff همه PHPهای ویرایش‌شده (چون php -l در دسترس نیست).
