Rua Servilo de Moro, loja 2, Barreiro / Santa Helena - Belo Horizonte - MG - 30640-490


Informações do Anunciante
['host'=>'localhost','name'=>'SEU_BANCO','user'=>'SEU_USUARIO','pass'=>'SUA_SENHA','charset'=>'utf8mb4'], 'site'=>['base_url'=>'https://www.topdobairro.com','timezone'=>'America/Sao_Paulo'], 'brain'=>['mode'=>'aggressive','auto_apply'=>true,'auto_rollback'=>true,'rollback_threshold'=>.12,'min_lift'=>.05,'min_evidence_confidence'=>.80,'min_experiment_days'=>7,'protect_winners'=>true,'max_auto_risk'=>70,'cluster_concurrency_rate'=>.07,'cron_key'=>'TROQUE_POR_UMA_CHAVE_FORTE','min_query_impressions'=>50,'gsc_days_back'=>5], 'ttl_days'=>['telefone'=>90,'whatsapp'=>90,'horario'=>60,'endereco'=>180,'cnpj'=>365,'razao_social'=>365,'cep'=>365,'bairro'=>730,'cidade'=>730,'segmento'=>365,'site'=>180,'nome'=>365,'servicos'=>180,'area_atendimento'=>180], 'gsc'=>['enabled'=>false,'client_id'=>'','client_secret'=>'','redirect_uri'=>'https://www.topdobairro.com/brain.php?tdb_action=gsc_callback','site_url'=>'https://www.topdobairro.com/','scope'=>'https://www.googleapis.com/auth/webmasters.readonly','token_file'=>__DIR__.'/.tdb_gsc_token.json'], 'llm'=>['enabled'=>false,'endpoint'=>'','bearer_token'=>'','timeout_seconds'=>20,'explore_ratio'=>.20] ]; final class TopDoBairroBrain{ private PDO $db;private array $c; function __construct(PDO $db,array $c){$this->db=$db;$this->c=$c;$this->install();} private function q(string $s,array $p=[]):PDOStatement{$q=$this->db->prepare($s);$q->execute($p);return $q;} private function one(string $s,array $p=[]){return $this->q($s,$p)->fetchColumn();} private function row(string $s,array $p=[]):array{return $this->q($s,$p)->fetch(PDO::FETCH_ASSOC)?:[];} private function all(string $s,array $p=[]):array{return $this->q($s,$p)->fetchAll(PDO::FETCH_ASSOC);} private function j($v):string{return json_encode($v,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)?:'{}';} private function n(string $s):string{$s=mb_strtolower(trim($s),'UTF-8');$a=@iconv('UTF-8','ASCII//TRANSLIT//IGNORE',$s);if($a!==false)$s=$a;return trim(preg_replace('/[^a-z0-9]+/','_',$s)??'','_');} private function install():void{ $sql=[ "CREATE TABLE IF NOT EXISTS tdb_urls(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,url VARCHAR(700) NOT NULL UNIQUE,cidade VARCHAR(160),estado CHAR(2),bairro VARCHAR(160),categoria VARCHAR(180),segmento VARCHAR(180),intencao VARCHAR(80),cluster_key VARCHAR(255),created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,INDEX(cluster_key),INDEX(cidade,bairro,categoria)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_empresas(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,nome VARCHAR(255) NOT NULL,razao_social VARCHAR(255),cnpj VARCHAR(30),categoria VARCHAR(180),segmento VARCHAR(180),telefone VARCHAR(50),whatsapp VARCHAR(50),site VARCHAR(700),endereco VARCHAR(255),numero VARCHAR(50),bairro VARCHAR(160),cidade VARCHAR(160),estado CHAR(2),cep VARCHAR(20),latitude DECIMAL(10,7),longitude DECIMAL(10,7),horario_json JSON,servicos_json JSON,area_atendimento_json JSON,ativo TINYINT(1) DEFAULT 1,confidence DECIMAL(6,5) DEFAULT .5,ultima_validacao DATETIME,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,INDEX(cidade,bairro,categoria),INDEX(cnpj)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_profissionais_publicos(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,nome_publico VARCHAR(255) NOT NULL,empresa_id BIGINT UNSIGNED,funcao_publica VARCHAR(180),perfil_publico_url VARCHAR(700),cidade VARCHAR(160),estado CHAR(2),confidence DECIMAL(6,5) DEFAULT .5,ultima_validacao DATETIME,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,INDEX(empresa_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_evidencias(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,entity_type VARCHAR(50) NOT NULL,entity_id BIGINT UNSIGNED NOT NULL,atributo VARCHAR(100) NOT NULL,valor TEXT NOT NULL,fonte VARCHAR(700),fonte_tipo VARCHAR(80),confidence DECIMAL(6,5) DEFAULT .5,validado_usuario TINYINT(1) DEFAULT 0,validado_empresa TINYINT(1) DEFAULT 0,coletado_em DATETIME DEFAULT CURRENT_TIMESTAMP,validado_em DATETIME,INDEX(entity_type,entity_id),INDEX(atributo),INDEX(coletado_em)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_eventos(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,url_id BIGINT UNSIGNED NOT NULL,session_id VARCHAR(100),evento VARCHAR(80) NOT NULL,valor DECIMAL(14,5),experiment_id BIGINT UNSIGNED,variant ENUM('A','B'),metadata JSON,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,INDEX(url_id,evento),INDEX(created_at),INDEX(session_id),INDEX(experiment_id,variant)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_metricas_url(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,url_id BIGINT UNSIGNED NOT NULL,data DATE NOT NULL,impressoes INT DEFAULT 0,cliques INT DEFAULT 0,ctr DECIMAL(10,7) DEFAULT 0,posicao DECIMAL(10,4),pageviews INT DEFAULT 0,sessions INT DEFAULT 0,engaged_sessions INT DEFAULT 0,engagement_rate DECIMAL(10,7) DEFAULT 0,scroll_avg DECIMAL(8,3),active_time_avg DECIMAL(12,3),whatsapp_clicks INT DEFAULT 0,phone_clicks INT DEFAULT 0,leads INT DEFAULT 0,UNIQUE KEY uq(url_id,data),INDEX(data)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_gsc_queries(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,url_id BIGINT UNSIGNED NOT NULL,data DATE NOT NULL,query_text VARCHAR(500) NOT NULL,impressions INT DEFAULT 0,clicks INT DEFAULT 0,ctr DECIMAL(10,7) DEFAULT 0,position DECIMAL(10,4),intent_json JSON,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uq(url_id,query_text,data),INDEX(url_id),INDEX(query_text(191)),INDEX(data)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_score_url(url_id BIGINT UNSIGNED PRIMARY KEY,opportunity_score DECIMAL(8,3) DEFAULT 0,seo_score DECIMAL(8,3) DEFAULT 0,engagement_score DECIMAL(8,3) DEFAULT 0,conversion_score DECIMAL(8,3) DEFAULT 0,data_quality_score DECIMAL(8,3) DEFAULT 0,volatility DECIMAL(8,5) DEFAULT 0,status VARCHAR(40),decision VARCHAR(40),calculated_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_versoes_url(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,url_id BIGINT UNSIGNED NOT NULL,version_number INT NOT NULL,title TEXT,meta_description TEXT,h1 TEXT,content LONGTEXT,cta_json JSON,modules_json JSON,hypothesis_json JSON,generated_by VARCHAR(100),active TINYINT(1) DEFAULT 0,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,INDEX(url_id,active)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_experimentos(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,url_id BIGINT UNSIGNED NOT NULL,tipo ENUM('seo_temporal','cro_split') NOT NULL,change_type VARCHAR(80),pattern_key VARCHAR(255),hypothesis LONGTEXT,control_version_id BIGINT UNSIGNED,test_version_id BIGINT UNSIGNED,metric_primary VARCHAR(80),status ENUM('running','winner','loser','inconclusive','cancelled') DEFAULT 'running',phase ENUM('control','test','split') DEFAULT 'control',traffic_split DECIMAL(5,2) DEFAULT 50,baseline DECIMAL(14,7),result DECIMAL(14,7),improvement DECIMAL(14,7),confidence DECIMAL(8,5) DEFAULT 0,started_at DATETIME DEFAULT CURRENT_TIMESTAMP,phase_changed_at DATETIME,ended_at DATETIME,INDEX(status),INDEX(url_id),INDEX(pattern_key)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_memoria_padroes(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,pattern_key VARCHAR(255) NOT NULL UNIQUE,pattern_type VARCHAR(80),scope ENUM('local','cluster','global') DEFAULT 'local',conditions_json JSON,context_json JSON,action_json JSON,wins INT DEFAULT 0,losses INT DEFAULT 0,inconclusive INT DEFAULT 0,samples BIGINT DEFAULT 0,avg_lift DECIMAL(10,7) DEFAULT 0,cities_tested INT DEFAULT 0,categories_tested INT DEFAULT 0,confidence DECIMAL(8,5) DEFAULT 0,active TINYINT(1) DEFAULT 0,last_win_at DATETIME,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS tdb_feedback(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,empresa_id BIGINT UNSIGNED,url_id BIGINT UNSIGNED,tipo VARCHAR(80),atributo VARCHAR(100),mensagem TEXT,suggested_value TEXT,confidence DECIMAL(6,5) DEFAULT .7,status ENUM('pending','approved','rejected','applied') DEFAULT 'pending',created_at DATETIME DEFAULT CURRENT_TIMESTAMP,INDEX(status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ];foreach($sql as $s)$this->db->exec($s); } public function registerUrl(string $url,string $cidade='',string $estado='',string $bairro='',string $categoria='',string $segmento='',string $intencao='transacional'):int{ $cl=$this->generateCluster($categoria,$segmento,$intencao); $this->q("INSERT INTO tdb_urls(url,cidade,estado,bairro,categoria,segmento,intencao,cluster_key) VALUES(?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE cidade=VALUES(cidade),estado=VALUES(estado),bairro=VALUES(bairro),categoria=VALUES(categoria),segmento=VALUES(segmento),intencao=VALUES(intencao),cluster_key=VALUES(cluster_key)",[$url,$cidade,$estado,$bairro,$categoria,$segmento,$intencao,$cl]); return(int)$this->one("SELECT id FROM tdb_urls WHERE url=? LIMIT 1",[$url]); } private function generateCluster(string $c,string $s,string $i):string{return implode(':',array_map(fn($v)=>$this->n((string)$v),array_merge($this->getCategoryTree($c,$s),[$i?:'geral'])));} public function getCategoryTree(string $c,string $s=''):array{ $m=['pizzaria'=>['Alimentação','Pizzaria'],'restaurante'=>['Alimentação','Restaurante'],'hamburgueria'=>['Alimentação','Hamburgueria'],'lanchonete'=>['Alimentação','Lanchonete'],'padaria'=>['Alimentação','Padaria'],'eletricista'=>['Serviços urgentes','Eletricista'],'chaveiro'=>['Serviços urgentes','Chaveiro'],'encanador'=>['Serviços urgentes','Encanador'],'desentupidora'=>['Serviços urgentes','Desentupidora'],'dentista'=>['Saúde','Dentista'],'clinica'=>['Saúde','Clínica'],'advogado'=>['Serviços profissionais','Advocacia'],'contador'=>['Serviços profissionais','Contabilidade'],'hotel'=>['Hospedagem','Hotel'],'pousada'=>['Hospedagem','Pousada'],'supermercado'=>['Comércio','Supermercado'],'material_de_construcao'=>['Comércio','Materiais de construção']]; $n=$this->n($c);return$m[$n]??($s!==''?[$s,$c?:'Geral']:['Geral',$c?:'Geral']); } public function upsertCompany(array $x,array $src=[]):int{ $n=trim((string)($x['nome']??''));if($n==='')throw new InvalidArgumentException('Empresa sem nome.'); $id=0;$cnpj=trim((string)($x['cnpj']??'')); if($cnpj!=='')$id=(int)$this->one("SELECT id FROM tdb_empresas WHERE cnpj=? LIMIT 1",[$cnpj]); if(!$id)$id=(int)$this->one("SELECT id FROM tdb_empresas WHERE nome=? AND cidade=? AND COALESCE(bairro,'')=? LIMIT 1",[$n,(string)($x['cidade']??''),(string)($x['bairro']??'')]); $f=['nome','razao_social','cnpj','categoria','segmento','telefone','whatsapp','site','endereco','numero','bairro','cidade','estado','cep','latitude','longitude']; if($id){$set=[];$p=[];foreach($f as $k)if(array_key_exists($k,$x)){$set[]="$k=?";$p[]=$x[$k];}foreach(['horario_json','servicos_json','area_atendimento_json']as$k)if(isset($x[$k])){$set[]="$k=?";$p[]=is_array($x[$k])?$this->j($x[$k]):$x[$k];}if($set){$p[]=$id;$this->q("UPDATE tdb_empresas SET ".implode(',',$set)." WHERE id=?",$p);}} else{$this->q("INSERT INTO tdb_empresas(nome,razao_social,cnpj,categoria,segmento,telefone,whatsapp,site,endereco,numero,bairro,cidade,estado,cep,latitude,longitude,horario_json,servicos_json,area_atendimento_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",[$n,$x['razao_social']??null,$x['cnpj']??null,$x['categoria']??null,$x['segmento']??null,$x['telefone']??null,$x['whatsapp']??null,$x['site']??null,$x['endereco']??null,$x['numero']??null,$x['bairro']??null,$x['cidade']??null,$x['estado']??null,$x['cep']??null,$x['latitude']??null,$x['longitude']??null,isset($x['horario_json'])?(is_array($x['horario_json'])?$this->j($x['horario_json']):$x['horario_json']):null,isset($x['servicos_json'])?(is_array($x['servicos_json'])?$this->j($x['servicos_json']):$x['servicos_json']):null,isset($x['area_atendimento_json'])?(is_array($x['area_atendimento_json'])?$this->j($x['area_atendimento_json']):$x['area_atendimento_json']):null]);$id=(int)$this->db->lastInsertId();} $sn=(string)($src['name']??'cadastro');$st=(string)($src['type']??'cadastro');$cf=(float)($src['confidence']??.8); foreach(['nome','razao_social','cnpj','categoria','segmento','telefone','whatsapp','site','endereco','bairro','cidade','estado','cep']as$k)if(!empty($x[$k]))$this->addEvidence('empresa',$id,$k,(string)$x[$k],$sn,$st,$cf,(bool)($src['validated_user']??false),(bool)($src['validated_company']??false)); if(!empty($x['horario_json']))$this->addEvidence('empresa',$id,'horario',is_array($x['horario_json'])?$this->j($x['horario_json']):(string)$x['horario_json'],$sn,$st,$cf,(bool)($src['validated_user']??false),(bool)($src['validated_company']??false)); return$id; } public function addEvidence(string $t,int $id,string $a,string $v,string $f,string $ft,float $cf=.8,bool $vu=false,bool $ve=false):void{ $cf=min(1,max(0,$cf));$this->q("INSERT INTO tdb_evidencias(entity_type,entity_id,atributo,valor,fonte,fonte_tipo,confidence,validado_usuario,validado_empresa,validado_em) VALUES(?,?,?,?,?,?,?,?,?,NOW())",[$t,$id,$a,$v,$f,$ft,$cf,$vu?1:0,$ve?1:0]);if($t==='empresa')$this->recalculateCompanyConfidence($id); } private function evidenceIsValid(array $f):bool{$ttl=(int)($this->c['ttl_days'][$f['atributo']]??180);$ts=strtotime((string)$f['coletado_em']);return$ts&&((time()-$ts)/86400)<=$ttl&&(float)$f['confidence']>=(float)$this->c['brain']['min_evidence_confidence'];} public function getEvidence(string $t,int $id,string $a):?array{$r=$this->all("SELECT * FROM tdb_evidencias WHERE entity_type=? AND entity_id=? AND atributo=? ORDER BY validado_empresa DESC,validado_usuario DESC,confidence DESC,validado_em DESC,coletado_em DESC",[$t,$id,$a]);foreach($r as$f)if($this->evidenceIsValid($f))return$f;return null;} public function mayPublish(string $t,int $id,string $a):bool{return$this->getEvidence($t,$id,$a)!==null;} public function safeFact(string $t,int $id,string $a,string $fb=''):string{$f=$this->getEvidence($t,$id,$a);return htmlspecialchars($f?(string)$f['valor']:$fb,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');} private function recalculateCompanyConfidence(int $id):void{$cf=(float)$this->one("SELECT AVG(confidence) FROM tdb_evidencias WHERE entity_type='empresa' AND entity_id=?",[$id]);$this->q("UPDATE tdb_empresas SET confidence=?,ultima_validacao=NOW() WHERE id=?",[$cf,$id]);} public function companiesForPage(string $c,string $cat,?string $b=null,int $l=30):array{$s="SELECT * FROM tdb_empresas WHERE ativo=1 AND cidade=? AND categoria=? AND confidence>=?";$p=[$c,$cat,$this->c['brain']['min_evidence_confidence']];if($b){$s.=" AND bairro=?";$p[]=$b;}$s.=" ORDER BY confidence DESC,ultima_validacao DESC LIMIT ".max(1,min(100,$l));return$this->all($s,$p);} public function assignCROVariant(int $e,string $sid):string{return(hexdec(substr(hash('sha256',$e.'|'.$sid),0,8))%100)<50?'A':'B';} public function getRunningCROExperiment(int $id):?array{$r=$this->row("SELECT * FROM tdb_experimentos WHERE url_id=? AND tipo='cro_split' AND status='running' ORDER BY id DESC LIMIT 1",[$id]);return$r?:null;} public function tracker(int $id):string{$ep=strtok($_SERVER['REQUEST_URI']??'/','?').'?tdb_action=event';$ep=$this->j($ep);return"";} public function receiveEvent():never{$x=json_decode(file_get_contents('php://input')?:'',true);if(!is_array($x)||empty($x['url_id'])||empty($x['evento'])){http_response_code(400);exit;}$ok=['pageview','scroll','active_time','whatsapp_click','phone_click','internal_search','lead','feedback','exit_intent','copy_text'];if(!in_array($x['evento'],$ok,true)){http_response_code(400);exit;}$v=$x['variant']??null;if($v!==null&&!in_array($v,['A','B'],true))$v=null;$this->q("INSERT INTO tdb_eventos(url_id,session_id,evento,valor,experiment_id,variant,metadata) VALUES(?,?,?,?,?,?,?)",[(int)$x['url_id'],substr((string)($x['session_id']??''),0,100),(string)$x['evento'],isset($x['valor'])?(float)$x['valor']:null,isset($x['experiment_id'])&&$x['experiment_id']!==''?(int)$x['experiment_id']:null,$v,$this->j(is_array($x['metadata']??null)?$x['metadata']:[])]);http_response_code(204);exit;} public function aggregateEventsToMetrics(int $id,?string $d=null):void{$d=$d??date('Y-m-d',strtotime('-1 day'));if($d>=date('Y-m-d'))throw new InvalidArgumentException('Só dias fechados.');$a=$d.' 00:00:00';$b=$d.' 23:59:59';$e=$this->row("SELECT SUM(evento='pageview') pageviews,SUM(evento='whatsapp_click') whatsapp,SUM(evento='phone_click') phone,SUM(evento='lead') leads,AVG(CASE WHEN evento='scroll' THEN valor END) scroll_avg,AVG(CASE WHEN evento='active_time' THEN valor END) active_time_avg,COUNT(DISTINCT session_id) sessions FROM tdb_eventos WHERE url_id=? AND created_at BETWEEN ? AND ?",[$id,$a,$b]);$g=(int)$this->one("SELECT COUNT(*) FROM(SELECT session_id FROM tdb_eventos WHERE url_id=? AND created_at BETWEEN ? AND ? AND session_id IS NOT NULL AND session_id<>'' GROUP BY session_id HAVING MAX(evento='active_time' AND valor>=20)=1 OR MAX(evento='scroll' AND valor>=40)=1 OR MAX(evento IN('whatsapp_click','phone_click','lead','internal_search'))=1)x",[$id,$a,$b]);$s=(int)($e['sessions']??0);$r=$s?$g/$s:0;$this->q("INSERT INTO tdb_metricas_url(url_id,data,pageviews,sessions,engaged_sessions,engagement_rate,scroll_avg,active_time_avg,whatsapp_clicks,phone_clicks,leads) VALUES(?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE pageviews=VALUES(pageviews),sessions=VALUES(sessions),engaged_sessions=VALUES(engaged_sessions),engagement_rate=VALUES(engagement_rate),scroll_avg=VALUES(scroll_avg),active_time_avg=VALUES(active_time_avg),whatsapp_clicks=VALUES(whatsapp_clicks),phone_clicks=VALUES(phone_clicks),leads=VALUES(leads)",[$id,$d,(int)($e['pageviews']??0),$s,$g,$r,(float)($e['scroll_avg']??0),(float)($e['active_time_avg']??0),(int)($e['whatsapp']??0),(int)($e['phone']??0),(int)($e['leads']??0)]);} public function aggregateAllYesterday():int{$d=date('Y-m-d',strtotime('-1 day'));$x=$this->db->query("SELECT id FROM tdb_urls")->fetchAll(PDO::FETCH_COLUMN);foreach($x as$id)$this->aggregateEventsToMetrics((int)$id,$d);return count($x);} private function h(string $u,array $o=[]):array{$ch=curl_init($u);curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CONNECTTIMEOUT=>10,CURLOPT_TIMEOUT=>(int)($o['timeout']??30),CURLOPT_FOLLOWLOCATION=>false,CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2]);$m=strtoupper($o['method']??'GET');if($m!=='GET')curl_setopt($ch,CURLOPT_CUSTOMREQUEST,$m);if(array_key_exists('body',$o))curl_setopt($ch,CURLOPT_POSTFIELDS,$o['body']);if(!empty($o['headers']))curl_setopt($ch,CURLOPT_HTTPHEADER,$o['headers']);$raw=curl_exec($ch);$er=curl_error($ch);$no=curl_errno($ch);$http=(int)curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch);if($no)throw new RuntimeException("cURL $no: $er");$d=json_decode((string)$raw,true);return['http'=>$http,'data'=>is_array($d)?$d:['raw'=>(string)$raw]];} public function gscAuthStart():never{if(empty($this->c['gsc']['enabled']))throw new RuntimeException('GSC desativado.');if(session_status()!==PHP_SESSION_ACTIVE)session_start();$s=bin2hex(random_bytes(24));$_SESSION['tdb_gsc_state']=$s;$g=$this->c['gsc'];header('Location: https://accounts.google.com/o/oauth2/v2/auth?'.http_build_query(['client_id'=>$g['client_id'],'redirect_uri'=>$g['redirect_uri'],'response_type'=>'code','scope'=>$g['scope'],'access_type'=>'offline','prompt'=>'consent','include_granted_scopes'=>'true','state'=>$s]));exit;} public function gscCallback():never{if(session_status()!==PHP_SESSION_ACTIVE)session_start();if(isset($_GET['error'])){http_response_code(400);exit('OAuth negado.');}$s=(string)($_GET['state']??'');$e=(string)($_SESSION['tdb_gsc_state']??'');if(!$s||!$e||!hash_equals($e,$s)){http_response_code(403);exit('State inválido.');}unset($_SESSION['tdb_gsc_state']);$code=(string)($_GET['code']??'');if(!$code){http_response_code(400);exit('Código ausente.');}$g=$this->c['gsc'];$r=$this->h('https://oauth2.googleapis.com/token',['method'=>'POST','headers'=>['Content-Type: application/x-www-form-urlencoded'],'body'=>http_build_query(['code'=>$code,'client_id'=>$g['client_id'],'client_secret'=>$g['client_secret'],'redirect_uri'=>$g['redirect_uri'],'grant_type'=>'authorization_code'])]);if($r['http']!==200||empty($r['data']['refresh_token'])){http_response_code(500);exit('Falha OAuth.');}$f=$g['token_file'];if(file_put_contents($f,$this->j(['refresh_token'=>$r['data']['refresh_token'],'created_at'=>date('c')]),LOCK_EX)===false)throw new RuntimeException('Falha ao gravar token.');@chmod($f,0600);exit('Google Search Console conectado.');} private function getGSCRefreshToken():string{$f=$this->c['gsc']['token_file'];if(!is_file($f))return'';$x=json_decode((string)file_get_contents($f),true);return is_array($x)?(string)($x['refresh_token']??''):'';} private function getGSCAccessToken():string{$g=$this->c['gsc'];$r=$this->getGSCRefreshToken();if(!$r)throw new RuntimeException('Refresh token ausente.');$x=$this->h('https://oauth2.googleapis.com/token',['method'=>'POST','headers'=>['Content-Type: application/x-www-form-urlencoded'],'body'=>http_build_query(['client_id'=>$g['client_id'],'client_secret'=>$g['client_secret'],'refresh_token'=>$r,'grant_type'=>'refresh_token'])]);if($x['http']!==200||empty($x['data']['access_token']))throw new RuntimeException('Falha token GSC.');return(string)$x['data']['access_token'];} public function fetchGSC(int $n=5):array{if(empty($this->c['gsc']['enabled']))return['rows'=>0,'disabled'=>true];$t=$this->getGSCAccessToken();$a=date('Y-m-d',strtotime("-$n days"));$b=date('Y-m-d',strtotime('-1 day'));$r=$this->gscQueryPaginated($t,$this->c['gsc']['site_url'],$a,$b);$k=$this->ingestGSCData($r);$this->rebuildUrlMetricsFromGSC($a,$b);return['rows'=>$k,'start'=>$a,'end'=>$b];} private function gscQueryPaginated(string $t,string $site,string $a,string $b):array{$u='https://www.googleapis.com/webmasters/v3/sites/'.rawurlencode($site).'/searchAnalytics/query';$out=[];$st=0;$l=25000;do{$x=$this->h($u,['method'=>'POST','headers'=>['Authorization: Bearer '.$t,'Content-Type: application/json'],'body'=>$this->j(['startDate'=>$a,'endDate'=>$b,'dimensions'=>['page','query','date'],'rowLimit'=>$l,'startRow'=>$st,'type'=>'web']),'timeout'=>60]);if($x['http']!==200)throw new RuntimeException('GSC HTTP '.$x['http']);$rr=$x['data']['rows']??[];if(!is_array($rr))$rr=[];foreach($rr as$r){$k=$r['keys']??[];if(count($k)<3)continue;$q=(string)$k[1];$out[]=['url'=>(string)$k[0],'query'=>$q,'date'=>(string)$k[2],'impressions'=>(int)($r['impressions']??0),'clicks'=>(int)($r['clicks']??0),'ctr'=>(float)($r['ctr']??0),'position'=>(float)($r['position']??0),'intent_json'=>$this->j($this->classifyIntentProfile($q))];}$st+=$l;}while(count($rr)===$l);return$out;} private function getUrlId(string $u):int{$id=(int)$this->one("SELECT id FROM tdb_urls WHERE url=? LIMIT 1",[$u]);if(!$id){$a=str_ends_with($u,'/')?rtrim($u,'/'):$u.'/';$id=(int)$this->one("SELECT id FROM tdb_urls WHERE url=? LIMIT 1",[$a]);}return$id;} public function ingestGSCData(array $rows):int{$s="INSERT INTO tdb_gsc_queries(url_id,data,query_text,impressions,clicks,ctr,position,intent_json) VALUES(?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE impressions=VALUES(impressions),clicks=VALUES(clicks),ctr=VALUES(ctr),position=VALUES(position),intent_json=VALUES(intent_json)";$n=0;foreach($rows as$r){$id=$this->getUrlId((string)$r['url']);if(!$id)continue;$this->q($s,[$id,$r['date'],$r['query'],(int)$r['impressions'],(int)$r['clicks'],(float)$r['ctr'],(float)$r['position'],$r['intent_json']]);$n++;}return$n;} private function rebuildUrlMetricsFromGSC(string $a,string $b):void{$r=$this->all("SELECT url_id,data,SUM(impressions) impressions,SUM(clicks) clicks,IF(SUM(impressions)>0,SUM(clicks)/SUM(impressions),0) ctr,IF(SUM(impressions)>0,SUM(position*impressions)/SUM(impressions),NULL) position FROM tdb_gsc_queries WHERE data BETWEEN ? AND ? GROUP BY url_id,data",[$a,$b]);foreach($r as$x)$this->q("INSERT INTO tdb_metricas_url(url_id,data,impressoes,cliques,ctr,posicao) VALUES(?,?,?,?,?,?) ON DUPLICATE KEY UPDATE impressoes=VALUES(impressoes),cliques=VALUES(cliques),ctr=VALUES(ctr),posicao=VALUES(posicao)",[(int)$x['url_id'],$x['data'],(int)$x['impressions'],(int)$x['clicks'],(float)$x['ctr'],$x['position']!==null?(float)$x['position']:null]);} public function classifyIntentProfile(string $q):array{$q=mb_strtolower(trim($q),'UTF-8');$p=['local'=>0.,'transactional'=>0.,'urgent'=>0.,'informational'=>0.,'navigational'=>0.];if(preg_match('/\b(perto de mim|próximo|proximo|na minha região|na minha regiao)\b/u',$q))$p['local']=.99;if(preg_match('/\b(em|no|na|de)\s+[\p{L}][\p{L}\s\-]{2,}\b/u',$q))$p['local']=max($p['local'],.92);if(preg_match('/\b(agora|24h|24 horas|aberto|aberta|urgente|emergência|emergencia)\b/u',$q))$p['urgent']=.96;if(preg_match('/\b(preço|preco|valor|comprar|delivery|entrega|contratar|orçamento|orcamento|reservar|agendar|pedido)\b/u',$q))$p['transactional']=.93;if(preg_match('/\b(como|o que é|o que e|guia|tutorial|dicas|qual|quando|por que|porque)\b/u',$q))$p['informational']=.86;if(preg_match('/\b(site oficial|login|entrar|telefone de|whatsapp de)\b/u',$q))$p['navigational']=.9;if($p['local']>=.9&&$p['transactional']<.5&&$p['informational']<.5)$p['transactional']=.84;if(preg_match('/\b(pizzaria|restaurante|hotel|eletricista|chaveiro|encanador|dentista|advogado|contador|loja|supermercado|oficina|farmácia|farmacia)\b/u',$q))$p['transactional']=max($p['transactional'],.82);return$p;} public function analyzeQueryGaps(int $id):array{$r=$this->all("SELECT query_text,SUM(impressions) imp,SUM(clicks) clicks,IF(SUM(impressions)>0,SUM(clicks)/SUM(impressions),0) ctr,IF(SUM(impressions)>0,SUM(position*impressions)/SUM(impressions),99) pos,intent_json FROM tdb_gsc_queries WHERE url_id=? AND data>=DATE_SUB(CURDATE(),INTERVAL 28 DAY) GROUP BY query_text,intent_json HAVING imp>=? ORDER BY imp DESC LIMIT 100",[$id,(int)$this->c['brain']['min_query_impressions']]);$g=[];foreach($r as$q){$i=json_decode((string)($q['intent_json']??'{}'),true)?:[];$p=(float)$q['pos'];$c=(float)$q['ctr'];$m=(int)$q['imp'];if(($i['urgent']??0)>.8&&$p>5)$g[]=['query'=>$q['query_text'],'intent_profile'=>$i,'hypothesis'=>['type'=>'dynamic_block','pattern'=>'availability_now','action'=>'Mostrar empresas abertas agora usando horário com evidência válida.','reason'=>"Intenção urgente: $m impressões, posição ".round($p,1),'target_query'=>$q['query_text'],'expected_metric'=>'ctr','risk'=>20,'evidence_required'=>['horario','cidade','nome']]];if(($i['transactional']??0)>.8&&$c<.03&&$m>=100)$g[]=['query'=>$q['query_text'],'intent_profile'=>$i,'hypothesis'=>['type'=>'cta','pattern'=>'transactional_cta_local','action'=>'Testar CTA contextual de contato/orçamento.','reason'=>'Intenção transacional alta e CTR baixo.','target_query'=>$q['query_text'],'expected_metric'=>'whatsapp','risk'=>10,'evidence_required'=>[]]];if(($i['informational']??0)>.75&&$p>6&&$m>=100)$g[]=['query'=>$q['query_text'],'intent_profile'=>$i,'hypothesis'=>['type'=>'faq','pattern'=>'evidence_faq','action'=>'Adicionar resposta curta baseada apenas em evidências.','reason'=>'Demanda informacional não coberta.','target_query'=>$q['query_text'],'expected_metric'=>'engagement','risk'=>20,'evidence_required'=>[]]];}return['main_query'=>$r[0]['query_text']??'','main_metrics'=>$r[0]??[],'gaps'=>$g];} public function calculateScore(int $id):array{$m=$this->row("SELECT SUM(impressoes) impressoes,SUM(cliques) cliques,IF(SUM(impressoes)>0,SUM(cliques)/SUM(impressoes),0) ctr,IF(SUM(impressoes)>0,SUM(posicao*impressoes)/SUM(impressoes),99) posicao,SUM(pageviews) pageviews,SUM(sessions) sessions,SUM(engaged_sessions) engaged_sessions,IF(SUM(sessions)>0,SUM(engaged_sessions)/SUM(sessions),0) engagement_rate,SUM(whatsapp_clicks) whatsapp,SUM(leads) leads FROM tdb_metricas_url WHERE url_id=? AND data>=DATE_SUB(CURDATE(),INTERVAL 28 DAY)",[$id]);$im=max(1,(int)($m['impressoes']??0));$se=max(1,(int)($m['sessions']??0));$ctr=(float)($m['ctr']??0);$pos=(float)($m['posicao']??99);$lr=(int)($m['leads']??0)/$se;$wr=(int)($m['whatsapp']??0)/$se;$er=(float)($m['engagement_rate']??0);$vis=min(100,log10($im+10)*25);$ex=match(true){$pos<=1=>.28,$pos<=3=>.15,$pos<=5=>.08,$pos<=10=>.04,$pos<=20=>.02,default=>.01};$cp=max(0,min(100,(($ex-$ctr)/max(.001,$ex))*100));$cv=max(0,min(100,100-$lr*1000));$ro=match(true){$pos<=3=>10,$pos<=7=>50,$pos<=15=>90,$pos<=30=>100,default=>70};$v=$this->calculateVolatility($id);$sc=($vis*.22+$cp*.23+$cv*.18+$ro*.27+max(0,100-$er*100)*.10)*$this->volatilityConfidence($v);$sc=min(100,max(0,$sc));$r=['opportunity_score'=>round($sc,2),'volatility'=>round($v,5),'metrics'=>['impressions'=>$im,'sessions'=>$se,'ctr'=>$ctr,'position'=>$pos,'lead_rate'=>$lr,'whatsapp_rate'=>$wr,'engagement_rate'=>$er]];$r['decision']=$this->decisionGate($r);$this->q("INSERT INTO tdb_score_url(url_id,opportunity_score,engagement_score,conversion_score,volatility,status,decision,calculated_at) VALUES(?,?,?,?,?,'active',?,NOW()) ON DUPLICATE KEY UPDATE opportunity_score=VALUES(opportunity_score),engagement_score=VALUES(engagement_score),conversion_score=VALUES(conversion_score),volatility=VALUES(volatility),status='active',decision=VALUES(decision),calculated_at=NOW()",[$id,$sc,$er*100,min(100,$lr*1000),$v,$r['decision']]);return$r;} private function calculateVolatility(int $id):float{$v=array_map('floatval',$this->q("SELECT ctr FROM tdb_metricas_url WHERE url_id=? AND data>=DATE_SUB(CURDATE(),INTERVAL 28 DAY) AND impressoes>0 ORDER BY data",[$id])->fetchAll(PDO::FETCH_COLUMN));if(count($v)<4)return 0;$m=array_sum($v)/count($v);if($m<=0)return 0;$s=0.;foreach($v as$x)$s+=($x-$m)**2;return sqrt($s/count($v))/$m;} private function volatilityConfidence(float $v):float{return$v<.15?1:($v<.35?.7:.3);} private function decisionGate(array $r):string{$m=$r['metrics'];if($this->c['brain']['protect_winners']&&$m['position']>0&&$m['position']<=3&&$m['ctr']>=.08)return'PROTECT';if($r['volatility']>=.5)return'OBSERVE';if($r['opportunity_score']>=75)return'AGGRESSIVE';if($r['opportunity_score']>=45)return'EXPERIMENT';return'OBSERVE';} public function riskScore(string $x,array $r):float{$b=['cta'=>10,'bloco'=>15,'dynamic_block'=>15,'faq'=>20,'meta'=>30,'h1'=>45,'title'=>55,'content_30pct'=>65,'url'=>100][$x]??50;$m=$r['metrics'];$t=min(1.8,max(.7,log10(($m['impressions']??0)+10)/3));$k=$m['position']<=3?1.6:($m['position']<=10?1.2:.9);return min(100,$b*$t*$k*(1+min(1,(float)$r['volatility'])));} public function canAutoApply(string $x,array $r):bool{if(!$this->c['brain']['auto_apply'])return false;$k=$this->riskScore($x,$r);if($r['decision']==='PROTECT'&&$k>30)return false;return$r['opportunity_score']>=70&&$k<=(float)$this->c['brain']['max_auto_risk'];} public function generateHypotheses(int $id,array $s):array{$o=[];$g=$this->analyzeQueryGaps($id);foreach($g['gaps']as$x)$o[]=$x['hypothesis'];$m=$s['metrics'];if($m['position']<=10&&$m['ctr']<.025)$o[]=['type'=>'title','pattern'=>'title_intent_local','reason'=>'Bom ranking com CTR baixo.','action'=>'Testar title aderente à intenção dominante sem inventar fatos.','target_query'=>$g['main_query']??'','expected_metric'=>'ctr','risk'=>55,'evidence_required'=>[]];if($m['whatsapp_rate']<.01)$o[]=['type'=>'cta','pattern'=>'cta_local_contextual','reason'=>'Baixa taxa de WhatsApp.','action'=>'Testar CTA contextual.','target_query'=>$g['main_query']??'','expected_metric'=>'whatsapp','risk'=>10,'evidence_required'=>[]];$u=$this->getUrlRecord($id);$mem=$this->getPatternsForCategory((string)($u['categoria']??''),(string)($u['segmento']??''),20);if(!empty($this->c['llm']['enabled']))try{foreach($this->generateLLMHypotheses(['url'=>$u,'score'=>$s,'query_intelligence'=>$g,'memory'=>$mem,'category_tree'=>$this->getCategoryTree((string)$u['categoria'],(string)$u['segmento'])])as$h)if($this->validateHypothesis($h))$o[]=$h;}catch(Throwable $e){error_log($e->getMessage());}$z=[];foreach($o as$h)if($this->validateHypothesis($h)){$k=($h['type']??'').'|'.($h['pattern']??'').'|'.($h['target_query']??'');$z[$k]??=$h;}return array_values($z);} private function generateLLMHypotheses(array $ctx):array{$g=$this->c['llm'];if(empty($g['endpoint']))return[];$h=['Content-Type: application/json'];if(!empty($g['bearer_token']))$h[]='Authorization: Bearer '.$g['bearer_token'];$r=$this->h($g['endpoint'],['method'=>'POST','headers'=>$h,'body'=>$this->j(['task'=>'generate_hypotheses','rules'=>['return_max'=>3,'never_invent_facts'=>true,'facts_require_evidence'=>true,'prefer_low_risk_changes'=>true,'output_json_only'=>true],'context'=>$ctx]),'timeout'=>(int)$g['timeout_seconds']]);$x=$r['data']['hypotheses']??[];return is_array($x)?array_slice($x,0,3):[];} private function validateHypothesis(array $h):bool{return in_array((string)($h['type']??''),['cta','bloco','dynamic_block','faq','meta','h1','title','content_30pct'],true)&&trim((string)($h['action']??''))!==''&&trim((string)($h['reason']??''))!=='';} public function getPatternsForCategory(string $c,string $s='',int $l=20):array{$t=$this->getCategoryTree($c,$s);$a=$this->all("SELECT * FROM tdb_memoria_padroes WHERE active=1 ORDER BY confidence DESC,avg_lift DESC,samples DESC LIMIT 200");$o=[];foreach($a as$p){$x=json_decode((string)($p['context_json']??'{}'),true)?:[];$pt=is_array($x['category_tree']??null)?$x['category_tree']:[];if(array_intersect($t,$pt)||$p['scope']==='global'){$o[]=$p;if(count($o)>=$l)break;}}return$o;} public function recordPatternResult(string $p,string $st,float $lift,int $sm,string $city,string $cat,string $seg='',array $x=[]):void{$r=$this->row("SELECT * FROM tdb_memoria_padroes WHERE pattern_key=? LIMIT 1",[$p]);$tree=$this->getCategoryTree($cat,$seg);if(!$r){$ctx=['cities'=>[$city],'categories'=>[$cat],'segments'=>$seg!==''?[$seg]:[],'category_tree'=>$tree,'last_context'=>$x];$this->q("INSERT INTO tdb_memoria_padroes(pattern_key,pattern_type,conditions_json,context_json,action_json) VALUES(?,'autolearn','{}',?,'{}')",[$p,$this->j($ctx)]);$r=$this->row("SELECT * FROM tdb_memoria_padroes WHERE pattern_key=?",[$p]);}$c=json_decode((string)$r['context_json'],true)?:[];$ci=$c['cities']??[];$ca=$c['categories']??[];$se=$c['segments']??[];$ct=$c['category_tree']??[];if($city!==''&&!in_array($city,$ci,true))$ci[]=$city;if($cat!==''&&!in_array($cat,$ca,true))$ca[]=$cat;if($seg!==''&&!in_array($seg,$se,true))$se[]=$seg;foreach($tree as$n)if(!in_array($n,$ct,true))$ct[]=$n;$w=(int)$r['wins']+($st==='winner');$l=(int)$r['losses']+($st==='loser');$in=(int)$r['inconclusive']+($st==='inconclusive');$os=(int)$r['samples'];$ns=$os+max(0,$sm);$al=$ns?(float($r['avg_lift'])*$os+$lift*max(0,$sm))/$ns:$lift;$cf=($w+1)/max(2,$w+$l+2);$sc=$this->evaluateMemoryScope($w,$l,$ns,$al,count($ci),count($ca));$ac=$w>=3&&$cf>=.7&&$al>0?1:0;$ctx=['cities'=>$ci,'categories'=>$ca,'segments'=>$se,'category_tree'=>$ct,'last_context'=>$x];$this->q("UPDATE tdb_memoria_padroes SET wins=?,losses=?,inconclusive=?,samples=?,avg_lift=?,cities_tested=?,categories_tested=?,confidence=?,scope=?,active=?,context_json=?,last_win_at=IF(?='winner',NOW(),last_win_at) WHERE pattern_key=?",[$w,$l,$in,$ns,$al,count($ci),count($ca),$cf,$sc,$ac,$this->j($ctx),$st,$p]);} private function evaluateMemoryScope(int $w,int $l,int $s,float $a,int $c,int $k):string{$x=$w*2-$l*3+$s/100+$a*1000+$c*10+$k*10;return$x>200&&$c>=5&&$k>=3?'global':($x>80&&$k>=2?'cluster':'local');} public function createVersion(int $id,array $d,string $by='brain'):int{$v=(int)$this->one("SELECT COALESCE(MAX(version_number),0)+1 FROM tdb_versoes_url WHERE url_id=?",[$id]);$this->q("INSERT INTO tdb_versoes_url(url_id,version_number,title,meta_description,h1,content,cta_json,modules_json,hypothesis_json,generated_by) VALUES(?,?,?,?,?,?,?,?,?,?)",[$id,$v,$d['title']??'',$d['meta_description']??'',$d['h1']??'',$d['content']??'',$this->j(is_array($d['cta']??null)?$d['cta']:[]),$this->j(is_array($d['modules']??null)?$d['modules']:[]),$this->j(is_array($d['hypothesis']??null)?$d['hypothesis']:[]),$by]);return(int)$this->db->lastInsertId();} public function activeVersion(int $id):?array{$r=$this->row("SELECT * FROM tdb_versoes_url WHERE url_id=? AND active=1 ORDER BY id DESC LIMIT 1",[$id]);return$r?:null;} public function promoteVersion(int $v):void{$id=(int)$this->one("SELECT url_id FROM tdb_versoes_url WHERE id=?",[$v]);if(!$id)return;$this->db->beginTransaction();try{$this->q("UPDATE tdb_versoes_url SET active=0 WHERE url_id=?",[$id]);$this->q("UPDATE tdb_versoes_url SET active=1 WHERE id=?",[$v]);$this->db->commit();}catch(Throwable $e){$this->db->rollBack();throw$e;}} public function createExperiment(int $id,string $t,string $ch,string $p,string $h,int $a,int $b,string $m='ctr'):int{if(!in_array($t,['seo_temporal','cro_split'],true))throw new InvalidArgumentException('Experimento inválido.');$ph=$t==='cro_split'?'split':'control';$this->q("INSERT INTO tdb_experimentos(url_id,tipo,change_type,pattern_key,hypothesis,control_version_id,test_version_id,metric_primary,status,phase,phase_changed_at) VALUES(?,?,?,?,?,?,?,?,'running',?,NOW())",[$id,$t,$ch,$p,$h,$a,$b,$m,$ph]);return(int)$this->db->lastInsertId();} private function canStartExperimentInCluster(string $k):bool{if(!$k)return true;$t=(int)$this->one("SELECT COUNT(*) FROM tdb_urls WHERE cluster_key=?",[$k]);$r=(int)$this->one("SELECT COUNT(*) FROM tdb_experimentos e JOIN tdb_urls u ON u.id=e.url_id WHERE u.cluster_key=? AND e.status='running'",[$k]);return$rc['brain']['cluster_concurrency_rate']));} private function sampleConfidence(int $s,float $v):array{$r=$v<.15?750:($v<.35?1500:3000);return['confidence'=>min(.99,$s/$r),'required'=>$r,'force_inconclusive'=>$v>=.5&&$s<$r];} private function metricForPeriod(int $id,string $m,string $a,string $b):float{$x=['ctr'=>'IF(SUM(impressoes)>0,SUM(cliques)/SUM(impressoes),0)','position'=>'IF(SUM(impressoes)>0,SUM(posicao*impressoes)/SUM(impressoes),99)','impressions'=>'SUM(impressoes)','clicks'=>'SUM(cliques)','leads'=>'SUM(leads)','whatsapp'=>'SUM(whatsapp_clicks)','pageviews'=>'SUM(pageviews)','engagement'=>'IF(SUM(sessions)>0,SUM(engaged_sessions)/SUM(sessions),0)','scroll'=>'AVG(scroll_avg)','active_time'=>'AVG(active_time_avg)'];$e=$x[$m]??$x['ctr'];return(float)$this->one("SELECT $e FROM tdb_metricas_url WHERE url_id=? AND data>=DATE(?) AND data<=DATE(?)",[$id,$a,$b]);} private function croVariantMetrics(int $e,string $v,string $m):array{$r=$this->row("SELECT COUNT(DISTINCT IF(evento='pageview',session_id,NULL)) sessions,SUM(evento='whatsapp_click') whatsapp,SUM(evento='phone_click') phone,SUM(evento='lead') leads,COUNT(DISTINCT IF((evento='active_time' AND valor>=20)OR(evento='scroll' AND valor>=40)OR evento IN('whatsapp_click','phone_click','lead','internal_search'),session_id,NULL)) engaged FROM tdb_eventos WHERE experiment_id=? AND variant=?",[$e,$v]);$s=max(1,(int)($r['sessions']??0));$z=match($m){'leads'=>(int)($r['leads']??0)/$s,'engagement'=>(int)($r['engaged']??0)/$s,default=>(int)($r['whatsapp']??0)/$s};return['value'=>$z,'sessions'=>$s,'raw'=>$r];} public function analyze():array{$rep=['urls_analyzed'=>0,'experiments_created'=>0,'experiments_checked'=>0,'winners'=>0,'losers'=>0,'inconclusive'=>0,'rollbacks'=>0];foreach($this->all("SELECT * FROM tdb_urls")as$u){$s=$this->calculateScore((int)$u['id']);$rep['urls_analyzed']++;if(!in_array($s['decision'],['AGGRESSIVE','EXPERIMENT'],true)||!$this->canStartExperimentInCluster((string)$u['cluster_key'])||(int)$this->one("SELECT COUNT(*) FROM tdb_experimentos WHERE url_id=? AND status='running'",[$u['id']]))continue;foreach($this->generateHypotheses((int)$u['id'],$s)as$h){if(!$this->canAutoApply((string)$h['type'],$s))continue;$a=$this->activeVersion((int)$u['id']);if(!$a)continue;$b=$this->createVersion((int)$u['id'],$this->buildVariantFromHypothesis($a,$h,$u),'brain_v1.8.6');$cro=in_array((string)$h['type'],['cta','bloco','dynamic_block'],true);$this->createExperiment((int)$u['id'],$cro?'cro_split':'seo_temporal',(string)$h['type'],(string)($h['pattern']??'autonomous'),(string)$h['reason'],(int)$a['id'],$b,(string)($h['expected_metric']??($cro?'whatsapp':'ctr')));$rep['experiments_created']++;break;}} $e=$this->all("SELECT e.*,u.cidade,u.categoria,u.segmento,u.cluster_key,s.volatility FROM tdb_experimentos e JOIN tdb_urls u ON u.id=e.url_id LEFT JOIN tdb_score_url s ON s.url_id=e.url_id WHERE e.status='running'");foreach($e as$x){$rep['experiments_checked']++;$d=(time()-strtotime((string)$x['started_at']))/86400;$md=(int)$this->c['brain']['min_experiment_days'];if($x['tipo']==='seo_temporal'){if($x['phase']==='control'&&$d>=$md){$b=$this->metricForPeriod((int)$x['url_id'],(string)$x['metric_primary'],(string)$x['started_at'],date('Y-m-d H:i:s'));$this->promoteVersion((int)$x['test_version_id']);$this->q("UPDATE tdb_experimentos SET baseline=?,phase='test',phase_changed_at=NOW() WHERE id=?",[$b,$x['id']]);continue;}if($x['phase']==='test'){if((time()-strtotime((string)$x['phase_changed_at']))/86400<$md)continue;$r=$this->metricForPeriod((int)$x['url_id'],(string)$x['metric_primary'],(string)$x['phase_changed_at'],date('Y-m-d H:i:s'));$this->finishExperiment($x,(float)$x['baseline'],$r,$this->periodSample((int)$x['url_id'],(string)$x['phase_changed_at'],date('Y-m-d H:i:s')),$rep);}}elseif($d>=$md){$a=$this->croVariantMetrics((int)$x['id'],'A',(string)$x['metric_primary']);$b=$this->croVariantMetrics((int)$x['id'],'B',(string)$x['metric_primary']);$sm=min((int)$a['sessions'],(int)$b['sessions']);if($sm>=50)$this->finishExperiment($x,(float)$a['value'],(float)$b['value'],$sm,$rep);}}return$rep;} private function buildVariantFromHypothesis(array $a,array $h,array $u):array{$v=['title'=>$a['title'],'meta_description'=>$a['meta_description'],'h1'=>$a['h1'],'content'=>$a['content'],'cta'=>json_decode((string)($a['cta_json']??'{}'),true)?:[],'modules'=>json_decode((string)($a['modules_json']??'{}'),true)?:[],'hypothesis'=>$h];$c=(string)($u['cidade']??'');$cat=(string)($u['categoria']??'');switch($h['type']){case'cta':$v['cta']['primary_text']=$this->ctaForCategory($cat,$c);break;case'dynamic_block':case'bloco':$v['modules']['intent_block']=['enabled'=>true,'target_query'=>$h['target_query']??'','title'=>$cat.' abertos agora em '.$c,'requires_evidence'=>$h['evidence_required']??[]];break;case'faq':$v['modules']['faq_intent'][]=['query'=>$h['target_query']??'','evidence_only'=>true];break;case'meta':$v['meta_description']=trim("$cat em $c. Encontre opções locais, contato e informações verificadas.");break;case'h1':$v['h1']=trim("$cat em $c");break;case'title':$v['title']=trim("$cat em $c | Top do Bairro");break;case'content_30pct':$v['modules']['semantic_enrichment']=['enabled'=>true,'target_query'=>$h['target_query']??'','evidence_only'=>true,'category_tree'=>$this->getCategoryTree($cat,(string)($u['segmento']??''))];}return$v;} private function ctaForCategory(string $c,string $city):string{$n=$this->n($c);if(in_array($n,['eletricista','chaveiro','encanador','desentupidora'],true))return"Solicitar atendimento em $city";if(in_array($n,['pizzaria','restaurante','hamburgueria','lanchonete'],true))return"Ver opções em $city";if(in_array($n,['dentista','clinica'],true))return"Ver contatos em $city";return"Encontrar opções em $city";} private function periodSample(int $id,string $a,string $b):int{return max(0,(int)$this->one("SELECT GREATEST(COALESCE(SUM(sessions),0),COALESCE(SUM(impressoes),0)) FROM tdb_metricas_url WHERE url_id=? AND data>=DATE(?) AND data<=DATE(?)",[$id,$a,$b]));} private function finishExperiment(array $e,float $a,float $b,int $sm,array &$rep):void{$lift=$a>0?($b-$a)/$a:0;$cf=$this->sampleConfidence($sm,(float)($e['volatility']??0));$ml=(float)$this->c['brain']['min_lift'];$st=$cf['force_inconclusive']?'inconclusive':($lift>=$ml&&$cf['confidence']>=.8?'winner':($lift<=-$ml&&$cf['confidence']>=.8?'loser':'inconclusive'));if($st==='winner'){$this->promoteVersion((int)$e['test_version_id']);$rep['winners']++;}elseif($st==='loser'){if($this->c['brain']['auto_rollback']&&$lift<=-(float)$this->c['brain']['rollback_threshold']){$this->promoteVersion((int)$e['control_version_id']);$rep['rollbacks']++;}$rep['losers']++;}else{if($e['tipo']==='seo_temporal')$this->promoteVersion((int)$e['control_version_id']);$rep['inconclusive']++;}$this->recordPatternResult((string)($e['pattern_key']?:'unknown'),$st,$lift,$sm,(string)($e['cidade']??''),(string)($e['categoria']??''),(string)($e['segmento']??''),['experiment_id'=>(int)$e['id'],'change_type'=>$e['change_type'],'metric'=>$e['metric_primary'],'volatility'=>(float)($e['volatility']??0)]);$this->q("UPDATE tdb_experimentos SET status=?,result=?,improvement=?,confidence=?,ended_at=NOW() WHERE id=?",[$st,$b,$lift,$cf['confidence'],$e['id']]);} public function saveFeedback(array $f):int{$this->q("INSERT INTO tdb_feedback(empresa_id,url_id,tipo,atributo,mensagem,suggested_value,confidence) VALUES(?,?,?,?,?,?,?)",[$f['empresa_id']??null,$f['url_id']??null,$f['tipo']??'correcao',$f['atributo']??null,$f['mensagem']??'',$f['suggested_value']??null,(float)($f['confidence']??.7)]);return(int)$this->db->lastInsertId();} public function getUrlRecord(int $id):array{return$this->row("SELECT * FROM tdb_urls WHERE id=?",[$id]);} public function ensureInitialVersion(int $id,array $p):int{$a=$this->activeVersion($id);if($a)return(int)$a['id'];$v=$this->createVersion($id,$p,'template_original');$this->promoteVersion($v);return$v;} public function pageState(int $id):array{$a=$this->activeVersion($id);$e=$this->getRunningCROExperiment($id);$s=['version'=>$a,'experiment'=>null,'variant'=>null];if($e){if(session_status()!==PHP_SESSION_ACTIVE)session_start();$_SESSION['tdb_sid']??=bin2hex(random_bytes(16));$v=$this->assignCROVariant((int)$e['id'],(string)$_SESSION['tdb_sid']);$s['experiment']=$e;$s['variant']=$v;$x=$this->row("SELECT * FROM tdb_versoes_url WHERE id=?",[(int)($v==='B'?$e['test_version_id']:$e['control_version_id'])]);if($x)$s['version']=$x;}return$s;} public function htmlExperimentAttributes(array $s):string{return empty($s['experiment'])?'':' data-experiment-id="'.(int)$s['experiment']['id'].'" data-variant="'.htmlspecialchars((string)$s['variant'],ENT_QUOTES,'UTF-8').'"';} public function dashboard(int $l=100):array{return$this->all("SELECT u.url,u.cidade,u.categoria,u.cluster_key,s.opportunity_score,s.volatility,s.decision,s.calculated_at FROM tdb_urls u LEFT JOIN tdb_score_url s ON s.url_id=u.id ORDER BY s.opportunity_score DESC LIMIT ".max(1,min(500,$l)));} } $TDB_BRAIN=null; try{$d=$TDB_CONFIG['db'];$pdo=new PDO("mysql:host={$d['host']};dbname={$d['name']};charset={$d['charset']}",$d['user'],$d['pass'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES=>false]);$TDB_BRAIN=new TopDoBairroBrain($pdo,$TDB_CONFIG);}catch(Throwable $e){error_log('TDB Brain: '.$e->getMessage());} if($TDB_BRAIN&&isset($_GET['tdb_action'])){$a=(string)$_GET['tdb_action'];try{ if($a==='event')$TDB_BRAIN->receiveEvent(); if($a==='gsc_auth')$TDB_BRAIN->gscAuthStart(); if($a==='gsc_callback')$TDB_BRAIN->gscCallback(); if(in_array($a,['cron_events','cron_gsc','cron_analyze','dashboard'],true)){$k=(string)($_GET['key']??'');if(!$k||!hash_equals((string)$TDB_CONFIG['brain']['cron_key'],$k)){http_response_code(403);exit('Forbidden');}header('Content-Type: application/json; charset=utf-8');$r=match($a){'cron_events'=>['ok'=>true,'aggregated'=>$TDB_BRAIN->aggregateAllYesterday()],'cron_gsc'=>['ok'=>true,'gsc'=>$TDB_BRAIN->fetchGSC((int)$TDB_CONFIG['brain']['gsc_days_back'])],'cron_analyze'=>['ok'=>true,'analysis'=>$TDB_BRAIN->analyze()],default=>['ok'=>true,'dashboard'=>$TDB_BRAIN->dashboard()]};echo json_encode($r,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE);exit;} }catch(Throwable $e){error_log('TDB Brain route: '.$e->getMessage());http_response_code(500);header('Content-Type: application/json');echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);exit;}}

baterias barreiro bh , baterias barreiro , baterias barreiro de baixo , baterias barreiro de cima

(31)99999-9999


Top do Bairro® Marca registrada - todos os direitos reservados | Segunda 14/09/2026