Compare commits

..

No commits in common. "main" and "feature/affiliate-links" have entirely different histories.

6 changed files with 5 additions and 109 deletions

View file

@ -54,28 +54,6 @@ SNIPE_DB=data/snipe.db
# own ID; the CF cloud instance uses CF's campaign ID (disclosed in the UI).
# EBAY_AFFILIATE_CAMPAIGN_ID=
# ── LLM inference (vision / photo analysis) ──────────────────────────────────
# circuitforge-core LLMRouter auto-detects backends from these env vars
# (no llm.yaml required). Backends are tried in this priority order:
# 1. ANTHROPIC_API_KEY → Claude API (cloud; requires Paid tier key)
# 2. OPENAI_API_KEY → OpenAI-compatible endpoint
# 3. OLLAMA_HOST → local Ollama (default: http://localhost:11434)
# Leave all unset to disable LLM features (photo analysis won't run).
# ANTHROPIC_API_KEY=
# ANTHROPIC_MODEL=claude-haiku-4-5-20251001
# OPENAI_API_KEY=
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_MODEL=gpt-4o-mini
# OLLAMA_HOST=http://localhost:11434
# OLLAMA_MODEL=llava:7b
# CF Orchestrator — managed inference for Paid+ cloud users (internal use only).
# Self-hosted users leave this unset; it has no effect without a valid allocation token.
# CF_ORCH_URL=https://orch.circuitforge.tech
# ── In-app feedback (beta) ────────────────────────────────────────────────────
# When set, a feedback FAB appears in the UI and routes submissions to Forgejo.
# Leave unset to silently hide the button (demo/offline deployments).

View file

@ -52,7 +52,6 @@ class TrustScorer:
signal_scores, is_dup, seller,
listing_id=listing.id or 0,
listing_title=listing.title,
listing_condition=listing.condition,
times_seen=listing.times_seen,
first_seen_at=listing.first_seen_at,
price=listing.price,

View file

@ -23,9 +23,8 @@ _SCRATCH_DENT_KEYWORDS = frozenset([
"crack", "cracked", "chip", "chipped",
"damage", "damaged", "cosmetic damage",
"blemish", "wear", "worn", "worn in",
# Parts / condition catch-alls (also matches eBay condition field strings verbatim)
# Parts / condition catch-alls
"as is", "for parts", "parts only", "spares or repair", "parts or repair",
"parts/repair", "parts or not working", "not working",
# Evasive redirects — seller hiding damage detail in listing body
"see description", "read description", "read listing", "see listing",
"see photos for", "see pics for", "see images for",
@ -73,7 +72,6 @@ class Aggregator:
seller: Optional[Seller],
listing_id: int = 0,
listing_title: str = "",
listing_condition: str = "",
times_seen: int = 1,
first_seen_at: Optional[str] = None,
price: float = 0.0,
@ -139,9 +137,7 @@ class Aggregator:
)
if photo_hash_duplicate and not is_established_retailer:
red_flags.append("duplicate_photo")
if (listing_title and _has_damage_keywords(listing_title)) or (
listing_condition and _has_damage_keywords(listing_condition)
):
if listing_title and _has_damage_keywords(listing_title):
red_flags.append("scratch_dent_mentioned")
# Staging DB signals

View file

@ -8,7 +8,7 @@ version = "0.1.0"
description = "Auction listing monitor and trust scorer"
requires-python = ">=3.11"
dependencies = [
"circuitforge-core>=0.8.0",
"circuitforge-core",
"streamlit>=1.32",
"requests>=2.31",
"imagehash>=4.3",

View file

@ -80,45 +80,6 @@ def test_suspicious_price_flagged_when_price_genuinely_low():
assert "suspicious_price" in result.red_flags_json
def test_scratch_dent_flagged_from_title_slash_variant():
"""Title containing 'parts/repair' (slash variant, no 'or') must trigger scratch_dent_mentioned."""
agg = Aggregator()
scores = {k: 15 for k in ["account_age", "feedback_count",
"feedback_ratio", "price_vs_market", "category_history"]}
result = agg.aggregate(
scores, photo_hash_duplicate=False, seller=None,
listing_title="Generic Widget XL - Parts/Repair",
)
assert "scratch_dent_mentioned" in result.red_flags_json
def test_scratch_dent_flagged_from_condition_field():
"""eBay formal condition 'for parts or not working' must trigger scratch_dent_mentioned
even when the listing title contains no damage keywords."""
agg = Aggregator()
scores = {k: 15 for k in ["account_age", "feedback_count",
"feedback_ratio", "price_vs_market", "category_history"]}
result = agg.aggregate(
scores, photo_hash_duplicate=False, seller=None,
listing_title="Generic Widget XL",
listing_condition="for parts or not working",
)
assert "scratch_dent_mentioned" in result.red_flags_json
def test_scratch_dent_not_flagged_for_clean_listing():
"""Clean title + 'New' condition must NOT trigger scratch_dent_mentioned."""
agg = Aggregator()
scores = {k: 15 for k in ["account_age", "feedback_count",
"feedback_ratio", "price_vs_market", "category_history"]}
result = agg.aggregate(
scores, photo_hash_duplicate=False, seller=None,
listing_title="Generic Widget XL",
listing_condition="new",
)
assert "scratch_dent_mentioned" not in result.red_flags_json
def test_new_account_not_flagged_when_age_absent():
"""account_age_days=None (scraper tier) must NOT trigger new_account or account_under_30_days."""
agg = Aggregator()

View file

@ -91,17 +91,6 @@
aria-label="Search filters"
>
<!-- Clear all filters only shown when at least one filter is active -->
<button
v-if="activeFilterCount > 0"
type="button"
class="filter-clear-btn"
@click="resetFilters"
aria-label="Clear all filters"
>
Clear filters ({{ activeFilterCount }})
</button>
<!-- eBay Search Parameters -->
<!-- These are sent to eBay. Changes require a new search to take effect. -->
<h2 class="filter-section-heading filter-section-heading--search">
@ -416,7 +405,7 @@ onMounted(() => {
// Filters
const DEFAULT_FILTERS: SearchFilters = {
const filters = reactive<SearchFilters>({
minTrustScore: 0,
minPrice: undefined,
maxPrice: undefined,
@ -435,13 +424,7 @@ const DEFAULT_FILTERS: SearchFilters = {
mustExclude: '',
categoryId: '',
adapter: 'auto' as 'auto' | 'api' | 'scraper',
}
const filters = reactive<SearchFilters>({ ...DEFAULT_FILTERS })
function resetFilters() {
Object.assign(filters, DEFAULT_FILTERS)
}
})
// Parse comma-separated keyword strings into trimmed, lowercase, non-empty term arrays
const parsedMustInclude = computed(() =>
@ -784,27 +767,6 @@ async function onSearch() {
margin-bottom: var(--space-2);
}
/* Clear all filters button */
.filter-clear-btn {
display: flex;
align-items: center;
gap: var(--space-1);
width: 100%;
padding: var(--space-1) var(--space-2);
margin-bottom: var(--space-2);
background: color-mix(in srgb, var(--color-red, #ef4444) 12%, transparent);
color: var(--color-red, #ef4444);
border: 1px solid color-mix(in srgb, var(--color-red, #ef4444) 30%, transparent);
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.filter-clear-btn:hover {
background: color-mix(in srgb, var(--color-red, #ef4444) 22%, transparent);
}
/* Section headings that separate eBay Search params from local filters */
.filter-section-heading {
font-size: 0.6875rem;