- Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/ SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu fonksiyonları barındırıyor - RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/ rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi - 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client, ai/cagri, credits) bilinçli eklenmedi - Davranış değişikliği yok; pnpm build yeşil Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
111 lines
4.1 KiB
TypeScript
111 lines
4.1 KiB
TypeScript
import "server-only";
|
||
import { count } from "drizzle-orm";
|
||
import { connection } from "next/server";
|
||
import { appDb, schema } from "@/lib/appdb";
|
||
import type { SosyalKanitVerisi } from "@/lib/sosyal-kanit-sabitler";
|
||
|
||
/**
|
||
* Fiyat bölümündeki sosyal kanıt şeridinin sayıları.
|
||
*
|
||
* İki sayı, iki ayrı kaynak — bilerek:
|
||
*
|
||
* 1. "X öğrenci listesini oluşturdu" KENDİ veritabanımızdan gelir (`reports`
|
||
* tablosu; satır yalnızca liste gerçekten üretildikten sonra yazılır,
|
||
* bkz. app/sonuc/actions.ts). Bu sayı ziyaretçiye verilen bir söz olduğu
|
||
* için gerçek olmak zorunda; Rybbit'ten okunsaydı reklam engelleyiciler
|
||
* yüzünden olduğundan düşük çıkardı.
|
||
* 2. "Şu anda X kişi sitede" yalnızca Rybbit'ten gelebilir: giriş yapmamış
|
||
* ziyaretçinin bizim veritabanımızda hiçbir izi yok.
|
||
*
|
||
* Rybbit env'i eksikse (dev, anahtar tanımsız) canlı sayı `null` döner ve
|
||
* şeritte o satır hiç çizilmez — asla uydurulmuş sayı gösterilmez.
|
||
*/
|
||
|
||
/** Rybbit'in "şu an sitede" penceresi (GA'nın 30 dakikalık tanımıyla aynı). */
|
||
const CANLI_DAKIKA = 30;
|
||
|
||
const LISTE_TTL_MS = 10 * 60_000;
|
||
const CANLI_TTL_MS = 30_000;
|
||
|
||
const HOST = process.env.RYBBIT_HOST;
|
||
const API_KEY = process.env.RYBBIT_API_KEY;
|
||
/**
|
||
* DİKKAT: Rybbit'in istatistik API'si SAYISAL site kimliği ister
|
||
* (`/api/sites/:siteId/...` sorgusu ClickHouse'ta `site_id` Int32'ye
|
||
* bağlanıyor). `RYBBIT_SITE_ID` ise script'in kullandığı herkese açık
|
||
* kimlik (f2cb...) — ikisi farklı, birbirinin yerine geçmez.
|
||
*/
|
||
const STATS_SITE_ID = process.env.RYBBIT_STATS_SITE_ID;
|
||
|
||
type Kutu<T> = { deger: T; zaman: number };
|
||
|
||
/**
|
||
* Süreç içi TTL önbelleği. `use cache` yerine bunu kullanıyoruz: derleme
|
||
* anında `data/` boş (bkz. Dockerfile), dolayısıyla prerender edilen bir
|
||
* sayı 0 olarak taşa yazılırdı. `connection()` + bu bellek, sayıyı istek
|
||
* anına taşır ama her ziyaretçide DB/Rybbit'e gitmez. Uygulama tek süreçli
|
||
* standalone olarak çalıştığı için modül belleği yeterli.
|
||
*/
|
||
const bellek: { liste?: Kutu<number>; canli?: Kutu<number | null> } = {};
|
||
|
||
/** Üretilmiş tercih listesi sayısı. DB okunamazsa son bilinen değer korunur. */
|
||
export async function olusturulanListeSayisi(): Promise<number> {
|
||
await connection();
|
||
|
||
const onceki = bellek.liste;
|
||
if (onceki && Date.now() - onceki.zaman < LISTE_TTL_MS) return onceki.deger;
|
||
|
||
let deger = onceki?.deger ?? 0;
|
||
try {
|
||
const [satir] = await appDb.select({ n: count() }).from(schema.reports);
|
||
deger = satir?.n ?? 0;
|
||
} catch {
|
||
// Sayı gösterilemezse şerit gizlenir; sayfa asla hata vermez.
|
||
}
|
||
bellek.liste = { deger, zaman: Date.now() };
|
||
return deger;
|
||
}
|
||
|
||
/** Son 30 dakikadaki tekil ziyaretçi. Rybbit yoksa/erişilemezse null. */
|
||
export async function anlikZiyaretci(): Promise<number | null> {
|
||
if (!HOST || !API_KEY || !STATS_SITE_ID) return null;
|
||
|
||
// `Date.now()`den önce şart: prerender sırasında saat okumak yasak, istek
|
||
// anına bağlanmamız gerekiyor. Kardeş fonksiyondaki `connection()` bunu
|
||
// karşılamıyor — ikisi `Promise.all` ile paralel koşuyor.
|
||
await connection();
|
||
|
||
const onceki = bellek.canli;
|
||
if (onceki && Date.now() - onceki.zaman < CANLI_TTL_MS) return onceki.deger;
|
||
|
||
let deger: number | null = null;
|
||
try {
|
||
const cevap = await fetch(
|
||
`${HOST}/api/sites/${STATS_SITE_ID}/live-user-count?minutes=${CANLI_DAKIKA}`,
|
||
{
|
||
headers: { Authorization: `Bearer ${API_KEY}` },
|
||
cache: "no-store",
|
||
// Analitik hiçbir koşulda sayfayı bekletmesin
|
||
signal: AbortSignal.timeout(3000),
|
||
},
|
||
);
|
||
if (cevap.ok) {
|
||
const veri = (await cevap.json()) as { count?: number };
|
||
if (typeof veri.count === "number" && Number.isFinite(veri.count)) {
|
||
deger = veri.count;
|
||
}
|
||
}
|
||
} catch {
|
||
// Yut: canlı satır çizilmez, şeridin geri kalanı ayakta kalır.
|
||
}
|
||
bellek.canli = { deger, zaman: Date.now() };
|
||
return deger;
|
||
}
|
||
|
||
export async function sosyalKanitVerisi(): Promise<SosyalKanitVerisi> {
|
||
const [liste, canli] = await Promise.all([
|
||
olusturulanListeSayisi(),
|
||
anlikZiyaretci(),
|
||
]);
|
||
return { liste, canli };
|
||
} |