dev
This commit is contained in:
parent
8fdcd0b901
commit
9e20ae6c5f
@ -14,3 +14,4 @@
|
||||
- [Roadmap: слой БД](roadmap.md) — MariaDB/PDO реализован и протестирован (реальное подключение); Mongo/Elasticsearch написаны, ждут внешней инфры от пользователя
|
||||
- [Работа с референс-кодом из других проектов](feedback_reference_code_handling.md) — пользователь кидает код из eoffice_v3 и т.п.: адаптировать критически, не копировать вслепую, но уважать явные повторные сигналы по неймингу
|
||||
- [Идея: CLI-точка входа](idea_cli_entrypoint.md) — отложенный cli.php для миграций/крона
|
||||
- [Никакого inline JS в HTML](feedback_no_inline_js.md) — только jQuery `.on()` в отдельном .js-файле, даже для мелочи вроде onchange
|
||||
|
||||
22
.claude/memory/feedback_no_inline_js.md
Normal file
22
.claude/memory/feedback_no_inline_js.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: feedback-no-inline-js
|
||||
description: Никогда не писать inline JS-атрибуты (onchange/onclick/...) в HTML — только jQuery через .on()
|
||||
metadata:
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Пользователь прямо запретил inline JS-обработчики в HTML (`onchange="this.form.submit()"` и подобные) —
|
||||
даже для мелочи вроде автосабмита `<select>` при смене значения.
|
||||
|
||||
**Why:** Явно сказал «никогда не пиши onchange="this.form.submit() такое в html» и прислал пример из
|
||||
своего другого проекта — везде используется jQuery с делегированием событий через `.on()` на контейнер
|
||||
(`$('#content').on('click', '#selector', fn)`), а не атрибуты `onXxx=` в разметке.
|
||||
|
||||
**How to apply:** Для любого интерактивного поведения (submit по смене select, клики, любые обработчики)
|
||||
писать JS в отдельном файле (`App/media/js/*.js`, подключается через `$this->setScript('name.js')` — см.
|
||||
`System\Classes\View`), с jQuery-биндингом `$(function () { $(container).on(event, selector, handler); })`.
|
||||
Никаких `onclick=`/`onchange=`/`onsubmit=` и т.п. атрибутов в `.html`-шаблонах — даже одна строчка.
|
||||
|
||||
Это касается и уже написанного `System\Classes\ProfilerToolbar` (там сейчас `onclick` инлайном для
|
||||
переключения панели/вкладок, самодостаточности ради) — при следующей правке этого файла стоит переписать
|
||||
на тот же паттерн, если пользователь укажет, что это тоже нужно поправить (сейчас не просил явно).
|
||||
@ -44,6 +44,7 @@ return [
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'enabled' => false,
|
||||
'path' => APPPATH . '/logs',
|
||||
'threshold' => 'debug',
|
||||
],
|
||||
|
||||
6
App/media/js/logs.js
Normal file
6
App/media/js/logs.js
Normal file
@ -0,0 +1,6 @@
|
||||
$(function () {
|
||||
// Смена даты/канала/уровня — сразу отправляет форму; текст ищем по кнопке "Найти".
|
||||
$('#logs-filters').on('change', 'select[name="date"], select[name="channel"], select[name="level"]', function () {
|
||||
$(this).closest('form').trigger('submit');
|
||||
});
|
||||
});
|
||||
@ -15,10 +15,12 @@ $level_class = static fn(string $l): string => [
|
||||
'info' => 'primary',
|
||||
'debug' => 'secondary',
|
||||
][$l] ?? 'secondary';
|
||||
|
||||
$this->setScript('logs.js');
|
||||
?>
|
||||
<h1 class="h3 mb-3">Логи</h1>
|
||||
|
||||
<form method="get" class="row g-2 mb-3">
|
||||
<form method="get" class="row g-2 mb-3" id="logs-filters">
|
||||
<div class="col-auto">
|
||||
<select name="date" class="form-select">
|
||||
<?php if (!in_array($filters['date'], $dates, true)): ?>
|
||||
|
||||
@ -495,6 +495,8 @@ Log::error('Сбой оплаты');
|
||||
Log::debug(...); Log::warning(...);
|
||||
```
|
||||
|
||||
- Вкл/выкл целиком — `Log::$enabled` → `Config::get('log','enabled')` → `true` по умолчанию;
|
||||
`false` — `write()` ничего не пишет ни в один файл, независимо от уровня/порога.
|
||||
- Уровни: `debug(100) < info(200) < warning(300) < error(400)`. Пишутся только уровни не ниже
|
||||
порога `Log::$threshold` (по умолчанию из `Config::get('log','threshold')` → `debug`).
|
||||
- Канал по уровню: `info`/`debug` → файл `action-…`, `warning`/`error` → файл `error-…`.
|
||||
@ -620,7 +622,7 @@ PHPUnit 11 в Docker-контейнере `bicycle`. Bootstrap: `tests/bootstrap
|
||||
| `tests/Unit/CSRFTest.php` | `token()` стабильность/формат, `validate()`, `field()`, нет токена в сессии |
|
||||
| `tests/Unit/ControllerTest.php` | `executeAction()`, порядок `before/action/after`, no-op хуки |
|
||||
| `tests/Unit/ControllerAfterTest.php` | `after()` веб-`Controller`: заголовок `X-Profiler` только для admin, содержит SQL-сводку — через `xdebug_get_headers()`, иначе `markTestSkipped()` |
|
||||
| `tests/Unit/LogTest.php` | уровни/порог, каналы (action/error), формат, append, `strtr`, маскировка, `requestInfo()` |
|
||||
| `tests/Unit/LogTest.php` | уровни/порог, `enabled` (выкл — ничего не пишет), каналы (action/error), формат, append, `strtr`, маскировка, `requestInfo()` |
|
||||
| `tests/Unit/FileLogReaderTest.php` | парсинг, фильтры (level/q/channel), newest-first, missing file, `dates()` |
|
||||
| `tests/Unit/FileAuthDriverTest.php` | `login()` верно/неверно/неизвестный логин, `loggedIn()`, `getUser()` без пароля, `logout()`, `checkPassword()`, отсутствие файла |
|
||||
| `tests/Unit/AuthTest.php` | `instance()` драйвер по умолчанию/явный, кэширование по драйверу, неизвестный драйвер → исключение |
|
||||
@ -636,7 +638,7 @@ PHPUnit 11 в Docker-контейнере `bicycle`. Bootstrap: `tests/bootstrap
|
||||
| `tests/Unit/MongoDriverTest.php` | `instance()`, `collection()`, `database()` — требует `ext-mongodb`, иначе `markTestSkipped()` |
|
||||
| `tests/Unit/ProfilerToolbarTest.php` | `render()` пусто для гостя/не-admin, панель с временем/памятью/SQL для admin, вкладки Vars/Files/Route, маскировка чувствительных ключей, вкладка Custom только при `addData()`, `EXPLAIN` для реального SELECT / пропуск для не-SELECT |
|
||||
|
||||
**247 тестов, 384 assertion — все проходят (8 skipped: Elasticsearch/Mongo без живой инфраструктуры — MariaDB подключена и все её тесты реально проходят, см. разделы DataBase/Elasticsearch/Mongo выше).**
|
||||
**248 тестов, 386 assertion — все проходят (8 skipped: Elasticsearch/Mongo без живой инфраструктуры — MariaDB подключена и все её тесты реально проходят, см. разделы DataBase/Elasticsearch/Mongo выше).**
|
||||
|
||||
### Frontend dependencies (через Composer)
|
||||
|
||||
|
||||
@ -40,6 +40,9 @@ class Log
|
||||
self::ERROR => 'error',
|
||||
];
|
||||
|
||||
/** @var bool|null Писать ли логи вообще; null → Config('log','enabled') → true */
|
||||
public static ?bool $enabled = null;
|
||||
|
||||
/** @var string|null Каталог логов; null → Config('log','path') → APPPATH/logs */
|
||||
public static ?string $directory = null;
|
||||
|
||||
@ -59,6 +62,10 @@ class Log
|
||||
*/
|
||||
public static function write(string $level, string $message, array $variables = []): void
|
||||
{
|
||||
if (!self::enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::weight($level) < self::weight(self::threshold())) {
|
||||
return;
|
||||
}
|
||||
@ -68,8 +75,8 @@ class Log
|
||||
}
|
||||
|
||||
$dir = self::directory();
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0775, true);
|
||||
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$line = sprintf("[%s] %s: %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message);
|
||||
@ -143,6 +150,16 @@ class Log
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Писать ли логи вообще (из свойства, конфига или дефолт true).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function enabled(): bool
|
||||
{
|
||||
return self::$enabled ?? Config::get('log', 'enabled') ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Минимальный уровень записи (из свойства, конфига или дефолт debug).
|
||||
*
|
||||
|
||||
@ -32,7 +32,7 @@ class ControllerAfterTest extends TestCase
|
||||
|
||||
header_remove();
|
||||
|
||||
$this->users_file = sys_get_temp_dir() . '/bicycle_after_users_' . uniqid() . '.php';
|
||||
$this->users_file = sys_get_temp_dir() . '/bicycle_after_users_' . uniqid('', true) . '.php';
|
||||
file_put_contents($this->users_file, '<?php return ' . var_export([
|
||||
'admin' => ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'admin'],
|
||||
'bob' => ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'manager'],
|
||||
|
||||
@ -13,7 +13,7 @@ class FileAuthDriverTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->file = sys_get_temp_dir() . '/bicycle_auth_users_' . uniqid() . '.php';
|
||||
$this->file = sys_get_temp_dir() . '/bicycle_auth_users_' . uniqid('', true) . '.php';
|
||||
|
||||
file_put_contents($this->file, '<?php return ' . var_export([
|
||||
'bob' => [
|
||||
@ -108,7 +108,7 @@ class FileAuthDriverTest extends TestCase
|
||||
|
||||
public function testMissingUsersFileBehavesAsEmptyList(): void
|
||||
{
|
||||
$driver = new FileAuthDriver(sys_get_temp_dir() . '/does_not_exist_' . uniqid() . '.php');
|
||||
$driver = new FileAuthDriver(sys_get_temp_dir() . '/does_not_exist_' . uniqid('', true) . '.php');
|
||||
$this->assertFalse($driver->login('bob', 'secret'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ class FileLogReaderTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dir = sys_get_temp_dir() . '/bicycle_reader_' . uniqid();
|
||||
$this->dir = sys_get_temp_dir() . '/bicycle_reader_' . uniqid('', true);
|
||||
$this->today = date('Y-m-d');
|
||||
mkdir($this->dir);
|
||||
Log::$directory = $this->dir;
|
||||
|
||||
@ -11,9 +11,10 @@ class LogTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dir = sys_get_temp_dir() . '/bicycle_log_' . uniqid();
|
||||
$this->dir = sys_get_temp_dir() . '/bicycle_log_' . uniqid('', true);
|
||||
Log::$directory = $this->dir;
|
||||
Log::$threshold = Log::DEBUG;
|
||||
Log::$enabled = true;
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
@ -26,6 +27,7 @@ class LogTest extends TestCase
|
||||
}
|
||||
Log::$directory = null;
|
||||
Log::$threshold = null;
|
||||
Log::$enabled = null;
|
||||
}
|
||||
|
||||
private function logFile(string $channel): string
|
||||
@ -64,6 +66,16 @@ class LogTest extends TestCase
|
||||
$this->assertStringContainsString('WARNING: предупреждение', file_get_contents($this->logFile('error')));
|
||||
}
|
||||
|
||||
public function testDisabledSkipsWritingEntirely(): void
|
||||
{
|
||||
Log::$enabled = false;
|
||||
|
||||
Log::error('не должно записаться');
|
||||
|
||||
$this->assertFileDoesNotExist($this->logFile('action'));
|
||||
$this->assertFileDoesNotExist($this->logFile('error'));
|
||||
}
|
||||
|
||||
public function testThresholdFiltersLowerLevels(): void
|
||||
{
|
||||
Log::$threshold = Log::WARNING;
|
||||
|
||||
@ -18,7 +18,7 @@ class ProfilerToolbarTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->users_file = sys_get_temp_dir() . '/bicycle_toolbar_users_' . uniqid() . '.php';
|
||||
$this->users_file = sys_get_temp_dir() . '/bicycle_toolbar_users_' . uniqid('', true) . '.php';
|
||||
file_put_contents($this->users_file, '<?php return ' . var_export([
|
||||
'admin' => ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'admin'],
|
||||
'bob' => ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'manager'],
|
||||
|
||||
@ -12,7 +12,7 @@ class ViewTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tmpFile = sys_get_temp_dir() . '/bicycle_view_' . uniqid() . '.html';
|
||||
$this->tmpFile = sys_get_temp_dir() . '/bicycle_view_' . uniqid('', true) . '.html';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
|
||||
Loading…
Reference in New Issue
Block a user