TubeLens

Versioning · Keep a Changelog

Everything that changed.

Transparent change history of TubeLens, from the first public version to today. Each entry lists what was added, changed, fixed or migrated.

Current version v0.3.0

Added

  • TLR · TubeLens Editorial Rating — three-axis classification system. Every analysis now ships with the Lupometer (editorial quality in 4 states: Certified → Clear → Neutral → Red), the Suggested Age tier (5 levels — 0+ / 10+ / 13+ / 16+ / 18+ — derived from 12 content signals), and the Editorial Seal (Full · Partial · None, inspired by FCC §73.1212 and FTC Endorsement Guides). Full spec in docs/tlr-methodology.md. Inspired by public IARC principles, without affiliation or endorsement.
  • Self-critique pass in the analysis pipeline — a second LLM call verifies conformity with the calibration traps and emits structured corrections that are applied deterministically in code.
  • 5 TLR SEO pages: /safe-for-kids, /teen-safe, /adult-only, /editorial-pleno, /certified-videos.
  • Inline TLR filters in /rankings/videos and /rankings/channels (age tier · editorial seal · lupometer). Plain GET form preserving existing filters.
  • TlrViewsNav — 5 chips at the top of /rankings, /rankings/videos, /rankings/channels, /channels, and /category/[c].
  • "Copy analysis" button producing a formatted text ready to paste as a YouTube comment. Chrome extension (v0.4.0) has the equivalent.
  • JSON-LD audience.suggestedMinAge + contentRating on the Review's itemReviewed (VideoObject) — recognised by Google and Bing as age suitability.
  • Channel-level TLR aggregation: 5 new columns on channels (dominant_age_tier, dominant_editorial_seal, dominant_lupometer_state, tlr_certified_count, tlr_clear_or_better_pct) with an automatic trigger on video_analyses and full backfill.
  • Admin panel at /admin/tlr with distributions across the three axes and frequency of the 12 suitability signals.
  • INOSX institutional band in the global footer (support@inosx.com · +1 (302) 457-7323 · "by INOSX" → inosx.com).
  • Floating version badge at the bottom-right of every page, with a popover summarising the latest releases and a link to a public /changelog page.

Changed

  • AI provider migrated from OpenRouter free models (nemotron, qwen3-next) to OpenAI directly (gpt-4.1-mini via @ai-sdk/openai). Free models hit Vercel function timeouts with the expanded TLR prompt; latency dropped to ~3–5s per analysis. OpenRouter is kept as an emergency fallback.
  • Calibration softening pass (2026-05-14) — after detecting excessive compression (80% of analyses falling into weak), trap ceilings were softened by +1 point on credibility/density/originality, the "default to lower when unsure" trap was rewritten as "score honestly across the full range", and the critic now applies a tolerance of 1.0 (sub-point deltas are treated as noise rather than inflation).
  • Lupometer lup_blurred renamed from "Lupa Embaçada / Blurred Lens / Lupa Empañada" to "Lupa Vermelha / Red Lens / Lupa Roja". The previous label read as a defect of the instrument (the magnifying glass), not the content.
  • Verdict UI reinforced on /analyze/[videoId]: the score number and the seal are now rendered in a gradient color-coded by score, the mascot grew from 96 to 112px, and the LupometerChip is shown below the seal.
  • LLM schema: the suitability_signals array is now mandatory and always emits all 12 signals with intensity 0–5. Schema is compatible with OpenAI structured outputs (no .optional() on required fields).
  • Public pages updated with the TLR narrative: /, /about, /pricing, /founders, /methodology, /pro, /extension, /sources, /press. Pricing tier features include TLR access (Reader: full classification; Editor: advanced filters).
  • Chrome extension bumped from 0.3.0 → 0.4.0 with full TLR in the popup and an updated share template.

Migrations applied

  • tlr_v0_1_columns · rank_videos_tlr · channel_tlr_aggregates · recompute_channel_tlr_stats · rank_channels_tlr

