diff --git a/docs/quality/qa-company-profile.json b/docs/quality/qa-company-profile.json
new file mode 100644
index 0000000..e1a41ca
--- /dev/null
+++ b/docs/quality/qa-company-profile.json
@@ -0,0 +1,18 @@
+{
+ "date": "2026-09-06",
+ "environment": "localhost:5182 synthetic local user; external AI disabled",
+ "checks": {
+ "manualWithoutScan": true,
+ "discardUnsaved": true,
+ "saveAcknowledged": true,
+ "reloadPreserved": true,
+ "scanFailureVisible": true,
+ "failurePreservesProduct": true
+ },
+ "limits": [
+ "Manual product add, discard, save, reload and failed scan tested",
+ "Successful scan proposal mapping tested with stub; no real website AI scan",
+ "Structured brand/model/UTS/service-capacity portfolio remains open",
+ "Concurrent profile edits from multiple devices not tested"
+ ]
+}
diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md
index b091445..bfe719d 100644
--- a/docs/quality/uygulama-durumu.md
+++ b/docs/quality/uygulama-durumu.md
@@ -889,3 +889,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Kullanım kılavuzuna grup açıklaması eklendi. Kanban yardımı yeni durum seçimi, kaydetme, hazırlık özeti ve desteklenmeyen Slack/Teams gönderimine göre güncellendi.
- Yerel normal hesapta dört grup, hazırlık panosunun açılması, normal kullanıcıda AI ayarının gizli olması ve yan panel kısayoluyla grubun değişmesi dahil 6 kontrol geçti (qa-navigation-groups.json). Derleme geçti. Önceki 200 otomatik test tekrar çalıştırılmadı; sunucu/hizmet hesabı değişmedi.
- Yeni menünün tam mobil/yalnız-klavye kabulü ve yardım metinlerinin kalan ekranlardaki iddiaları ayrıca incelenmeli. Bu değişiklik gizlenen yönetici araçlarının sunucu yetkilendirmesi yerine geçmez.
+
+### D07 — Firma taramasını düzenlenebilir öneriye dönüştürme — 6 Eylül 2026
+- İncelemede gerçek tarama hatasının alan adından ürün tahminiyle gizlendiği, zamanlayıcının sahte yüzde/başarı ürettiği ve profilin inceleme öncesinde değiştirildiği görüldü. Bu fallback ve otomatik uygulama kaldırıldı.
+- Site taraması ortak zaman aşımı/oturum kontrollü istekle yalnız öneri döndürür; bozuk listeler hata olur. Düzenleme ekranında sektör, ürün/hizmet, sertifika beyanı ve ihale konuları elle eklenebilir/kaldırılabilir. Tarama olmadan boş profilden başlanabilir.
+- Profil ve uyum eşiği yalnız Profili kaydet ile sunucu yanıtı beklendikten sonra uygulama durumuna aktarılır. Eski arka plan profil autosave kaldırıldı. Tarama/yerel düzenleme/kapama kendiliğinden profil yazmaz. Hata mevcut ürünleri korur; modal kapanınca tarama istemcisi iptal edilir, hesap değişiminde yerel taslak ayrılır.
+- Sahte ilerleme yüzdeleri, konfeti, doğrulanmış sektör/ÜTS iddiaları kaldırıldı. Modal başlığı/alanlar/kapama etiketlendi; odak geri dönüşü, Escape ve Tab sınırı eklendi.
+- 202 otomatik test ve derleme geçti. Yerel normal hesapta taramasız ekleme, kaydetmeden kapatma, kayıt onayı, yenileme sonrası kalıcılık, yapılandırılmamış tarama hatası ve mevcut ürünü koruma dahil 6 tarayıcı kontrolü geçti (qa-company-profile.json). Başarılı tarama öneri dönüşümü stub testte doğrulandı; gerçek AI/site taraması yapılmadı.
+- Marka/model, yapılandırılmış ÜTS durumu ve servis kapasitesi portföyü henüz eklenmedi. Çok cihazlı eşzamanlı profil düzenleme ve başarılı gerçek tarama kabulü açık. Profil kaydı için mevcut hesap veri deposu kullanılır; bu tur yeni sunucu şeması yoktur.
diff --git a/src/components/CompanyScannerModal.jsx b/src/components/CompanyScannerModal.jsx
index 5dc5f9a..3c5dec3 100644
--- a/src/components/CompanyScannerModal.jsx
+++ b/src/components/CompanyScannerModal.jsx
@@ -1,313 +1,69 @@
-import React, { useState } from 'react';
-import { useApp } from '../context/AppContext';
-import { Globe, Bot, Sparkles, CheckCircle2, ShieldCheck, Tag, X, Loader2, Database, ArrowRight } from 'lucide-react';
-import confetti from 'canvas-confetti';
-
-export const CompanyScannerModal = () => {
- const { isCompanyScannerOpen, setIsCompanyScannerOpen, userProfile, scanCompanyWebsite, updateCompanyCapabilities, uyumEsigi, setUyumEsigi, gommeDurum } = useApp();
-
- const [urlInput, setUrlInput] = useState(userProfile.website || '');
- const [isScanning, setIsScanning] = useState(false);
- const [scanStep, setScanStep] = useState(0);
- const [scanSuccess, setScanSuccess] = useState(false);
-
- const [newProductInput, setNewProductInput] = useState('');
-
- if (!isCompanyScannerOpen) return null;
-
- const handleStartScan = async (e) => {
- e.preventDefault();
- if (!urlInput.trim()) return;
-
- setIsScanning(true);
- setScanSuccess(false);
- setScanStep(1);
-
- setTimeout(() => setScanStep(2), 600);
- setTimeout(() => setScanStep(3), 1200);
-
- const capabilities = await scanCompanyWebsite(urlInput);
- setIsScanning(false);
- setScanSuccess(true);
- confetti({ particleCount: 70, spread: 60 });
- };
-
- const caps = userProfile.scrapedCapabilities;
-
- const handleRemoveSector = (indexToRemove) => {
- if (!caps?.extractedSectors) return;
- const updated = caps.extractedSectors.filter((_, idx) => idx !== indexToRemove);
- updateCompanyCapabilities({ extractedSectors: updated });
- };
-
- const handleRemoveProduct = (indexToRemove) => {
- if (!caps?.extractedProducts) return;
- const updated = caps.extractedProducts.filter((_, idx) => idx !== indexToRemove);
- updateCompanyCapabilities({ extractedProducts: updated });
- };
-
- const handleRemoveCert = (indexToRemove) => {
- if (!caps?.extractedCertifications) return;
- const updated = caps.extractedCertifications.filter((_, idx) => idx !== indexToRemove);
- updateCompanyCapabilities({ extractedCertifications: updated });
- };
-
- const handleAddCustomProduct = (e) => {
- e.preventDefault();
- if (!newProductInput.trim() || !caps) return;
- const updated = [...(caps.extractedProducts || []), newProductInput.trim()];
- updateCompanyCapabilities({ extractedProducts: updated });
- setNewProductInput('');
- };
-
- return (
-
-
-
- {/* Header */}
-
-
-
-
-
-
-
- Firma Web Sitesi Tarama & Detaylı Ürün Analiz Motoru
-
-
Ürün ve faaliyet alanı önerilerini sitenizden çıkarır; profilinize eklemeden önce doğrulayın.
-
-
-
-
-
- {/* Modal Body */}
-
-
- {/* Strict Sector Protection Banner */}
-
-
-
- Sektör Filtresi Aktif: Sadece web sitenizdeki kesinleşmiş faaliyet alanları ve detaylı ürün kataloğu işlenir.
-
-
- Açıklanabilir ürün ve sektör eşleşmesi
-
-
-
- {/* URL Form */}
-
-
- {/* Scanning Live Indicator */}
- {isScanning && (
-
-
-
-
- Derin Sayfa Metin Analizi ve Ürün Çıkarımı Sürüyor...
-
- %{(scanStep * 33).toFixed(0)}
-
-
-
-
-
-
-
-
= 1 ? 'text-blue-900 font-bold' : 'text-slate-400'}>
- 1. 🌐 {urlInput} alan adına bağlanıldı, harici ve alakasız sektör tanımları elendi.
-
-
= 2 ? 'text-blue-900 font-bold' : 'text-slate-400'}>
- 2. 📄 Web sitesindeki "Hakkımızda", "Ürünler" ve "Katalog" alt sayfalarındaki ürün isimleri ayıklandı.
-
-
= 3 ? 'text-blue-900 font-bold' : 'text-slate-400'}>
- 3. 💾 Firmanın gerçek ürün yelpazesi ve ISO/ÜTS kalite belgeleri İhaleist veritabanına işlendi.
-
-
-
- )}
-
- {scanSuccess && (
-
-
- Web sitenizdeki ürünler ve doğrudan doğrulanmış sektörler veritabanına işlendi! Alakasız sektörler elendi.
-
Ürünlerinizi elle ekleyebilir veya web sitenizden öneri alabilirsiniz. Tarama, ürün uygunluğunu ya da ÜTS/sertifika geçerliliğini doğrulamaz. Kaydetmeden kapatırsanız bu ekrandaki değişiklikler uygulanmaz.
+
+ {busy&&
{busy==='scan'?'Site taranıyor; sonuç bekleniyor…':'Profil kaydediliyor…'}
}
+ {error&&
{error}
}
+ {message&&
{message}
}
+ {caps.ozet&&
Tarama özeti: {caps.ozet}
}
+
+
+
Uyum skoru terim eşleşmesi ve varsa anlamsal yakınlık girdisidir; başvuru yeterliliği değildir.
+
+
+
;
+}
diff --git a/src/context/AppContext.jsx b/src/context/AppContext.jsx
index 21d3f3d..625b445 100644
--- a/src/context/AppContext.jsx
+++ b/src/context/AppContext.jsx
@@ -2,7 +2,7 @@ import {mergeServerNotifications,acknowledgeNotifications} from '../services/not
import { searchTender } from '../services/searchTender';
import { normalizeTenderSearch, matchingItemResults } from '../services/tenderSearch';
import React, {createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
-import { parseWebsiteCapabilitiesDynamically } from '../services/webScraperService';
+import { scanCompanyProposal } from '../services/companyProfile';
import { havuzuPuanla, profilMetni, profilTerimleri } from '../services/uyum';
import { getToken } from '../services/auth';
import { fetchLiveTenders, fetchTenderDetail, mergeUserState } from '../services/tenderApi';
@@ -479,14 +479,6 @@ export const AppProvider = ({ children }) => {
};
const [userProfile, setUserProfile] = useState(() => defaultProfile(getStoredUser()));
- useEffect(() => {
- const veri = {
- company: userProfile.company, website: userProfile.website,
- scrapedCapabilities: userProfile.scrapedCapabilities, uyumEsigi: userProfile.uyumEsigi,
- };
- if (veriSenkron) ilkYazmaDegil('firma_profil', veri);
- }, [userProfile.company, userProfile.website, userProfile.scrapedCapabilities, userProfile.uyumEsigi, veriSenkron]);
- const setUyumEsigi = (v) => setUserProfile((p) => ({ ...p, uyumEsigi: Math.max(0, Math.min(100, Number(v) || 0)) }));
// Anlamsal bonus: profil metni sunucuda gömülür, havuzla kosinüs alınır,
// z-skor → 0..20. Profil terimi yoksa ya da oturum yoksa istenmez. Havuz
@@ -522,47 +514,20 @@ export const AppProvider = ({ children }) => {
[tenders, userProfile.scrapedCapabilities, gommeBonus],
);
- // Önce gerçek tarama (/api/company/scan: site metni + model), başarısızsa
- // alan adından tahmin eden eski yol. Sonuç uyum skorunun girdisi olur.
- const scanCompanyWebsite = async (url) => {
- const session = getToken();
- let yeni = null;
+ const scanCompanyWebsite = scanCompanyProposal;
+ const profileSaveLock=useRef(false);
+ const updateCompanyCapabilities = async (capabilities,{website=userProfile.website,threshold=userProfile.uyumEsigi}={}) => {
+ if(!authUser||!veriSenkron)throw new Error('Hesap verileri yüklenmeden profil kaydedilemez.');
+ if(profileSaveLock.current)throw new Error('Önceki profil kaydı sürüyor.');
+ const session=getToken();
+ profileSaveLock.current=true;
try {
- const r = await fetch('/api/company/scan', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
- body: JSON.stringify({ url }),
- });
- const d = await r.json();
- if (d.success && d.sonuc) {
- const so = d.sonuc;
- yeni = {
- scrapedAt: new Date().toLocaleString('tr-TR'),
- extractedSectors: so.sektorler || [],
- extractedProducts: so.urunler || [],
- extractedCertifications: so.sertifikalar || [],
- ihaleKonulari: so.ihaleKonulari || [],
- ozet: so.ozet || null,
- tarananSayfa: d.tarananSayfa,
- matchScoreBoost: 0,
- gercekTarama: true,
- };
- }
- } catch { /* aşağıda yedek yol */ }
- if (!yeni) yeni = { ...parseWebsiteCapabilitiesDynamically(url), ihaleKonulari: [], gercekTarama: false };
- if(getToken() !== session) throw new Error('Oturum değişti; firma taraması bu hesaba aktarılmadı.');
- setUserProfile((prev) => ({ ...prev, website: url, scrapedCapabilities: yeni }));
- return yeni;
- };
-
- const updateCompanyCapabilities = (updatedCapabilities) => {
- setUserProfile(prev => ({
- ...prev,
- scrapedCapabilities: {
- ...prev.scrapedCapabilities,
- ...updatedCapabilities
- }
- }));
+ const profile={company:userProfile.company,website,scrapedCapabilities:capabilities,uyumEsigi:Math.max(0,Math.min(100,Number(threshold)||0))};
+ await setUserData('firma_profil',profile);
+ if(getToken()!==session)throw new Error('Oturum değişti; profil bu hesaba aktarılmadı.');
+ setUserProfile(previous=>({...previous,...profile}));
+ return profile;
+ }finally{profileSaveLock.current=false;}
};
const [notifications, setNotifications] = useState([]);
@@ -741,7 +706,6 @@ export const AppProvider = ({ children }) => {
setActiveTab,
tenders: tendersSkorlu,
uyumEsigi: userProfile.uyumEsigi,
- setUyumEsigi,
gommeDurum,
setTenders,
isLiveMode,
diff --git a/src/services/companyProfile.js b/src/services/companyProfile.js
new file mode 100644
index 0000000..b6df96e
--- /dev/null
+++ b/src/services/companyProfile.js
@@ -0,0 +1,15 @@
+import {accountRequest,requireObject} from './accountRequest.js';
+export function companyScanProposal(data) {
+ const result=requireObject(data,'sonuc');
+ const array=key=>{
+ if(!Array.isArray(result[key])||result[key].some(value=>typeof value!=='string'||!value.trim()))throw new Error('Tarama önerileri eksik veya geçersiz. Profil değiştirilmedi.');
+ return [...new Set(result[key].map(value=>value.trim()))];
+ };
+ return {scrapedAt:new Date().toLocaleString('tr-TR'),extractedSectors:array('sektorler'),extractedProducts:array('urunler'),
+ extractedCertifications:array('sertifikalar'),ihaleKonulari:array('ihaleKonulari'),ozet:typeof result.ozet==='string'?result.ozet:null,
+ tarananSayfa:data.tarananSayfa,gercekTarama:true,matchScoreBoost:0};
+}
+export async function scanCompanyProposal(url,{signal,request=accountRequest}={}) {
+ const data=await request('/api/company/scan',{method:'POST',timeoutMs:120000,signal,body:JSON.stringify({url})});
+ return companyScanProposal(data);
+}
diff --git a/tests/company-profile.test.mjs b/tests/company-profile.test.mjs
new file mode 100644
index 0000000..0788ce8
--- /dev/null
+++ b/tests/company-profile.test.mjs
@@ -0,0 +1,13 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {scanCompanyProposal,companyScanProposal} from '../src/services/companyProfile.js';
+test('failed company scan throws instead of fabricating domain-derived products',async()=>{
+ await assert.rejects(scanCompanyProposal('https://lab-medical.example',{request:async()=>{throw new Error('Provider unavailable');}}),/Provider unavailable/);
+ for(const sonuc of [{},{urunler:'kit'},{sektorler:[],urunler:[{}],sertifikalar:[],ihaleKonulari:[]}])assert.throws(()=>companyScanProposal({sonuc}));
+});
+test('successful scan is returned as editable source suggestions with no profile write request',async()=>{
+ const calls=[];const result=await scanCompanyProposal('https://example.test',{request:async(url,options)=>{calls.push({url,options});return {sonuc:{sektorler:[' Sağlık ','Sağlık'],urunler:['Kit'],sertifikalar:[],ihaleKonulari:['Laboratuvar']},tarananSayfa:2};}});
+ assert.equal(calls.length,1);assert.equal(calls[0].url,'/api/company/scan');
+ assert.deepEqual(result.extractedSectors,['Sağlık']);assert.deepEqual(result.extractedProducts,['Kit']);
+ assert.equal(result.gercekTarama,true);
+});