Files
vtube/docs/ADS.md

413 lines
28 KiB
Markdown
Raw Normal View History

# tubev — Ads Integration
Реклама через **adspyglass.com**. Основне джерело монетизації — балансуємо CTR vs UX/PSI.
## Status
🟢 **Working knowledge** — Skeleton наповнюємо.
## ⚙️ Automated regression test — `ad-test`
Швидкий smoke перевірити що ad flow не поламано після зміни.
```bash
# default — 3 URLs з 8148 (швидкий smoke)
ad-test
# конкретні URLs
ad-test "https://www.xn--3dsq7teoyo9d.com/v/box/123/foo" \
"https://t1.atube.sex/v/box/456/bar"
# з screenshots + JSON report
ad-test --screenshot --json "https://..." "https://..."
```
**Що перевіряє per URL:**
1. ✓ ad-bundle loaded (`window._adConfig` set, spot IDs читаються)
2. ✓ popunder SDK loads
3.`/api/users/<vast_spot>` повертає `<Ad>` (не empty no-fill)
4. ✓ mode = "vast" після popunder cooldown
5.`.asg-container` injects на click `pjs_play_btn` (правильний `data-spot-id`)
6. ✓ console clean (no `[ASGB LOADER]` errors, no `pageerror`)
**Exit code:** 0 = всі pass, 1 = ≥1 fail. Можна chain у CI / git hook.
**Реалізація:**
- Wrapper: `/home/w4/bin/ad-test`
- Script: `/home/w4/playwright-tests/ad-regression.mjs` (Playwright + Chromium headless)
- Repo copy (versioned): `scripts/ad-regression.mjs` + `scripts/ad-test.sh`
**Обмеження headless:**
- Real video playback не verifies (autoplay restrictions, VPAID не fully supported у chromium-headless-shell)
- Network-level VAST chain validation (через `<Ad>/<InLine>/<Wrapper>` детекцію) cover 95% regressions без візуальної перевірки
- Якщо треба видеоплей — `ad-test --full` (full chromium, але VPAID все ще incomplete)
- Альтернатива для full visual: запусти у real browser з `?debug=1` + дивись HUD
## Architecture
### Mirrors (зеркала)
Реклама вантажиться через **зеркала-домени**, не прямо `adspyglass.com`:
- `a5.g--o.info` — ad orchestration (preconnect target у modern setup)
- `surstrom.com` — inline ad-spot loader (legacy pattern)
- `agego.com` — verifycdn (`verifycdn.agego.com/v1/verify.js`)
- `g--o.info` — base domain
- (інші додаються на потребу)
**Чому зеркала:** anti-block — adblockers баняють відомі ad-домени. Зеркала маскують та ротуються.
**Mirror lifecycle:**
- Зеркала **періодично баняться** (adblock list updates)
- Заміна — **mass-operation через Адміна** (одразу на всі сайти)
- Ми не керуємо вибором зеркал — це адмінська функція
### Three architecture patterns coexist (audit 2026-05-02)
Site-level audit показав **3 patterns** — не uniform:
| Pattern | Sites | Як виглядає | Примітка |
|---------|-------|-------------|----------|
| **A) Modern bundle** | 1 (тільки 8148) | `<script src="/static/js/ad-bundle.min.js?v=<md5>" defer>` + `views/modules/banners/*adspy*.etlua` partials | Canonical — є build pipeline, source JS у `views/static/js/` |
| **B) Modern partials** | ~23 | `preconnect``a5.g--o.info` + adspy `.etlua` partials, але **без** ad-bundle.min.js | Modules orchestrate, no central bundle |
| **C) Legacy inline** | 31 (8112-style) | Inline `<script async src="//surstrom.com/<random>.js">` + iframes до `surstrom.com/api/spots/<id>` + `tb.load_frame_baner_v2()` API | Pre-modular epoch |
**77 із 94 сайтів** мають adspy banner modules у `views/modules/banners/*adspy*.etlua`. Решта (17) — legacy без banner partials або special structure.
### Build pipeline (тільки 8148)
`/home/nosfortube/frontend_8148/views/static/js/build-ad-bundle.sh` концатує+minify-їть 5 source JS-файлів у `ad-bundle.min.js`:
1. `ad-config.js` — config / placement IDs / mirror domains
2. `ad-core.js` — ядро ad-orchestration
3. `ad-mute.js` — mute / autoplay control
4. `vast-preroll.js` — pre-roll реклама перед video
5. `ad-bootstrap.js` — entry-point / init
**Source files:** усі у `/home/nosfortube/frontend_8148/views/static/js/` (унікально для 8148).
Tool: **terser** (concat + minify). Triggered by `~/git-save-all.sh` коли source новіший за bundle.
Cache-bust: md5 → `?v=...` у `layout.etlua` (детально у [DEPLOY.md](DEPLOY.md#cache-bust-механізм)).
### Banner partial loader — `tb.load_frame_baner_v2`
Source: `views/static/js/lib/common/js/tbanner.etlua` (shared lib з кешуючою етлуа-обгорткою) + `tbanner_min.etlua` (minified inline). Підключається у layout.etlua через `<% render("static.js.lib.common.js.tbanner_min") %>`.
**Сигнатура:** `tb.load_frame_baner_v2(spot_url, dom_target, timeouts, attrs [, callback])`
- `spot_url``//a5.g--o.info/api/spots/<spot_id>?p=1`
- `dom_target``#selector` куди append iframe
- `timeouts``{event_min_timeout, event_max_timeout}` ms
- `attrs``{height, width, ...}` iframe attributes / style
- `callback` — викликається після append (для native — інсталяція postMessage listener-у)
**Lazy-load logic:**
- Чекає user event (`scroll`, `mousemove`, `touchstart`, `resize`, `mouseenter`, `click`) АБО `event_min_timeout` (раніше)
- Якщо tab hidden → відкладає до `visibilitychange`
- `event_max_timeout` — fallback hard limit
- Counter per banner — окрема черга для кожного instance
- iframe attrs default: `sandbox="allow-scripts allow-popups allow-forms allow-same-origin"`, `loading="lazy"`, `class="na"`
### Native banners — auto-height через postMessage
Native (`tbn1`, `tbn2`, `tbn3`) — adaptive ads з невідомою висотою. Pattern:
1. **На стороні ASG** користувач прописує код банера (TrafficStars або ExoClick) + кастом-CSS для responsive grid + кастом-JS який postMessage-ить розмір.
2. **На стороні сервера** ASG обертає у iframe (через `/api/spots/<id>` endpoint).
3. **Frontend** ловить `window.message` event від iframe → парсить JSON `{url, height}` → встановлює `height` attribute на iframe з `transition: all .3s`.
**Network providers за ASG (приклад інтеграції на adспy admin side):**
- **TrafficStars** — `cdn.tsyndicate.com/sdk/v1/n.js` + `NativeAd({element_id, spot, type: "label-under", cols:4, rows:1, ...})`
- **ExoClick** — `a.magsrv.com/ad-provider.js` + `<ins class="eas..." data-zoneid="..." data-keywords="%KW%" data-sub="%SUB1%"></ins>` + `(AdProvider).push({"serve":{}})`
Обидва вкладають кастомний `<style>` з 4-col responsive grid (≥739px: 4 cols, 450-738: 2, <450: 1). Плюс postMessage скрипт що шле parent розмір на load/resize/mutation.
ASG керує цими SDK runtime-no — ми бачимо тільки iframe.
### Spot ID location matrix (8148 — 16 spots)
| Тип | Файл | Spot ID location |
|-----|------|-----------------|
| popunder | `views/static/js/ad-config.js` | `popunder.spot` |
| VAST/in-video | `views/static/js/ad-config.js` | `vast.spot` |
| popunder cooldown matcher | `views/layout.etlua` | inline regex `(?:^|\|)<id>=` (must sync з ad-config) |
| desktop footer ×4 | `views/modules/banners/footer_descktop_adspy.etlua` | hardcoded URL `spots/<id>?p=1` |
| mobile footer/header/middle | `views/modules/banners/{footer,header,middle}_mobile_adspy.etlua` | same |
| desktop sidebar ×2 | `views/modules/banners/embed_sidebar_adspy.etlua` | same |
| mobile sidebar ×2 | `views/modules/banners/embed_mobile_sidebar_adspy.etlua` | same |
| native (3 partials) | `views/modules/banners/native_*adspy.etlua` | same |
**При зміні spot IDs (rotation / re-creation):** оновити ВСІ перелічені місця, потім `bash /home/w4/git-save-all.sh "msg"` — auto-rebuild bundle + cache-bust.
⚠️ **Не забути `layout.etlua` cooldown regex** — окрема hardcoded reference на popunder spot ID (для localStorage `asgsl` cookie matching).
### Native 1thumb_a..f — single-banner-per-thumb pattern
`views/modules/banners/native_allpg_1thumb_{a,b,c,d,e,f}.etlua`**6 окремих banner модулів**, кожен:
- ОДИН `<div id="thmb<N>">` (унікальний DOM ID `#thmb1``#thmb6`)
- ОДИН spot URL `//a5.g--o.info/api/spots/<id>?p=1`
- script `tb.load_frame_baner_v2(spotURL, "#thmb<N>", ...)` injects iframe всередину
**Призначення:** заміняти thumbnail у grid-листингах (`cat_list_thumb`, `video.etlua`, `related_video`, `recommended_video`) на native banner imp що виглядає як thumb.
**Render умова:** if `i == 8 and not isMobile()` (desktop) **OR** `i == 5 and isMobile()` (mobile) — позиція у grid де banner injects.
⚠️ **Унікальність DOM ID per page:** один render = один module = один `#thmbN`. Якщо рендериш 1thumb_a у двох різних модулях (наприклад related_video AND recommended_video) на одній сторінці — отримаєш **DUPLICATE `#thmb1`** (HTML5 violation, ad iframe injects only into first match). Fix: використовуй РІЗНІ модулі (`1thumb_a` + `1thumb_b`) у різних блоках.
### Per-site spot inventory
#### 8148 (xn--3dsq7teoyo9d.com — pilot site)
Канонічний reference. Spot IDs у git history; перелік типів — у matrix вище.
#### 8161 (adultmovz.com)
Migrated 2026-05-03 (videojs4 → PlayerJS+ad-bundle). URL pattern `/v-arch/`.
| Spot ID | Тип | DOM target | File |
|---------|-----|------------|------|
| **514125** | popunder | — | `static/js/ad-config.js` + cooldown regex у `layout.etlua` |
| **514120** | VAST in-video | `pjs_play_btn` click | `static/js/ad-config.js` |
| 514116 | desktop footer A | `#da_a` | `modules/banners/footer_descktop_adspy.etlua` |
| 514117 | desktop footer B | `#da_b` | same |
| 514118 | desktop footer C | `#da_c` | same |
| 514119 | desktop footer D | `#da_d` | same |
| 514123 | mobile header | `#hdm` | `modules/banners/header_mobile_adspy.etlua` |
| 514124 | mobile footer | `#ftm` | `modules/banners/footer_mobile_adspy.etlua` |
| 514138 | mobile middle | `#mdm` | `modules/banners/middle_mobile_adspy.etlua` |
| 514121 | desktop sidebar A | `#sdd_a` | `modules/banners/embed_sidebar_adspy.etlua` |
| 514122 | desktop sidebar B | `#sdd_b` | same |
| 514139 | mobile sidebar A | `#dsd_a` | `modules/banners/embed_mobile_sidebar_adspy.etlua` |
| 514140 | mobile sidebar B | `#dsd_b` | same |
| 514295 | native desktop allpg | `#tbn1` | `modules/banners/native_allpg_desktop_adspy.etlua` |
| 514297 | native mobile allpg | `#tbn2` | `modules/banners/native_allpg_mobile_adspy.etlua` |
| 514296 | native embed | `#tbn3` | `modules/banners/native_embed_adspy.etlua` |
| 514371 | native 1thumb A | `#thmb1` | `modules/banners/native_allpg_1thumb_a.etlua` (use: related_video, cat_list_thumb, video) |
| 514372 | native 1thumb B | `#thmb2` | `1thumb_b.etlua` (use: recommended_video, cat_list_thumb, video) |
| 514373 | native 1thumb C | `#thmb3` | `1thumb_c.etlua` (cat_list_thumb, video) |
| 514374 | native 1thumb D | `#thmb4` | `1thumb_d.etlua` (cat_list_thumb, video) |
| 514375 | native 1thumb E | `#thmb5` | `1thumb_e.etlua` (cat_list_thumb, video) |
| 514376 | native 1thumb F | `#thmb6` | `1thumb_f.etlua` (cat_list_thumb, video) |
## Migration checklist — legacy (videojs4) → modern (PlayerJS + ad-bundle)
Контрольні чекпоінти при міграції наступного site (template після 8148/8161 досвіду). Кожен ✓ — окремий atomic commit.
### Pre-flight
- [ ] **Identify URL pattern.** `/v/` (default), `/v-arch/`, `/video/`, etc. Це йде у `ad-bootstrap.js` path checks (2 occurrences) і у тестових URL-ах.
- [ ] **Get spot IDs from ASG admin.** Мінімум 16 spots (popunder + VAST + 4 desktop footer + 1 mobile header + 1 mobile footer + 1 mobile middle + 2 desktop sidebar + 2 mobile sidebar + 3 native + опціонально 6 1thumb_a-f). Записати mapping.
- [ ] **Verify ASG spot config:** **frequency capping ENABLED** на popunder + VAST (інакше cooldown поломається — див. `## ⚠️ ASG spot config gotcha` вище).
### Banner modules
- [ ] **Replace banner spot IDs** у всіх `views/modules/banners/*adspy*.etlua` + `1thumb_*.etlua` (per inventory). Sed-batch friendly.
- [ ] **Verify zero stale IDs** після replace: `grep -r '<old_id>' views/` повинен returnати empty (test/comments OK).
### JS bundle (port from 8148)
- [ ] **Copy 5 source files**`views/static/js/`: `ad-config.js`, `ad-bootstrap.js`, `ad-core.js`, `ad-mute.js`, `vast-preroll.js` + `build-ad-bundle.sh`.
- [ ] **Edit `ad-config.js`:** set `popunder.spot`, `vast.spot`, `_adConfigBuildId` (унікальний marker для cache-bust verify).
- [ ] **Edit `ad-bootstrap.js`:** додати site URL pattern до **обох** path checks (HUD interval + `_adCtx` init), напр. `||location.pathname.indexOf('/v-arch/')===0`.
- [ ] **Build bundle:** `bash views/static/js/build-ad-bundle.sh` (concat + terser).
### layout.etlua
- [ ] **Add `class="no-js"` на `<html>`** + inline script `<head>` swap до `.js` (для iOS Safari `.js .vi-limiter video::-webkit-media-controls{display:none}` selector).
- [ ] **Add `<link rel="preconnect" href="https://a5.g--o.info" crossorigin>`.**
- [ ] **Add `_adSel` inline config** у `mysettings.location_css == "id"` block з selector mapping (native classes — `vi-limiter` у 8161, `vdo-blk-lmtr` у 8148).
- [ ] **Tabunder pre-detect script** з popunder spot ID regex `(?:^|\|)<popunder_id>=` (синхронізовано з ad-config!).
- [ ] **`<script src="/static/js/ad-bundle.min.js?v=<md5>" defer></script>`** з cache-bust md5.
- [ ] **Inline `lazyLoadFunc` definition**`views/static/js/lazysizes.min.js` content) ПІСЛЯ inline lazysizes core block — інакше `timeline-pjs.min.js` кине `ReferenceError: lazyLoadFunc is not defined` (8161 fix 2026-05-04).
- [ ] **Body inline scripts** (port 8148 lines 184-434): `_initPjs()`, `_revealPjs()`, `_playAt()`, click handler на `.<lmtr_class>` capture phase, VAST overlay click forwarder.
- [ ] **Guard counter calls:** `tb.start_events_v2(() => { if(typeof c==="function") c(<%-video.video_id%>,0,"click",0); }, ...)``c()` defined у async-loaded `counters.v2.min.js`, без guard race-error (`c is not defined`).
### id_index.etlua
- [ ] **Replace videojs4 DOM** з PlayerJS structure: `<div class="<lmtr_class> is-loading"><div class="<vdo_class> js-cleanVideo" id="pjs_container">` + `<video>` + `<div id="pjs_poster">` + `<img id="pjs_poster_img">` + `<div id="pjs_play_btn">` SVG + `<div id="pjs_loader">` SVG.
- [ ] **Use site-NATIVE classes** (не copy 8148 `vdo-blk-lmtr`/`vdo-blk-vdo`). Map до існуючих сайту: 8161 → `vi-limiter`/`vi-player`. Foreign classes створюють **footprint** і CSS дублі.
- [ ] **Inline styles target NATIVE classes** теж (`<style>.vi-limiter ...</style>`). Без дзеркал sed.
- [ ] **Timeline click handler:** `<a onClick="_playAt(_SECOND_)">` (НЕ legacy `player.setTime`).
- [ ] **Poster touch handler script:** localStorage `_pw` mark на `touchstart`/`mousedown` для tabunder pre-detect.
### CSS site-side (адаптивні правки)
- [ ] **Tooltip overlap:** `.<actions>{position:relative;z-index:5001}` — підняти actions row над `.asg-container{z-index:5000}`. Plus `.<msg-class>{z-index:10000}` як defense.
- [ ] **Timeline thumb hover-play:** `.<thumbs-block> .<item>.<can-play> a:hover .<img-class>:after{display:block}` + mobile `@media(max-width:768px){.<thumbs> ... :after{transform:translate(-50%,-50%) scale(.6)}}`.
- [ ] **Title wrap (`.heading-2` truncation):** `.<title-head> .<heading-2>{white-space:normal;overflow-wrap:anywhere;...}` — override будь-який single-line ellipsis.
- [ ] **`.<thumbs-block>` mobile scaling** — adapt 8148 patterns до native classes site-у.
### Verification
- [ ] **`ad-test "<test_url>"`** → 6/6 PASS.
- [ ] **Empirical Playwright probe:** capture `pageerror` array — має бути empty. Перевірити specifically `c is not defined`, `lazyLoadFunc is not defined`, `c2.min.js Unexpected identifier 'o'`.
- [ ] **Validate `c2.min.js` syntax:** `node --check views/static/js/c2.min.js` — passing. Якщо source `c2.js` edited без re-minify → broken `let` declarations у comma-sequence; fix: `terser views/static/js/c2.js -c -m -o views/static/js/c2.min.js`.
- [ ] **Banner DOM uniqueness:** `await page.evaluate(() => Array.from(document.querySelectorAll('iframe[src*="api/spots"]')))` collect → no duplicate parent IDs (`#thmb1` 2× = bug).
- [ ] **VAST endpoint probe:** `curl 'https://a5.g--o.info/api/users/<vast_spot>?v2=1&fill=0&url=<page-url>'` returns `<Ad>` (не empty `<VAST/>` — no-fill).
- [ ] **Popunder endpoint probe:** `curl 'https://a5.g--o.info/api/users/<popunder_spot>?host=<domain>&...'` returns `window.__NA.renderSpot({...})`.
- [ ] **t1 CDN serves fresh bundle:** `curl -sL "https://t1.<domain>/static/js/ad-bundle.min.js" | grep -oE 'spot:"[0-9]+"'` — bundle reflects новий config.
- [ ] **Real-browser smoke:** click play → VAST shows (after popunder cooldown set), click like → tooltip opens above player chrome, hover related thumb → play icon, hover timeline scene → smaller play icon (mobile) / regular (desktop).
### Post-deploy
- [ ] **Monitor adspyglass dashboard** після prod deploy — graph має продовжувати ріст; падіння = регресія.
- [ ] **Update site row у [SITES.md](SITES.md)** з generation tag (`v3 PlayerJS`).
- [ ] **Update memory `project_player_roadmap.md`** — додати site до v3 list.
## ASG SDK filename patterns (anti-footprint)
`a5.g--o.info/<filename>.js` server tolerates **будь-який filename matching pattern per type** — content однаковий. Use unique per site щоб diluteти footprint (adblock fingerprints).
**Patterns** (verified 2026-05-05):
- **POPUNDER**: 7 chars total, `^[A-Za-z0-9]{6}[0-9]$` (last char digit)
- **VAST**: 7 chars total, `^[A-Za-z0-9]{6}[a-hA-H]$` (last char `a-h` case-ins; letters i+ → 404)
- **BANNER `tb_config.banner_source`**: локальний файл у `views/static/js/<name>.js` per site, не ASG endpoint — окремий footprint layer (rename файла + update reference)
**Workflow:** generate per-site filenames → verify via `curl -sI` → apply mass-replace у `ad-config.js` (popunder.sdk, vast.sdk) → rebuild bundle → update `?v=<md5>` cache-bust у layout.
Detail: memory `reference_ad_filename_patterns` (15-site mapping applied 2026-05-05).
## Monitoring
### adspyglass dashboard — best diagnostic signal
`https://app.adspyglass.com/dashboard`
**Чому це best signal:**
- Live графік показу реклами per site
- **Падіння графіка** = реклама не показується = ймовірно поломка (frontend/template/JS error / banned mirror / CDN cache stale)
- **Ріст** = трафік підріс або UX покращився
Юзер каже: "більший показник саме для мене це лайв графік реклами від АСГ" — реклама падає миттєво коли щось не так, ще до того як прийдуть алерти.
### Інші alerts
- **Telegram + email** — критичні (downtime, errors)
- Billing dashboard `https://billing.g--o.info/cs/oursites/dashboard/` — статистика (зазвичай не дивимось)
### Template patterns
**Pattern A (8148 modern):**
```etlua
<link rel="preconnect" href="https://a5.g--o.info" crossorigin>
<script src="/static/js/ad-bundle.min.js?v=<%= ad_bundle_v %>" defer></script>
...
<% render("views.modules.banners.header_mobile_adspy") %>
<% render("views.modules.banners.footer_mobile_adspy") %>
<% render("views.modules.banners.footer_descktop_adspy") %>
```
**Pattern C (8112 legacy inline):**
```etlua
<link rel="dns-prefetch" href="//surstrom.com">
...
<script async src="//surstrom.com/qDap9.js"></script>
<iframe class="na" src="//surstrom.com/api/spots/72437?p=1" sandbox="..." loading="lazy"></iframe>
...
tb.load_frame_baner_v2("//surstrom.com/api/spots/72437?p=1","#tb0",{...},{...});
```
## Що треба з'ясувати + задокументувати
### Architecture migration
- [ ] Чи планується мігрувати pattern C → B/A (legacy → modern)?
- [ ] 8148 — pilot для bundle-architecture, чи поширюватиметься?
### CSP / Security
- [ ] CSP exceptions для зеркал — як підтримуються?
- [ ] Mirror swap — чи треба update CSP при заміні?
### Performance impact
- [ ] PSI вплив реклами — % LCP / TBT (per pattern)?
- [ ] Lazy-load ads below-fold — реалізовано (видно `loading="lazy"` на iframes)?
## VAST flow (8148 detailed)
### User experience
1. Користувач клацає play на `pjs_play_btn` (PlayerJS player)
2. `asgInVideoImpression` event від ASG SDK → `_decideAndApply` ловить → запускає `VastPreroll.show()`
3. ASG іnject-ить `.asg-container` з video preroll
4. `MutationObserver` ловить container → pin pointer + z-index 5000, добавляє progress bar (`.vast-progress` yellow #ffd000)
5. Coordinated mute: video у container синхронізується з `_vastMuted` localStorage (per-tab persist)
6. Після VAST done (video.ended OR `currentTime >= duration - 0.2`) → callback `window._asgAdDone()` → main video starts via `window._revealPjs()`
7. `enableMediaPlayHijack: true` блокує race у HTMLMediaElement.play() якщо `_adsLocked` — попереджає 1-frame audio artifact коли VAST стартує над main video
### Cooldown management
- ASG локальне сховище: `localStorage.asgsl` — pipe-separated `<spot_id>=<key>:<value>` записи. Ключі що використовуються нашим bundle:
- `global_rr:<timestamp>``_readCooldowns` parse-ить це як cooldown end time
- `n:<timestamp>` — impression handler копіює це у `_popRr` / `_vastRr` cookies+LS
- Дублюючі cookies: `_popRr`, `_vastRr`. Реплікуються з asgsl при impression event.
- Декрипт пам'яті: дві ключові події з ASG — `asgPopunderImpression`, `asgInVideoImpression` — тригерять запис cooldown.
### ⚠️ ASG spot config gotcha — frequency-capping mandatory
**Bug 2026-05-02 (8148, after account recreation):** popunder spot 514208 у новому ASG account-і був налаштований з `shows_limit:1` без frequency-capping rotation. Симптоми:
- popunder СПрацьовує (видно `asgPopunderImpression`)
- Але `asgsl` локально вигляд: `<spot>=keep_looping:false,tabunder:false,uuid:...,noloop:true,shows_limit:1`
- **Нема `n:<timestamp>`** → impression handler виходить без write `_popRr`
- **Нема `global_rr:<timestamp>`** → `_readCooldowns` повертає `popActive=false`
- → mode завжди "pop" → popunder fires кожен pageload → VAST SDK ніколи не loadиться
**Fix у ASG admin** (на стороні юзера, не у нас):
- Відкрити spot config → знайти "Global rotation" / "Frequency capping" / "Cooldown" / "Time-based capping" / "Show frequency"
- Встановити, наприклад, "once per 2 hours per user" — це додасть `n:` / `global_rr:` поля в asgsl
- **Те саме для VAST spot** — інакше після популярного pop firing, VAST mode triggered але cooldown поломаний
**Як перевірити:** після popunder fire відкрий DevTools → `localStorage.getItem('asgsl')` → шукай `global_rr:` або `n:`. Якщо нема → не налаштовано.
### ⚠️ ASG no-fill diagnostic — VAST returns empty `<VAST/>`
**Bug 2026-05-02 (8148, VAST never visible):** Mode перемикався у "vast", VAST SDK loaded, але реклама не з'являлась візуально.
Перевірка endpoint напряму:
```bash
curl 'https://a5.g--o.info/api/users/<vast_spot>?v2=1&fill=0&url=<page-url>'
```
Якщо response:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<VAST version="3.0">
</VAST>
```
— це **VAST no-fill**. ASG не має inventory для цього spot.
**Порівняй з popunder endpoint** який повертає повний JSON-like config з `creative_id`, `behavior`, тощо:
```js
window.__NA.renderSpot({ spot_id: ..., creative_id: 83660, config: {...} });
```
**Можливі причини no-fill (по пріоритету):**
1. Spot **type ≠ "In-video VAST"** — створено як Banner/Popunder/etc → VAST endpoint не має чого serve
2. Spot **Status pending review** — створено, чекає approval
3. **Creative не attached** — VAST потребує специфічний ад чи bidder pool
4. **Bidder / network не linked** — VAST джерело мережі (ExoClick VAST / TS / etc) не призначено
5. Budget / cap exhausted
**Як остаточно verify:** через playwright headless + manual inject `_popRr` форсити VAST mode → click play → дивитися чи з'являється `.asg-container`. Якщо ні + curl показує empty VAST → no-fill.
### Mode decision (`AdCore._decide`)
```
mode = popActive ? (vastActive || skipPattern ? "none" : "vast") : "pop"
```
| popunder | vast | mode |
|----------|------|------|
| available (cooldown OFF) | — | **pop** (priority) |
| у cooldown (recent fire) | available + не skip-pattern | **vast** |
| у cooldown | у cooldown OR skip-pattern | none |
**Implicit dependency:** VAST показується **тільки після того як popunder уже спрацював** і поставив cooldown. На свіжій сесії (нема `_popRr` у localStorage / cookies) → mode завжди "pop", VAST SDK навіть не завантажується.
⚠️ Це **gotcha при тестуванні**: якщо щойно фікснули popunder spot — VAST не з'явиться поки popunder не спрацює хоча б раз. Юзер має думати що "VAST зламаний", а він просто заблокований mode logic.
**Force VAST testing**: `?clearAds=1` URL param очищає `_popRr/_vastRr/_vastPatternIdx/_pw/asgsl/_pjsLog` — фрешний state. Тоді: page load → pop fires → cooldown set → refresh → mode=vast → VAST SDK loaded.
`vastPolicy "show-1-skip-1"` додатково: idx=0 → SHOW, idx=1 → SKIP (incremented per VAST impression). На свіжій сесії idx=0 → перша VAST показується, друга skip-неться.
### Policy choice
`pop-priority` policy + `show-1-skip-1` для VAST — **навмисний баланс ревеню vs UX**:
- Popunder показується відносно рідко (cap від ASG високий)
- VAST спрацьовує часто, тому штучно скачуємо до 50% (`show-1-skip-1`) — менше тиску на юзера, не вбиває engagement
- Якщо колись треба максимізувати ревеню → `vastPolicy: 'show-always'`
## Open questions for developer
1. Як здійснюється mirror swap — sed-replace у всіх template/JS файлах через Адміна, runtime config, чи інший механізм?
2. Які pattern використовується для НЕ-77 сайтів (17 без adspy banner modules) — special-case structure, "no ads" сайти, чи щось інше?
3. SDK URLs (`nDNVal3.js`, `9iO21Eb.js`) — чи стабільні чи можуть змінитися при mirror rotation?