Changed

  • Visual palette repositioned — investigative/Bellingcat-inspired vibe (initially staged for validation): - Background deep navy (#0a1929) replaces pure near-black. - Foreground warm cream (#f4ead5) replaces pure white — editorial vibe. - Primary accent copper (#cc7849) with gradient end copper-bright (#e88f4a) replaces violet/fuchsia. - Gold (#d4a574) as secondary accent on clip-text headings and highlighted text. - Red/amber/green preserve their functional semantics (Avoid/Acceptable/Recommended). - Logo, mascot (web + extension), and 42+ hardcoded occurrences of violet-* / fuchsia-* migrated to CSS vars (var(--accent), var(--accent-2), var(--gold), var(--accent-deep)). - Chrome extension bumped 0.2.2 → 0.3.0 (public zip regenerated). - Strategic rationale: YouTube's black + red would conflict with the "popularity ≠ quality" positioning and risk trademark dilution. The investigative palette (navy + copper + cream) reinforces the editorial / research-grade vibe aligned with the thesis.

Added

  • Public category pages at /[locale]/category/[category] — 11 categories × 3 locales = 33 SEO-friendly indexable URLs. Each shows top 10 best and worst channels + top 10 best and worst videos for the category, with 4 ItemList JSON-LD blocks (strong signal for Google and AI crawlers), search-optimised heading ("Best and worst [category] channels on YouTube"), cross-navigation to other categories, and a link to the full ranking. Added to the sitemap. "Categories" link in the footer (discovery). Works as an acquisition funnel: anonymous visitor discovers via Google → sees the public ranking → creates an account to run an analysis.
  • Formatted duration and "Short" badge shown across 3 surfaces to close the enriched metadata loop: - /analyze/[videoId]: duration + view count + Short badge in the header (next to the channel name). - /channel/[handle]: duration + Short badge on each video card in the grid. - /history: duration + Short badge on each history card. - Helpers formatDuration() ("1h12m" / "5m32s" / "42s") and formatViews() (locale-compact) in lib/format.ts.
  • Engagement metadata via Innertube (free, no API key). fetchEnrichedMetadata() provides view_count, like_count, duration_sec, published_at, is_short (inferred from duration ≤60s) at no extra cost. The schema gained 5 new columns on video_analyses.
  • <DivergenceCallout> component on /analyze/[videoId] — visible editorial differential per analysis. Renders when there's a divergence between views and score: - Viral but bad (≥500K views + score ≤4.5) → red callout with ⚠ icon. - Hidden gem (≤10K views + score ≥7.5) → green/violet callout with 💎 icon. - Silent when there's no relevant divergence. Metrics never enter the score — this is a post-hoc reading of the relation between the two variables. pt/en/es.
  • Appeals page in the methodology. New /methodology#appeals section documents the public process for a channel owner to contest a seal, score, detected signal, or justification. Covers: who can appeal, what can be appealed (5 items), how to appeal (4 items), turnaround (5 business days) + 4 possible outcomes (re-analysis, annotation, removal, justified retention), and 4 principles (transparency, non-retaliation, good faith, AI-separated process). Added to ToC + FAQ JSON-LD. Good-faith defense against defamation + fair process. pt/en/es.
  • Transcript clause in the privacy policy. Explicitly documents: the full transcript is not stored permanently (24h cache), not publicly displayed (only snippets cited in justifications — fair use), and the procedure for a channel owner to request removal of a cited excerpt. pt/en/es.
  • Footer with links (Methodology, Privacy, Extension, Pro, Contact) + disclaimer "TubeLens is independent, not affiliated with YouTube/Google" + copyright. Meets YouTube brand guidelines and mitigates trademark dilution risk. Applied in pt/en/es.
  • Video embed via youtube-nocookie.com on /analyze/[videoId] instead of a static thumbnail. The user can validate the video before/during reading the analysis. Privacy-enhanced mode (zero YouTube cookies until clicking play). Lazy-loaded.
  • Enriched JSON-LD (Review.positiveNotes/negativeNotes): detected green and red labels become ItemLists in the schema. Unlocks rich results for critical reviews and improves citation by LLMs (ChatGPT, Claude, Perplexity, Gemini).
  • VideoObject.embedUrl pointing to youtube-nocookie.com/embed/{videoId} in the analyse page's schema.org.
  • Magnifying-glass mascot (static 36px) in the seal table of the methodology. Each score band has its own expression (exceptional → stars, recommended → arc-up, acceptable → neutral, weak → eyebrow, avoid → tongue-out). Reinforces the visual identity and helps quick scanning of the scale.
  • Magnifying-glass mascots per seal (5 expressions: exceptional/recommended/acceptable/weak/avoid) replacing the play-triangle inside the lens — preserves the brand silhouette. Animated on /analyze/[videoId], /rankings, /rankings/{channels,videos}, and /channel/[handle]; static on /channels, /history, ChannelCard. Respects prefers-reduced-motion.
  • Chrome extension v0.2.2: animated mascot inside the result card. Vanilla-JS port of ScoreMascot.tsx in extension/lib/mascot.js. Same silhouette, same timing, same color system.
  • staging.tubelens.app domain attached to the develop branch with its own Preview env vars (NEXT_PUBLIC_SITE_URL points to staging).
  • Branded Supabase email templates managed via scripts/update-email-templates.mjs (Management API).
  • INOSX multi-project standard: versioned CLAUDE.md, .claude/settings.json with a PreToolUse(Bash) hook blocking personal email in commits, slash commands (/preflight, /changelog-add, /sync-develop, /promote-prod, /email-audit, /tubelens-credits, /tubelens-redeploy).
  • developmain branch flow formalised with a guided promotion process.

Changed

  • Shorts excluded from rankings and aggregated averages. YouTube Shorts (videos ≤60s) don't fit the 4-dimension rubric (density / clarity / credibility / originality). Filter applied to: - RPC rank_channels (Bayesian channel ranking) - RPC rank_videos (video ranking) - RPC recompute_channel_stats (channel aggregates) - JS query computeChannelMovement (↑↓ movement across periods) - Legacy rows with is_short=NULL pass through (we can't infer retroactively — we assume they weren't Shorts). - Individual Short analyses remain accessible via /analyze/[videoId] — only the rankings exclude them. - Documented in methodology Section 6 with a 3rd rules card (pt/en/es).
  • Score calibration — wave 2 (affects new analyses only): - After wave 1 (commit 5e6cc27): average rose 5.97 → 6.31 (+0.34) and the "Avoid" seal dropped from 15% to 6%, but the ceiling stayed at 8.2 with zero "Exceptional" across 17 analyses. - Wave 2 adds an explicit top-end reinforcement to the prompt with a multi-dimensional criterion: a video earns 9+ when it is well-sourced + didactic + dense + (depth OR originality OR uncommon rigor). Cites 4 known external references as qualitative anchors (Veritasium, 3Blue1Brown, Bellingcat, senior practitioner). - Opposite reinforcement: do not inflate. 5–6 is fine for competent median content. 7 is the floor of "good", not the ceiling.
  • Mascot animation: transform-origin: 50% 50% + transform-box: fill-box — fixes the rotation invisibility from the first attempt with pixel-based origin, which became a bounding-box offset instead of an SVG user-space coordinate.
  • ChannelRankCard now derives the seal from bayesian_score via sealForTotal() and renders a 44px animated mascot.
  • /extension page reads the version directly from manifest.json (single source of truth).
  • Score formula visible in the methodology updated to reflect the new weights (clarity 30%, originality 10%).
  • Brand repositioning: "popularity ≠ quality" as the editorial pillar. Home headline, tagline, SEO meta titles/descriptions, and methodology intro now articulate the differential: TubeLens contests YouTube's popularity metric and prioritises actual quality. Applied in pt/en/es.
  • New "Why TubeLens?" section on the home (3 cards: the algorithm optimises views / we optimise quality / you decide with the right metric).

Fixed

  • Anti-duplicate cache on /api/analyze: defensive re-check before INSERT + handling of unique_violation (23505) as cache hit (instead of 500).
  • Vercel Preview empty env vars caused staging.tubelens.app/[locale] to return 500 due to SSR Supabase crash. 3 of 4 vars copied to Preview; SUPADATA_API_KEY left for manual setup.
  • Extension zip download returned 404: the proxy.ts matcher (next-intl) didn't exclude .zip, so /tubelens-extension.zip was being redirected to /pt/tubelens-extension.zip (which doesn't exist). Added zip|pdf|woff2? to the extension allowlist.

