# tubev — Ads Integration
Реклама через **adspyglass.com**. Основне джерело монетизації — балансуємо CTR vs UX/PSI.
## Status
🟢 **Working knowledge** — Skeleton наповнюємо.
## 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) | `
...
<% 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
...
...
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 `=:` записи. Ключі що використовуються нашим bundle:
- `global_rr:` — `_readCooldowns` parse-ить це як cooldown end time
- `n:` — 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` локально вигляд: `=keep_looping:false,tabunder:false,uuid:...,noloop:true,shows_limit:1`
- **Нема `n:`** → impression handler виходить без write `_popRr`
- **Нема `global_rr:`** → `_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:`. Якщо нема → не налаштовано.
### 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?