Three intertwined bodies of work developed and tested together this session. Full test suite green: python (edge) + phpunit (cloud). ## Edge/Pi install path - setup_online.sh: apt-lock retry (apt-daily contention), Type=exec unit + StartLimitIntervalSec in [Unit], journalctl-on-failure, crash-loop detection. - onnxruntime is now best-effort (no sdist / no 32-bit-ARM wheel); [6/8] probes the real backend and hard-fails if none works. - Hailo HEF auto-fetch: edge/tools/fetch_hailo_hef.py (pinned sha256, refuses to guess 8 vs 8L, HAILO_ARCH override for serial-less boards); hailo_pose.py run() timeout fix. - install_offline.sh: mirrors the backend gate; onnxruntime optional. ## Cafe24 cloud package (cloud/deploy/) - build_cafe24_package.sh emits move_detect_app/ (private, one folder) + www/move_detect/ (docroot subdir); no-dev classmap autoloader; preflight.php; hardened .htaccess. - Subdirectory mount support: Request::baseFrom/pathFor strip the mount prefix; index.php walks up to find the app root; views/JS prefix every URL with APP_BASE; session cookie scoped to the mount. ## Delivery audit — silent-failure & correctness (docs/delivery-audit.md) All 4 blockers + all Phase-1 silent-failure + Phase-2 HIGH correctness: - Edge cloud clients send a custom User-Agent (cafe24 WAF blocks python-requests) and allow_redirects=False. - Daemon fails loud on blank CAFE24_URL/DEVICE_SECRET; run_loop logs swallowed exceptions; heartbeat carries real fps + per-camera health; UvcSource reopens a dead camera; offline queue is size-capped and drops poison rows; TARGET_FPS/QUEUE_DB validated. - Zone coordinates normalized 0..1 on both sides (were canvas-px vs camera-px — every zone misplaced); pinch zones emit PINCH/HIGH (were downgraded to WARNING). - Fall detection uses bbox cues when keypoints are low-confidence (occluded fallen person); ghost incident not cancelled by a bystander; displacement normalized per-second (FPS-invariant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
160 lines
8.1 KiB
PHP
160 lines
8.1 KiB
PHP
<?php
|
|
// Uploaded as www/_preflight.php. Open https://<domain>/_preflight.php once, confirm every
|
|
// row is OK, then DELETE THIS FILE.
|
|
//
|
|
// It exists because three host-level facts are unknowable from the repo and every one of
|
|
// them fails badly on a stock PHP build:
|
|
// * no argon2 -> src/Security/Password.php calls password_hash(..., PASSWORD_ARGON2ID)
|
|
// unguarded, and AuthController::DUMMY_HASH runs verify() on EVERY login
|
|
// attempt including unknown usernames -> the login page fatals for
|
|
// everyone, instantly.
|
|
// * no gd -> ImageProcessor::reencode() returns ok=false and IngestController's
|
|
// `continue` swallows it: events save with no snapshot, API still 201s.
|
|
// * no pdo_mysql -> every request 500s at Database::connect(). (mysqli being present does
|
|
// NOT imply pdo_mysql.)
|
|
//
|
|
// Deliberately prints booleans only — no phpinfo(), no paths, no config values — so the
|
|
// window between upload and deletion leaks nothing useful.
|
|
declare(strict_types=1);
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
header('X-Robots-Tag: noindex, nofollow');
|
|
|
|
// Find the app root exactly the way public/index.php does — container first, then the bare
|
|
// directory, probing each level for any marker. Using ANY marker (not all three) is
|
|
// deliberate here and differs from index.php on purpose: this page exists to DIAGNOSE a
|
|
// half-finished upload, so it must still locate a root that is missing vendor/ or config.php
|
|
// and then report precisely which piece is absent.
|
|
const APP_PRIVATE_DIR = 'move_detect_app';
|
|
$root = null;
|
|
$hasAnyMarker = static fn (string $d): bool =>
|
|
is_file($d . '/config.php') || is_file($d . '/vendor/autoload.php') || is_file($d . '/src/Config.php');
|
|
for ($d = dirname(__DIR__), $i = 0; $i < 4; $i++) {
|
|
foreach ([$d . '/' . APP_PRIVATE_DIR, $d] as $cand) {
|
|
if ($hasAnyMarker($cand)) { $root = $cand; break 2; }
|
|
}
|
|
if (dirname($d) === $d) break;
|
|
$d = dirname($d);
|
|
}
|
|
// Nothing found anywhere IS the diagnosis: fall back to where the files SHOULD be, so every
|
|
// row below reports a truthful "missing" instead of crashing on null.
|
|
$root = $root ?? dirname(dirname(__DIR__)) . '/' . APP_PRIVATE_DIR;
|
|
|
|
$checks = [
|
|
'PHP >= 8.2' => [
|
|
PHP_VERSION_ID >= 80200,
|
|
'Cafe24 관리자에서 PHP 버전을 8.2 이상으로 올리세요. (주의: 카페24는 PHP 버전 변경 시 데이터와 DB가 초기화된다고 안내합니다. 반드시 업로드 전에 정하세요.)',
|
|
],
|
|
'pdo_mysql 확장' => [
|
|
in_array('mysql', PDO::getAvailableDrivers(), true),
|
|
'DB 연결이 전혀 안 됩니다. 카페24에 pdo_mysql 활성화를 요청하세요.',
|
|
],
|
|
'gd 확장' => [
|
|
function_exists('imagecreatefromstring') && function_exists('imagejpeg'),
|
|
'사진(스냅샷)이 조용히 사라집니다. 이벤트는 저장되는데 사진만 없어집니다. 카페24에 gd 활성화를 요청하세요.',
|
|
],
|
|
'Argon2id 암호 해시' => [
|
|
defined('PASSWORD_ARGON2ID') && in_array('argon2id', password_algos(), true),
|
|
'로그인 페이지가 즉시 죽습니다. 이게 가장 흔한 배포 실패 원인입니다. 카페24에 Argon2 포함 PHP 빌드를 요청하세요.',
|
|
],
|
|
'config.php 있음' => [
|
|
is_readable($root . '/config.php'),
|
|
'FTP 최상위(www 밖)에 config.php 를 올리세요.',
|
|
],
|
|
// Checks the parsed VALUES, not the file text. Grepping the raw file for 'CHANGEME'
|
|
// would also match the template's explanatory comments, which survive a correct edit —
|
|
// so a properly-filled config would report 실패 forever.
|
|
'config.php 를 수정했음' => (function () use ($root) {
|
|
$fix = 'config.php 안의 CHANGEME 를 실제 DB 이름/아이디/비밀번호로 바꾸세요.';
|
|
if (!is_readable($root . '/config.php')) {
|
|
return [false, $fix];
|
|
}
|
|
$cfg = @require $root . '/config.php';
|
|
if (!is_array($cfg)) {
|
|
return [false, 'config.php 가 PHP 배열을 return 하지 않습니다. 파일이 깨졌는지 확인하세요.'];
|
|
}
|
|
foreach (['db_dsn', 'db_user', 'db_pass'] as $k) {
|
|
if (str_contains((string)($cfg[$k] ?? ''), 'CHANGEME')) {
|
|
return [false, $fix];
|
|
}
|
|
}
|
|
return [true, ''];
|
|
})(),
|
|
'storage/snapshots 쓰기 가능' => [
|
|
is_dir($root . '/storage/snapshots') && is_writable($root . '/storage/snapshots'),
|
|
'사진이 조용히 사라집니다. 폴더를 만들고 권한 750 으로 주세요.',
|
|
],
|
|
'src/ 있음' => [
|
|
is_readable($root . '/src/Config.php'),
|
|
'src/ 폴더를 FTP 최상위(www 밖)에 올리세요.',
|
|
],
|
|
'vendor/ 오토로더 있음' => [
|
|
is_readable($root . '/vendor/autoload.php'),
|
|
'vendor/ 폴더를 FTP 최상위(www 밖)에 올리세요. 없으면 모든 페이지가 죽습니다.',
|
|
],
|
|
// These probe THIS directory (the one the web serves), not $root. If the secrets turned
|
|
// up here, they are downloadable.
|
|
'src/ 가 웹에 노출되지 않음' => [
|
|
!is_readable(__DIR__ . '/src/Config.php'),
|
|
'src/ 가 웹에서 접근 가능한 폴더 안에 있습니다. FTP 최상위(www 밖)로 옮기세요.',
|
|
],
|
|
'config.php 가 웹에 노출되지 않음' => [
|
|
!is_readable(__DIR__ . '/config.php'),
|
|
'config.php 가 웹에서 접근 가능한 폴더 안에 있습니다. DB 비밀번호가 노출됩니다. FTP 최상위(www 밖)로 옮기세요.',
|
|
],
|
|
// Reports the LIVE mode rather than trusting the build. The package's chmod 600 cannot
|
|
// survive a zip round-trip or an FTP upload (neither carries unix modes), so the only
|
|
// honest place to check this is here, on the host. 0077 — NOT 0o077, which is PHP 8.1+
|
|
// syntax and would make this file fail to parse on the very old build we want to warn about.
|
|
'config.php 권한이 600' => [
|
|
is_readable($root . '/config.php')
|
|
&& (fileperms($root . '/config.php') & 0077) === 0,
|
|
'FTP 클라이언트에서 config.php 권한을 600 으로 바꾸세요. DB 비밀번호가 들어있습니다.',
|
|
],
|
|
'HTTPS 로 접속됨' => [
|
|
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
|
|| ($_SERVER['SERVER_PORT'] ?? '') === '443'
|
|
|| (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'),
|
|
'세션 쿠키가 cookie_secure=true 라서 HTTP 로는 로그인이 안 됩니다. 카페24에서 무료 SSL 을 켜세요.',
|
|
],
|
|
];
|
|
|
|
$failed = array_keys(array_filter($checks, fn($c) => !$c[0]));
|
|
?>
|
|
<!doctype html>
|
|
<meta charset="utf-8">
|
|
<title>move-detect 설치 점검</title>
|
|
<style>
|
|
body { font: 15px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif; max-width: 46rem; margin: 2rem auto; padding: 0 1rem; }
|
|
table { border-collapse: collapse; width: 100%; }
|
|
td { border-bottom: 1px solid #e5e5e5; padding: .5rem .4rem; vertical-align: top; }
|
|
td:first-child { white-space: nowrap; }
|
|
.ok { color: #0a7c2f; font-weight: 600; }
|
|
.no { color: #c02626; font-weight: 600; }
|
|
.fix { color: #666; font-size: 13px; }
|
|
.banner { padding: .75rem 1rem; border-radius: 6px; margin: 1rem 0; }
|
|
.good { background: #e8f6ec; }
|
|
.bad { background: #fdeaea; }
|
|
.warn { background: #fff6d8; border: 1px solid #e8cf7a; }
|
|
</style>
|
|
<h1>move-detect 설치 점검</h1>
|
|
|
|
<?php if ($failed): ?>
|
|
<p class="banner bad"><strong><?= count($failed) ?>개 항목 실패.</strong> 아래 빨간 줄의 안내를 먼저 해결하세요.</p>
|
|
<?php else: ?>
|
|
<p class="banner good"><strong>전부 통과.</strong> <a href="/login">/login</a> 으로 접속해 관리자 로그인을 확인하세요.</p>
|
|
<?php endif; ?>
|
|
|
|
<table>
|
|
<?php foreach ($checks as $name => [$ok, $fix]): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($name, ENT_QUOTES) ?></td>
|
|
<td class="<?= $ok ? 'ok' : 'no' ?>"><?= $ok ? 'OK' : '실패' ?></td>
|
|
<td class="fix"><?= $ok ? '' : htmlspecialchars($fix, ENT_QUOTES) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
|
|
<p class="banner warn">
|
|
<strong>확인이 끝나면 이 파일(<code>www/_preflight.php</code>)을 반드시 지우세요.</strong>
|
|
진단용이라 계속 열어둘 이유가 없습니다.
|
|
</p>
|