Removed

  • Google OAuth login and references to auth_extra / linked_* in i18n — avoids a dependency on a Google Cloud Console project.

Security

  • support@inosx.com linked to the GitHub account m2f0 (Settings → Emails) unblocks Vercel deploys for commits authored as INOSX.

Added

  • AI-driven critical analysis of YouTube videos: 4 dimensions (density 30%, clarity 25%, credibility 30%, originality 15%) + 28 multilabel signals with intensity 1–5 and justification.
  • Global cache by video_id in video_analyses (PK enforces uniqueness in the database).
  • Transcript cache with 24h TTL in transcript_cache — avoids burning transcript credits on retries.
  • Pages /channels, /channel/[handle], /rankings/{channels,videos} with Bayesian smoothing (C=5, threshold 5.0, minimum 3 videos), periods week/month/year/all with ↑↓ movement.
  • i18n pt/en/es via next-intl and the [locale] segment.
  • Full admin: /admin/{overview,users,analyses,channels,audit} + admin_audit_log table.
  • Chrome extension v0.2.1: one-click analysis from any youtube.com/watch?v=… page, popup with score and seal, share-as-comment (gated to Pro), localisation via _locales/.
  • SEO / GEO: dynamic sitemap.ts (statics + channels + analyses × 3 locales with hreflang), robots.ts permitting 13 AI crawlers, llms.txt + llms-full.txt (Answer.AI spec), JSON-LD per type (WebApplication, TechArticle, Review, FAQPage), dynamic OpenGraph at /[locale]/analyze/[videoId]/opengraph-image.tsx, automatic IndexNow.
  • Server-side auth gates (requireAuth(locale, currentPath)) on /channels, /rankings, /extension, /history, /account. Public pages kept: /, /methodology, /privacy, /pro, /auth/*.
  • /account page with avatar upload (Supabase Storage with per-user-folder RLS), bio (280 chars), social handles (X, Facebook, Instagram, website).
  • Branded Supabase email templates (confirm, reset, magic link, invite, email change, verification code) — managed via scripts/update-email-templates.mjs.
  • Account page + Header with avatar bubble.

Changed

  • Analysis cache hardened against race conditions: defensive re-check of video_analyses before INSERT + handling of unique_violation (23505) as cache hit (instead of 500).
  • Vendor scrub: no user-facing string mentions Claude/Anthropic/Vercel/Supabase/Supadata/Postgres/Zod.
  • Extension v0.2.1: labelName now respects the popup locale (fixes pt/en mixing in shared text).

Removed

  • Google OAuth login: removed to avoid a Google Cloud Console project dependency. Only email/password remained.
  • Supadata getVideo call: replaced by oEmbed (free), saves 1 credit per analysis.

Security

  • RLS enabled on profiles, video_analyses, user_video_analyses, channels, transcript_cache, admin_audit_log.
  • avatars bucket on Supabase Storage with write restricted by storage.foldername(name)[1] = auth.uid()::text.

Format: Keep a Changelog 1.1.0. Versioning: SemVer.

Each version accumulates changes since the previous one. Entries marked "In development" have not been released and may change before shipping.