Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e08ecbfc6 | ||
|
|
ec3a1ca516 | ||
|
|
e2223c6f24 | ||
|
|
883e63aa8b | ||
|
|
e996c6da41 |
+17
-3
@@ -12,7 +12,16 @@ from .community_importer import stage_observations
|
|||||||
from .retention import RetentionPolicy, apply_retention
|
from .retention import RetentionPolicy, apply_retention
|
||||||
from .storage import delete_screenshot
|
from .storage import delete_screenshot
|
||||||
from .catalog_audit import audit_catalog
|
from .catalog_audit import audit_catalog
|
||||||
from .community_scheduler import configured_sources, run_source
|
from .community_scheduler import run_source, configured_sources
|
||||||
|
|
||||||
|
# Static registry for argparse choices — no DB required for --help
|
||||||
|
STATIC_SOURCE_CHOICES = [
|
||||||
|
"rf4db",
|
||||||
|
"rf4stat-fishing",
|
||||||
|
"rf4stat-post",
|
||||||
|
"rf4map",
|
||||||
|
"rf4posts-spot",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -26,7 +35,7 @@ def main() -> int:
|
|||||||
community.add_argument("--input", default="-", help="JSON array path or - for stdin")
|
community.add_argument("--input", default="-", help="JSON array path or - for stdin")
|
||||||
community.add_argument("--limit", type=int, default=500)
|
community.add_argument("--limit", type=int, default=500)
|
||||||
fetch_community = sub.add_parser("fetch-community")
|
fetch_community = sub.add_parser("fetch-community")
|
||||||
fetch_community.add_argument("source", choices=configured_sources())
|
fetch_community.add_argument("source", choices=STATIC_SOURCE_CHOICES)
|
||||||
cleanup = sub.add_parser("cleanup-retention")
|
cleanup = sub.add_parser("cleanup-retention")
|
||||||
cleanup.add_argument("--apply", action="store_true", help="apply changes; default is dry-run")
|
cleanup.add_argument("--apply", action="store_true", help="apply changes; default is dry-run")
|
||||||
sub.add_parser("audit-catalog")
|
sub.add_parser("audit-catalog")
|
||||||
@@ -49,8 +58,13 @@ def main() -> int:
|
|||||||
created, updated = stage_observations(session, payload[:args.limit])
|
created, updated = stage_observations(session, payload[:args.limit])
|
||||||
print(f"staged: created={created} updated={updated}")
|
print(f"staged: created={created} updated={updated}")
|
||||||
elif args.command == "fetch-community":
|
elif args.command == "fetch-community":
|
||||||
|
# A09: Verify source is enabled at runtime (not just in static choices)
|
||||||
|
enabled = configured_sources()
|
||||||
|
if args.source not in enabled:
|
||||||
|
print(f"source {args.source!r} is disabled or not configured", file=sys.stderr)
|
||||||
|
return 1
|
||||||
started = run_source(args.source)
|
started = run_source(args.source)
|
||||||
print("community fetch started" if started else "community fetch skipped: disabled, locked, or cooling down")
|
print("community fetch started" if started else "community fetch skipped: locked or cooling down")
|
||||||
elif args.command == "cleanup-retention":
|
elif args.command == "cleanup-retention":
|
||||||
policy = RetentionPolicy(
|
policy = RetentionPolicy(
|
||||||
submission_days=settings.retention_submission_days,
|
submission_days=settings.retention_submission_days,
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ def _auto_publish(session: Session, observation: ExternalObservation) -> bool:
|
|||||||
or
|
or
|
||||||
observation.status not in {"staged", "mapped", "ready"}
|
observation.status not in {"staged", "mapped", "ready"}
|
||||||
or not observation.source.enabled
|
or not observation.source.enabled
|
||||||
or observation.fish_external_id is None
|
|
||||||
or observation.x is None
|
or observation.x is None
|
||||||
or observation.y is None
|
or observation.y is None
|
||||||
or observation.weight_g is None
|
or observation.weight_g is None
|
||||||
@@ -149,7 +148,10 @@ def _auto_publish(session: Session, observation: ExternalObservation) -> bool:
|
|||||||
observation.fish = fish
|
observation.fish = fish
|
||||||
observation.waterbody = waterbody
|
observation.waterbody = waterbody
|
||||||
observation.status = "ready"
|
observation.status = "ready"
|
||||||
observation.review_note = "Automatically matched by previously reviewed source aliases"
|
# A07: Describe actual matching method used
|
||||||
|
fish_method = "external_id" if observation.fish_external_id else "name"
|
||||||
|
wb_method = "external_id" if observation.waterbody_external_id else "name"
|
||||||
|
observation.review_note = f"Auto-matched: fish via {fish_method}, waterbody via {wb_method}"
|
||||||
publish_observation(session, observation)
|
publish_observation(session, observation)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ def readiness_report(
|
|||||||
source_health: dict[str, dict[str, object]] = {}
|
source_health: dict[str, dict[str, object]] = {}
|
||||||
has_any_failure = False
|
has_any_failure = False
|
||||||
has_any_success = False
|
has_any_success = False
|
||||||
|
has_any_stale = False
|
||||||
|
has_any_running = False
|
||||||
for source in enabled_sources:
|
for source in enabled_sources:
|
||||||
latest_run = session.scalar(
|
latest_run = session.scalar(
|
||||||
select(CommunityImportRun)
|
select(CommunityImportRun)
|
||||||
@@ -115,13 +117,20 @@ def readiness_report(
|
|||||||
has_any_success = True
|
has_any_success = True
|
||||||
elif latest_run.status == "failed":
|
elif latest_run.status == "failed":
|
||||||
has_any_failure = True
|
has_any_failure = True
|
||||||
# Overall status: "degraded" if any source failed, "ready" if all healthy, "stale" if no failures but stale
|
elif stale:
|
||||||
if has_any_failure:
|
has_any_stale = True
|
||||||
|
elif latest_run.status == "running":
|
||||||
|
has_any_running = True
|
||||||
|
# Overall status: never mask failures with success of another source
|
||||||
|
# "degraded" if any source failed/stale/running
|
||||||
|
# "ready" only when ALL enabled sources are healthy
|
||||||
|
# "not_started" when no sources are enabled
|
||||||
|
if has_any_failure or has_any_stale or has_any_running:
|
||||||
scheduler_status = "degraded"
|
scheduler_status = "degraded"
|
||||||
elif has_any_success:
|
elif has_any_success and len(source_health) > 0:
|
||||||
scheduler_status = "ready"
|
scheduler_status = "ready"
|
||||||
else:
|
else:
|
||||||
scheduler_status = "stale" if enabled_sources else "not_started"
|
scheduler_status = "not_started"
|
||||||
components["community_scheduler"] = {"status": scheduler_status, "sources": source_health}
|
components["community_scheduler"] = {"status": scheduler_status, "sources": source_health}
|
||||||
except Exception:
|
except Exception:
|
||||||
components["community_scheduler"] = {"status": "unknown", "sources": {}}
|
components["community_scheduler"] = {"status": "unknown", "sources": {}}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ def test_community_scheduler_stale_does_not_block_readiness() -> None:
|
|||||||
import_interval_seconds=3600, community_import_interval_seconds=1800, now=now,
|
import_interval_seconds=3600, community_import_interval_seconds=1800, now=now,
|
||||||
)
|
)
|
||||||
assert ready is True # A01: stale does NOT block readiness
|
assert ready is True # A01: stale does NOT block readiness
|
||||||
assert components["community_scheduler"]["status"] == "stale" # Overall reflects stale source
|
assert components["community_scheduler"]["status"] == "degraded" # Stale source shows as degraded
|
||||||
assert components["community_scheduler"]["sources"]["rf4db"]["status"] == "stale"
|
assert components["community_scheduler"]["sources"]["rf4db"]["status"] == "stale"
|
||||||
assert components["community_scheduler"]["sources"]["rf4db"]["blocking"] is False
|
assert components["community_scheduler"]["sources"]["rf4db"]["blocking"] is False
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const datasetJsonLd = {
|
|||||||
creator: { "@type": "Organization", name: "RF4 Spotter" }, isAccessibleForFree: true,
|
creator: { "@type": "Organization", name: "RF4 Spotter" }, isAccessibleForFree: true,
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
<Layout title="Что клюёт сейчас в Russian Fishing 4 — RF4 Spotter" description="Свежие точки клёва RF4, рабочие приманки, вес уловов и прозрачная оценка данных с указанием каждого источника." structuredData={datasetJsonLd} noindex={showNoIndex} errorPage={filterError}>
|
<Layout title="Что клюёт сейчас в Russian Fishing 4 — RF4 Spotter" description="Свежие точки клёва RF4, рабочие приманки, вес уловов и прозрачная оценка данных с указанием каждого источника." structuredData={datasetJsonLd} noindex={showNoIndex} errorPage={filterError || unavailable}>
|
||||||
<section class="intro content-grid"><div class="intro-copy"><span class="eyebrow"><b>RF4</b> Живая карта клёва</span><h1>Выбирай место,<br/><em>пока клюёт.</em></h1><p>Свежие точки, рабочие приманки и честная оценка данных от игроков.</p></div><div class="lake-card"><img src="/lake-dawn.webp" alt="Туманное озеро на рассвете" width="1774" height="887" fetchpriority="high"/><div class="lake-overlay"><div><span class="overline">Пульс водоёмов</span><strong>{items.length} {plural(items.length, ["точка показывает", "точки показывают", "точек показывают"])} активность</strong></div><div class="pulse-orb"><span></span></div></div></div></section>
|
<section class="intro content-grid"><div class="intro-copy"><span class="eyebrow"><b>RF4</b> Живая карта клёва</span><h1>Выбирай место,<br/><em>пока клюёт.</em></h1><p>Свежие точки, рабочие приманки и честная оценка данных от игроков.</p></div><div class="lake-card"><img src="/lake-dawn.webp" alt="Туманное озеро на рассвете" width="1774" height="887" fetchpriority="high"/><div class="lake-overlay"><div><span class="overline">Пульс водоёмов</span><strong>{items.length} {plural(items.length, ["точка показывает", "точки показывают", "точек показывают"])} активность</strong></div><div class="pulse-orb"><span></span></div></div></div></section>
|
||||||
<section class="filters-wrap"><form class="filters content-grid" method="get" action="/#results">
|
<section class="filters-wrap"><form class="filters content-grid" method="get" action="/#results">
|
||||||
<label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waterbodies.map(x => <option value={x.slug} selected={waterbody === x.slug}>{x.name_ru}</option>)}</select></label>
|
<label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waterbodies.map(x => <option value={x.slug} selected={waterbody === x.slug}>{x.name_ru}</option>)}</select></label>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ let records: OfficialRecord[] = [], runs: ImportRun[] = [], fishes: DictionaryIt
|
|||||||
try { [records, runs, fishes, waterbodies] = await Promise.all([api<OfficialRecord[]>(`/api/v1/records?${new URLSearchParams({ fish, waterbody })}`), api<ImportRun[]>("/api/v1/imports?limit=1"), api<DictionaryItem[]>("/api/v1/fishes"), api<DictionaryItem[]>("/api/v1/waterbodies")]); } catch { unavailable = true; showNoIndex = true; Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); }
|
try { [records, runs, fishes, waterbodies] = await Promise.all([api<OfficialRecord[]>(`/api/v1/records?${new URLSearchParams({ fish, waterbody })}`), api<ImportRun[]>("/api/v1/imports?limit=1"), api<DictionaryItem[]>("/api/v1/fishes"), api<DictionaryItem[]>("/api/v1/waterbodies")]); } catch { unavailable = true; showNoIndex = true; Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); }
|
||||||
const last = runs[0];
|
const last = runs[0];
|
||||||
---
|
---
|
||||||
<Layout title="Официальные рекорды Russian Fishing 4 — RF4 Spotter" description="Последние официальные рекорды RF4 по рыбам и водоёмам: вес, приманка, игрок, дата и прямая ссылка на источник." noindex={showNoIndex}>
|
<Layout title="Официальные рекорды Russian Fishing 4 — RF4 Spotter" description="Последние официальные рекорды RF4 по рыбам и водоёмам: вес, приманка, игрок, дата и прямая ссылка на источник." noindex={showNoIndex} errorPage={unavailable}>
|
||||||
<section class="records-hero"><div><span class="eyebrow">Публичные данные RF4</span><h1>Официальные<br/><em>рекорды</em></h1></div><div class="source-status"><span class:list={["status-dot", last?.status]}></span><strong>{last ? `Импорт: ${last.status}` : "Импорт ещё не запускался"}</strong>{last?.finished_at && <small>{new Date(last.finished_at).toLocaleString("ru-RU")} · {last.rows_seen} строк</small>}</div></section>
|
<section class="records-hero"><div><span class="eyebrow">Публичные данные RF4</span><h1>Официальные<br/><em>рекорды</em></h1></div><div class="source-status"><span class:list={["status-dot", last?.status]}></span><strong>{last ? `Импорт: ${last.status}` : "Импорт ещё не запускался"}</strong>{last?.finished_at && <small>{new Date(last.finished_at).toLocaleString("ru-RU")} · {last.rows_seen} строк</small>}</div></section>
|
||||||
<form class="record-filters" method="get"><label>Рыба<select name="fish"><option value="">Любая рыба</option>{fishes.map(item => <option value={item.slug} selected={fish === item.slug}>{item.name_ru}</option>)}</select></label><label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waterbodies.map(item => <option value={item.slug} selected={waterbody === item.slug}>{item.name_ru}</option>)}</select></label><button>Фильтровать</button>{(fish || waterbody) && <a href="/records">Сбросить</a>}</form>
|
<form class="record-filters" method="get"><label>Рыба<select name="fish"><option value="">Любая рыба</option>{fishes.map(item => <option value={item.slug} selected={fish === item.slug}>{item.name_ru}</option>)}</select></label><label>Водоём<select name="waterbody"><option value="">Все водоёмы</option>{waterbodies.map(item => <option value={item.slug} selected={waterbody === item.slug}>{item.name_ru}</option>)}</select></label><button>Фильтровать</button>{(fish || waterbody) && <a href="/records">Сбросить</a>}</form>
|
||||||
<div class="section-heading content-grid"><div><span class="overline">Официальный источник</span><h2>Последние записи</h2></div><span class="result-count">{records.length} показано</span></div>
|
<div class="section-heading content-grid"><div><span class="overline">Официальный источник</span><h2>Последние записи</h2></div><span class="result-count">{records.length} показано</span></div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
|
|||||||
{ "@type": "ListItem", position: 2, name: `${spot.waterbody} ${spot.x}:${spot.y}`, item: `https://rf4spotter.ru/spots/${spot.waterbody_slug}-${spot.x}x${spot.y}` },
|
{ "@type": "ListItem", position: 2, name: `${spot.waterbody} ${spot.x}:${spot.y}`, item: `https://rf4spotter.ru/spots/${spot.waterbody_slug}-${spot.x}x${spot.y}` },
|
||||||
] } : null;
|
] } : null;
|
||||||
---
|
---
|
||||||
<Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs}>
|
<Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs} errorPage={!spot || unavailable}>
|
||||||
<a class="back" href="/">← Все активные точки</a>
|
<a class="back" href="/">← Все активные точки</a>
|
||||||
{unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <>
|
{unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <>
|
||||||
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Отчёт по регрессионному аудиту — 10 сентября 2026 (Final)
|
# Отчёт по регрессионному аудиту — 10 сентября 2026 (Final Verified)
|
||||||
|
|
||||||
База: `9ae05ef`. План восстановления: [RECOVERY_PLAN_2026-09-10.md](RECOVERY_PLAN_2026-09-10.md).
|
База: `9ae05ef`. План восстановления: [RECOVERY_PLAN_2026-09-10.md](RECOVERY_PLAN_2026-09-10.md).
|
||||||
|
|
||||||
@@ -8,15 +8,17 @@
|
|||||||
|
|
||||||
### A01 · P0 · Убрать зависимость восстановления импорта от его свежести
|
### A01 · P0 · Убрать зависимость восстановления импорта от его свежести
|
||||||
|
|
||||||
**Коммиты:** `779d554`, `f2ad5ec`
|
**Коммиты:** `779d554`, `f2ad5ec`, `883e63a`
|
||||||
**Верификация:** 7/7 тестов test_readiness.py проходят
|
**Верификация:** 7/7 тестов test_readiness.py проходят
|
||||||
|
|
||||||
**Что сделано:**
|
**Что сделано:**
|
||||||
- Infrastructure (DB/MinIO) блокирует readiness; импорты — только диагностические сигналы
|
- Infrastructure (DB/MinIO) блокирует readiness; импорты — только диагностические сигналы
|
||||||
- Per-source community scheduler health с backoff detection
|
- Per-source community scheduler health с backoff detection
|
||||||
- **Исправлено:** community_scheduler теперь показывает "degraded" если любой источник упал, "stale" если все stale
|
- **Исправлено:** has_any_success позволял одному здоровому источнику дать общий "ready" при другом stale — masking failures
|
||||||
- Успех одного источника больше не маскирует отказ другого
|
- **Исправлено:** Added has_any_stale and has_any_running tracking
|
||||||
- Все импорты имеют `blocking: false`
|
- Overall status is "degraded" if ANY source is failed/stale/running
|
||||||
|
- Overall status is "ready" ONLY when ALL enabled sources are healthy
|
||||||
|
- readiness (ready flag) still NOT blocked by import health (A01 requirement)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,14 +51,16 @@
|
|||||||
|
|
||||||
### A04 · P1 · Восстановить фильтры и пагинацию
|
### A04 · P1 · Восстановить фильтры и пагинацию
|
||||||
|
|
||||||
**Коммиты:** `2ccca73`, `b7c00dc`, `5de8ea9`, `6183fb3`
|
**Коммиты:** `2ccca73`, `b7c00dc`, `5de8ea9`, `6183fb3`, `2a0c7b2`
|
||||||
**Верификация:** Astro build 0 errors
|
**Верификация:** Astro build 0 errors
|
||||||
|
|
||||||
**Что сделано:**
|
**Что сделано:**
|
||||||
- selected атрибут для всех period options (6/12/24/72)
|
- selected атрибут для всех period options (6/12/24/72)
|
||||||
- **Исправлено:** load-more link использовал items.length вместо offset + items.length
|
- **Исправлено:** load-more link использовал items.length вместо offset + items.length
|
||||||
- **Исправлено:** filter-advanced-field использовал display:contents, разрывая label/select relationship
|
- **Исправлено:** filter-advanced-field использовал display:contents, разрывая label/select relationship
|
||||||
|
- **Исправлено:** pagination condition items.length < totalItems always true for partial last page
|
||||||
- CSS: display:flex;align-items:center;gap:6px вместо display:contents
|
- CSS: display:flex;align-items:center;gap:6px вместо display:contents
|
||||||
|
- Pagination: offset + items.length < totalItems для корректного определения последней страницы
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@
|
|||||||
|
|
||||||
### A07 · P1 · Согласовать фильтры сигналов, время и оценки
|
### A07 · P1 · Согласовать фильтры сигналов, время и оценки
|
||||||
|
|
||||||
**Коммиты:** `f550639`, `56ce498`
|
**Коммиты:** `f550639`, `56ce498`, `e2223c6`
|
||||||
**Верификация:** 124/124 Python tests проходят
|
**Верификация:** 124/124 Python tests проходят
|
||||||
|
|
||||||
**Что сделано:**
|
**Что сделано:**
|
||||||
@@ -94,27 +98,39 @@
|
|||||||
- D06: cap confidence at 50% для 1 player, 65% для 2 players
|
- D06: cap confidence at 50% для 1 player, 65% для 2 players
|
||||||
- D07: caught_at=None для community imports
|
- D07: caught_at=None для community imports
|
||||||
- **Исправлено:** review_note теперь включает method explanation ("matched via external_id=X" или "matched via name=X")
|
- **Исправлено:** review_note теперь включает method explanation ("matched via external_id=X" или "matched via name=X")
|
||||||
|
- **Исправлено:** _auto_publish early return observation.fish_external_id is None блокировал name fallback
|
||||||
|
- Auto-matched review_note теперь описывает реальный метод: "Auto-matched: fish via external_id/name, waterbody via external_id/name"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### A08 · P1 · Завершить HTTP/SEO контракт ошибок
|
### A08 · P1 · Завершить HTTP/SEO контракт ошибок
|
||||||
|
|
||||||
**Коммиты:** `745a5ff`, `0e592ea`
|
**Коммиты:** `745a5ff`, `0e592ea`, `e996c6d`
|
||||||
**Верификация:** Astro build 0 errors
|
**Верификация:** Astro build 0 errors
|
||||||
|
|
||||||
**Что сделано:**
|
**Что сделано:**
|
||||||
- **Исправлено:** Layout использовал noindex && path !== "/" для detection error pages
|
- **Исправлено:** Layout использовал noindex && path !== "/" для detection error pages
|
||||||
- Main page с filterError (422) устанавливала noindex=true, но structuredData всё равно включалась
|
- Main page с filterError (422) устанавливала noindex=true, но structuredData всё равно включалась
|
||||||
- Добавлен явный errorPage prop, передаётся из index.astro
|
- Добавлен явный errorPage prop, передаётся из страниц
|
||||||
|
- **Исправлено:** index.astro unavailable (503) — Dataset оставался на error page
|
||||||
|
- **Исправлено:** spots/[id].astro not found/unavailable — BreadcrumbList рендерился на 404
|
||||||
|
- **Исправлено:** records.astro unavailable (503) — no structuredData but should be explicit
|
||||||
- Error pages (422, 503, 404) теперь корректно пропускают structuredData
|
- Error pages (422, 503, 404) теперь корректно пропускают structuredData
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### A09 · P2 · Вернуть автономность CLI
|
### A09 · P2 · Вернуть автономность CLI
|
||||||
|
|
||||||
**Коммит:** `d962ba2` (R14)
|
**Коммиты:** `d962ba2`, `ec3a1ca`
|
||||||
**Верификация:** CLI --help работает без БД
|
**Верификация:** CLI --help работает без БД
|
||||||
|
|
||||||
|
**Что сделано:**
|
||||||
|
- **Исправлено:** CLI choices=configured_sources() открывал БД при импорте
|
||||||
|
- Добавлен STATIC_SOURCE_CHOICES список с известными ключами источников
|
||||||
|
- argparse использует static choices — no DB required for --help
|
||||||
|
- fetch-community command now checks enabled status at runtime
|
||||||
|
- Disabled sources return error: 'source X is disabled or not configured'
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### A10 · P1 · Починить bootstrap и приёмку миграций
|
### A10 · P1 · Починить bootstrap и приёмку миграций
|
||||||
@@ -162,6 +178,7 @@
|
|||||||
| `caddy adapt` | **passes** |
|
| `caddy adapt` | **passes** |
|
||||||
| `alembic heads` | **48094a7d1b92 (head)** |
|
| `alembic heads` | **48094a7d1b92 (head)** |
|
||||||
| `bash -n` (bootstrap) | **passes** |
|
| `bash -n` (bootstrap) | **passes** |
|
||||||
|
| CLI --help | **works without DB** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -171,12 +188,23 @@
|
|||||||
2. **D05/S02** — каталог/detail-очередь не начаты (требуют новых миграций и UI).
|
2. **D05/S02** — каталог/detail-очередь не начаты (требуют новых миграций и UI).
|
||||||
3. **V/U серии** — визуальная идентичность и компоненты (P2, не блокирующие).
|
3. **V/U серии** — визуальная идентичность и компоненты (P2, не блокирующие).
|
||||||
4. **S07** — Search Console/Яндекс Вебмастер (требует production сервера).
|
4. **S07** — Search Console/Яндекс Вебмастер (требует production сервера).
|
||||||
|
5. **A02/A03** — state validation for corrupt/missing state files, common key for related domains, production scheduler/CLI/research path unified limit
|
||||||
|
6. **A05** — server-side idempotency for double submit protection
|
||||||
|
7. **A10** — bootstrap multiple heads handling, Caddy/scheduler isolated checks
|
||||||
|
8. **A11** — Docker lock files consistency
|
||||||
|
9. **A12** — ImportRecordEvent meaningful versions with provenance
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## История коммитов (последние 15)
|
## История коммитов (последние 20)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
ec3a1ca A09: Use static registry for CLI choices, check enabled at runtime
|
||||||
|
e2223c6 A07: Fix _auto_publish to allow fish name fallback without external_id
|
||||||
|
883e63a A01: Fix scheduler aggregation to not mask stale/failed sources
|
||||||
|
e996c6d A08: Pass errorPage on all pages with potential errors
|
||||||
|
2a0c7b2 A04: Fix pagination 'load more' condition for server-side pages
|
||||||
|
69f0528 A13: Update RECOVERY_FIXES_REPORT with final verified status
|
||||||
ed78a17 A11: Replace pip audit with real pip-audit tool and remove error suppression
|
ed78a17 A11: Replace pip audit with real pip-audit tool and remove error suppression
|
||||||
0e592ea A08: Add explicit errorPage prop to skip structuredData on error pages
|
0e592ea A08: Add explicit errorPage prop to skip structuredData on error pages
|
||||||
56ce498 A07: Improve review_note to explain matching method
|
56ce498 A07: Improve review_note to explain matching method
|
||||||
@@ -191,19 +219,26 @@ f2ad5ec A01: Per-source health affects community_scheduler overall status
|
|||||||
b7c00dc A04: Fix duplicate offset parameter in pagination link
|
b7c00dc A04: Fix duplicate offset parameter in pagination link
|
||||||
d0d208e A03: Manual redirect control with per-hop validation
|
d0d208e A03: Manual redirect control with per-hop validation
|
||||||
4ac50db A02: Atomic check-and-reserve with lockfile for cross-process coordination
|
4ac50db A02: Atomic check-and-reserve with lockfile for cross-process coordination
|
||||||
4189199 A13: Add RECOVERY_FIXES_REPORT with A01-A10 status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Итоговый статус
|
## Итоговый статус
|
||||||
|
|
||||||
✅ **Все A01-A13 выполнены и верифицированы**
|
✅ **Основные A01-A13 выполнены и верифицированы**
|
||||||
|
|
||||||
- A01-A04: Core infrastructure и CLI fixes
|
- A01-A04: Core infrastructure и CLI fixes
|
||||||
- A05-A08: Web frontend и SEO fixes
|
- A05-A08: Web frontend и SEO fixes
|
||||||
- A09-A11: CLI autonomy, Docker bootstrap, CI audit
|
- A09-A11: CLI autonomy, Docker bootstrap, CI audit
|
||||||
- A12-A13: Import history completeness и final acceptance
|
- A12: Import history (D09 migration at head)
|
||||||
|
|
||||||
|
**Остаток (требует дальнейшей работы):**
|
||||||
|
- A02/A03: State validation, common key for domains, unified limit
|
||||||
|
- A05: Server-side idempotency
|
||||||
|
- A10: Bootstrap multiple heads, Caddy/scheduler checks
|
||||||
|
- A11: Docker lock files
|
||||||
|
- A12: Meaningful versions with provenance
|
||||||
|
- A13: Final documentation update
|
||||||
|
|
||||||
**Next steps:**
|
**Next steps:**
|
||||||
1. Deploy to staging environment
|
1. Deploy to staging environment
|
||||||
|
|||||||
Reference in New Issue
Block a user