From 9fc971dc7c4c2444249c1c717667f54528a21af2 Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Mon, 24 Aug 2026 21:59:48 +0530 Subject: [PATCH 01/11] fix(site): correct content links and interface details --- src/components/community/CommunityContent.tsx | 14 +++--- src/components/contact/ContactContent.tsx | 7 +-- src/components/home/Hero.tsx | 43 +++++++++++++++---- src/components/layout/Footer.tsx | 2 +- src/lib/constants.ts | 10 +++++ 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/components/community/CommunityContent.tsx b/src/components/community/CommunityContent.tsx index 1539fe1..ae01ea7 100644 --- a/src/components/community/CommunityContent.tsx +++ b/src/components/community/CommunityContent.tsx @@ -12,9 +12,13 @@ import { FEATURED_CONTRIBUTORS, type ContributorProfile, } from "@/lib/contributors"; -import { GITHUB_REPO_URL } from "@/lib/constants"; +import { + GITHUB_REPO_URL, + GITHUB_ISSUES_URL, + PASSCODES_CONTRIBUTING_URL, +} from "@/lib/constants"; import { formatNumber } from "@/lib/utils"; -import { Star, GitFork, Users, Heart, Code2 } from "lucide-react"; +import { Star, GitFork, Users, BookOpen, Code2 } from "lucide-react"; import { GithubIcon } from "@/components/ui/BrandIcons"; type ContributorDisplay = ContributorProfile & { avatarUrl?: string }; @@ -176,16 +180,16 @@ export function CommunityContent() {

- + Read Contributing Guide {/* Two-Column Responsive Split Composition */}
- {/* LEFT — PRODUCT STORY (Desktop: Left Column, Mobile: Below Brand Panel) */} -
+ {/* LEFT — PRODUCT STORY (Desktop: Left Column, Mobile: Primary Story Flow) */} +
+ {/* Mobile Brand Identity Anchor (Mobile only: Logo / Product Identity top anchor) */} +
+
+
+ +
+
+
+ + Passcodes + + + {displayVersion} + +
+

+ Android Credential Vault +

+
+
+
+ {/* 1. Release Announcement Pill */} - + {displayVersion} - + Passcodes {latestEntry.version} is live - + - + See what's new{" "}
- {/* RIGHT — BRAND PRESENCE PANEL (Desktop: Right Column, Mobile: Top Hero Anchor) */} -
+ {/* RIGHT — PRODUCT IDENTITY PANEL (Desktop: Right Column, Mobile: Supporting Panel) */} +
{/* Subtle Ambient Radial Accent (Engineered, not glowing blob) */} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 7eae989..689545f 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -134,7 +134,7 @@ export function Footer() { rel="noopener noreferrer" className="text-[var(--footer-muted)] transition-colors hover:text-[var(--text)]" > - License (MIT/GPL) + MIT License
  • diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 85bafa0..2c7f475 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -28,8 +28,18 @@ export const SITE_META = { } as const; export const GITHUB_REPO_URL = `https://github.com/${REPO_PATH}`; +export const GITHUB_ISSUES_URL = `${GITHUB_REPO_URL}/issues`; export const GITHUB_RELEASES_URL = `${GITHUB_REPO_URL}/releases`; export const LICENSE_URL = `${GITHUB_REPO_URL}/blob/main/LICENSE.txt`; + +export const PASSCODES_DOCS_REPO_URL = + "https://github.com/PasscodesApp/Passcodes-Docs"; +export const PASSCODES_DOCS_URL = + "https://passcodesapp.github.io/Passcodes-Docs/"; +export const PASSCODES_CONTRIBUTING_URL = + "https://passcodesapp.github.io/Passcodes-Docs/contributing/CONTRIBUTING/"; +export const CONTRIBUTING_GUIDE_URL = PASSCODES_CONTRIBUTING_URL; + export const USER_GUIDE_URL = "https://passcodesapp.github.io/Passcodes-Docs/user-docs/installing/"; export const DOCS_RELEASE_NOTES_URL = From dcddd08e8b12d7ecc425194f934ff89362f1a7b3 Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Mon, 7 Sep 2026 18:22:05 +0530 Subject: [PATCH 02/11] fix(content): correct security and product claims Signed-off-by: harsha-vardhan-burra --- src/components/home/FeatureGrid.tsx | 16 ++++----- src/components/home/Hero.tsx | 39 ++++++++++++---------- src/components/shared/GithubStarDialog.tsx | 2 +- src/lib/constants.ts | 4 +-- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/components/home/FeatureGrid.tsx b/src/components/home/FeatureGrid.tsx index 76e0d01..81da2fc 100644 --- a/src/components/home/FeatureGrid.tsx +++ b/src/components/home/FeatureGrid.tsx @@ -15,21 +15,21 @@ const features = [ icon: Lock, title: "On-Device Storage", description: - "Your passwords live strictly on your device — never uploaded to any server or cloud database. True sovereignty over your keys.", + "Your passwords live strictly on your device — never uploaded to any server or cloud database. Complete control over your stored credentials.", }, { category: "Privacy", icon: WifiOff, title: "Designed for Offline Privacy", description: - "No internet connection required for vault operations. Zero cloud dependencies and complete local isolation.", + "No internet connection required for core password management. No cloud synchronization of your credentials.", }, { category: "Security", icon: Fingerprint, title: "Biometric Authentication", description: - "Unlock your credential vault instantaneously using device biometrics (fingerprint or face unlock) with PIN fallback.", + "Protect access to Passcodes using device biometrics (fingerprint or face unlock) with configurable auto-lock.", }, { category: "Architecture", @@ -43,14 +43,14 @@ const features = [ icon: Code2, title: "100% Open Source", description: - "Publicly auditable on GitHub. Built with community transparency and reproducible open-source builds.", + "Publicly auditable on GitHub under the MIT License. Built with community transparency and open development.", }, { category: "Portability", icon: RefreshCw, - title: "Encrypted Local Backup", + title: "Import & Export", description: - "Import and export compatibility with Google Passwords and standard CSVs. Easily manage and migrate your vault.", + "Import and export compatible password data using Google Passwords CSV format. Easily manage and migrate your credentials.", }, ]; @@ -64,8 +64,8 @@ export function FeatureGrid() {
    diff --git a/src/components/home/Hero.tsx b/src/components/home/Hero.tsx index 46b1a11..a0c6962 100644 --- a/src/components/home/Hero.tsx +++ b/src/components/home/Hero.tsx @@ -53,7 +53,7 @@ export function Hero() {

    - Android Credential Vault + Android Password Manager

  • @@ -95,15 +95,15 @@ export function Hero() {

    Your passwords,{" "} - stored securely + stored locally {" "} on your device.

    Passcodes is a free, open-source password - manager for Android. Local-first encrypted - storage with zero cloud dependencies and - complete data ownership. + manager for Android. Local-first on-device + storage with no credential cloud synchronization + and complete data ownership.

    @@ -200,7 +200,7 @@ export function Hero() {

    - Android Credential Vault + Android Password Manager

    @@ -216,18 +216,17 @@ export function Hero() { Local-first

    - Your data stays on your device. + No cloud account or server required.

    - Encrypted + On-Device

    - Credentials remain protected - locally. + Credentials stay on your device.

    @@ -243,13 +242,17 @@ export function Hero() { - {/* Supporting Cryptographic / Offline Detail */} -
    - Client-side AES-256-GCM + {/* Supporting Capability Detail */} +
    + Local Storage · - Argon2id Key Derivation + Biometric App Lock + + · + + Auto-Lock
    @@ -267,7 +270,7 @@ export function Hero() {
    {downloadStats ? formatNumber(downloadStats.totalDownloads) - : "1,000+"} + : "—"}
    @@ -287,7 +290,7 @@ export function Hero() { Storage
    - Local Encrypted + Local (SQLite)
    @@ -312,11 +315,11 @@ export function Hero() { diff --git a/src/components/shared/GithubStarDialog.tsx b/src/components/shared/GithubStarDialog.tsx index 81bf69d..aada961 100644 --- a/src/components/shared/GithubStarDialog.tsx +++ b/src/components/shared/GithubStarDialog.tsx @@ -25,7 +25,7 @@ export function GithubStarDialog() {

    Enjoying Passcodes?

    - If Passcodes helps you manage your passwords securely, + If Passcodes helps you manage your passwords locally, consider giving us a star on GitHub. It helps others discover the project! {repoInfo && ( diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2c7f475..192edea 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -19,9 +19,9 @@ export const NAV_ROUTES = [ ] as const; export const SITE_META = { - title: "Passcodes - Secure Password Manager for Android", + title: "Passcodes - Open Source Password Manager for Android", description: - "Passcodes is a free, open-source password manager for Android. Securely store and manage your passwords locally on your device.", + "Passcodes is a free, open-source password manager for Android. Store and manage your passwords locally on your device with no credential cloud sync.", url: "https://passcodesapp.github.io/Passcodes-Website", ogImage: "/og-image.png", twitterHandle: "@PasscodesApp", From 6265d15f22114083078d888f709bc9bd202537e6 Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Mon, 7 Sep 2026 18:22:43 +0530 Subject: [PATCH 03/11] fix(changelog): align release history with verified facts Signed-off-by: harsha-vardhan-burra --- src/app/changelog/page.tsx | 11 ++++--- src/lib/changelog.ts | 63 +++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index 473c297..321f1ff 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -62,15 +62,16 @@ export default function ChangelogPage() {

    - Engineering Journal + Release Highlights

    - Changelog & History + Release Highlights & Milestones

    - Complete timeline of new features, security - hardening, performance optimizations, and - architecture milestones shipped to Passcodes. + Curated milestones, architecture transitions, and + feature updates across Passcodes releases. Complete + historical notes and tags are maintained in our official + documentation and GitHub releases.

    diff --git a/src/lib/changelog.ts b/src/lib/changelog.ts index a61baee..3d3c24d 100644 --- a/src/lib/changelog.ts +++ b/src/lib/changelog.ts @@ -83,7 +83,7 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ "Modern Material UI design system with fluid animations and responsive layout.", "Comprehensive dark theme support with true black and low-contrast palettes.", "Screen capture and screenshot protection across all credential views.", - "Pull-to-refresh vault list and tab navigation improvements.", + "Pull-to-refresh password list and tab navigation improvements.", "Upgraded underlying foundation to Expo SDK 57.", ], sections: [ @@ -99,7 +99,7 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ { title: "Performance & Maintenance", items: [ - "Reduced memory consumption during large vault indexing.", + "Reduced memory consumption when loading large credential lists.", "Optimized bundle size and startup latency.", "Dependency bump to Expo SDK 57.", ], @@ -120,25 +120,25 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ summary: "Biometric authentication (fingerprint/face unlock), Google Passwords CSV import & export, password detail viewer, and data recovery safeguards.", highlights: [ - "Native biometric authentication for unlocking the password vault.", + "Native biometric authentication for unlocking app access.", "Seamless import and export compatibility with Google Passwords CSV.", - "Multi-purpose password detail screen with copy actions and password generator.", + "Multi-purpose password detail screen with quick copy actions.", "Data recovery troubleshooting tools and uninstall data preservation prompt.", ], sections: [ { title: "Security & Auth", items: [ - "Fingerprint and biometric prompt integration with fallback PIN/passcode.", - "Enhanced in-app auto-lock timers.", + "Fingerprint and biometric prompt integration via Android BiometricPrompt.", + "In-app auto-lock locking the app after 2 minutes of background inactivity.", ], }, { - title: "Vault Management", + title: "Credential Management", items: [ "Full import & export pipeline compatible with standard password formats.", "Inline password updating and credential editing.", - "Search and filtering across vault credentials.", + "Search and filtering across stored credentials.", ], }, { @@ -158,24 +158,25 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ { slug: "v3-0-0-alpha", version: "v3.0.0", - title: "v3.0.0 — Expo Ecosystem Architecture Remake", + title: "v3.0.0 — Complete Architecture Rewrite (Expo / React Native)", date: "2026-05-24", releaseType: "Alpha", category: "Architecture", summary: - "Major architectural rewrite migrating Passcodes to the modern Expo ecosystem for fast iteration and cross-platform readiness while preserving Android-first performance.", + "Major from-scratch architectural rewrite transitioning Passcodes from the legacy native Android codebase to Expo and React Native, establishing a clean foundation for core password management.", highlights: [ - "Complete remake and migration to the Expo ecosystem.", - "New state management and local persistent storage architecture.", - "Laid groundwork for modular UI architecture and streamlined EAS workflows.", + "Complete from-scratch rewrite replacing the retired native Android architecture with Expo and React Native.", + "New local persistent storage architecture built on SQLite and Drizzle ORM.", + "Initial baseline release providing core password storage and basic manager functionality.", + "Data recovery tools provided to migrate stored credentials from pre-v3 releases.", ], sections: [ { - title: "Architecture", + title: "Architecture Rewrite", items: [ - "Rewrote the core frontend framework on modern Expo.", - "Setup EAS build configurations and streamlined asset pipelines.", - "Cleaned up legacy build workflows and optimized module tree.", + "Replaced the previous native Kotlin/Compose architecture with Expo (React Native).", + "Transitioned local database layer to expo-sqlite with Drizzle ORM.", + "Setup modern build configurations and streamlined module tree.", ], }, ], @@ -192,18 +193,18 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ releaseType: "Beta", category: "Improvements", summary: - "Biometric authentication integration, dual navigation scaffolding, autofill service improvements, and database migration.", + "Biometric authentication integration, dual navigation scaffolding, and autofill service adjustments in the pre-rewrite native architecture.", highlights: [ - "Biometric authentication in Compose Multiplatform.", - "Autofill service framework enhancements.", - "Database migration to v2 structure.", + "Biometric authentication in Compose Multiplatform (pre-rewrite architecture).", + "Autofill service framework enhancements (preview/experimental).", + "End of support announcement for v2.x codebase ahead of architecture rewrite.", ], sections: [ { title: "Enhancements", items: [ "Dual navigation support (classical & modern screen layouts).", - "Autofill service improvements for seamless credential filling.", + "Experimental autofill adjustments in the native Android module.", "Migrated network client from OkHttp to Ktor.", ], }, @@ -219,11 +220,11 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ releaseType: "Beta", category: "Features", summary: - "Introduction of a unified design system in Compose Multiplatform, SQLite database v2 migration, and initial Android autofill service.", + "Introduction of a unified design system in Compose Multiplatform, SQLite database v2 migration, and preview Android autofill service in the native codebase.", highlights: [ - "Brand new Compose Multiplatform design system.", - "Android Autofill framework integration.", - "Database module migrated to KMP library module.", + "Brand new Compose Multiplatform design system (prior native architecture).", + "Preview Android Autofill framework integration (experimental).", + "Database module migrated to Kotlin Multiplatform library module.", ], githubUrl: "https://github.com/PasscodesApp/Passcodes/releases/tag/v2.0.0", @@ -236,12 +237,12 @@ export const CHANGELOG_ENTRIES: ChangelogEntry[] = [ releaseType: "Stable", category: "Features", summary: - "The inaugural stable release of Passcodes. Offline-first encrypted password storage on Android with zero cloud dependencies.", + "The inaugural stable release of the original native Android application. Offline-first local password storage on device with zero cloud dependencies.", highlights: [ - "Encrypted local SQLite password storage on device.", - "Password generator with customizable length and character sets.", - "Import/Export functionality and Google Passwords CSV compatibility.", - "Full open source release under GPL/MIT.", + "Local SQLite password storage on device via Android Room database.", + "Migrated core application codebase from Java to Kotlin.", + "In-app language and theme switching support with introductory onboarding hints.", + "Full open-source release under the MIT License.", ], githubUrl: "https://github.com/PasscodesApp/Passcodes/releases/tag/v1.0.0", From 86ef57679344a9d841bb549fa980b391e404f52c Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Mon, 7 Sep 2026 18:23:09 +0530 Subject: [PATCH 04/11] feat(downloads): add stable beta alpha release filters Signed-off-by: harsha-vardhan-burra --- src/components/downloads/DownloadCard.tsx | 14 ++++- src/components/downloads/DownloadsContent.tsx | 53 +++++++++++-------- src/components/downloads/ReleaseList.tsx | 37 +++++++++---- src/hooks/useGithubRelease.ts | 5 +- src/lib/github.ts | 51 +++++++++++++++++- 5 files changed, 123 insertions(+), 37 deletions(-) diff --git a/src/components/downloads/DownloadCard.tsx b/src/components/downloads/DownloadCard.tsx index b79ef33..7c2690c 100644 --- a/src/components/downloads/DownloadCard.tsx +++ b/src/components/downloads/DownloadCard.tsx @@ -6,6 +6,7 @@ import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { ArchDownload } from "@/components/downloads/ArchDownload"; import { formatNumber, formatDate, cn } from "@/lib/utils"; +import { classifyRelease, isYankedRelease } from "@/hooks/useGithubRelease"; import type { GithubRelease } from "@/types/github"; export function DownloadCard({ @@ -20,6 +21,8 @@ export function DownloadCard({ (sum, a) => sum + a.download_count, 0 ); + const channel = classifyRelease(release); + const isYanked = isYankedRelease(release); return (
    @@ -27,8 +30,15 @@ export function DownloadCard({

    {release.name || release.tag_name}

    {isLatest && Latest} - {release.prerelease && ( - Pre-release + {isYanked && Yanked} + {channel === "beta" && ( + Beta + )} + {channel === "alpha" && ( + Alpha + )} + {!isLatest && channel === "stable" && ( + Stable )}
    diff --git a/src/components/downloads/DownloadsContent.tsx b/src/components/downloads/DownloadsContent.tsx index 94ca987..e3dd0f4 100644 --- a/src/components/downloads/DownloadsContent.tsx +++ b/src/components/downloads/DownloadsContent.tsx @@ -12,11 +12,19 @@ import { useReleases, isRateLimitError, getLatestStableRelease, + classifyRelease, } from "@/hooks/useGithubRelease"; import { KOMI_STORE_URL, KOMI_BADGE_SRC } from "@/lib/constants"; import Link from "next/link"; -type Status = "all" | "stable" | "prerelease"; +type FilterChannel = "all" | "stable" | "beta" | "alpha"; + +const FILTER_CHANNELS: { id: FilterChannel; label: string }[] = [ + { id: "all", label: "All Releases" }, + { id: "stable", label: "Stable" }, + { id: "beta", label: "Beta" }, + { id: "alpha", label: "Alpha" }, +]; function KomiSection() { const [badgeFailed, setBadgeFailed] = useState(false); @@ -68,7 +76,7 @@ export function DownloadsContent() { error, } = useReleases(); const [query, setQuery] = useState(""); - const [status, setStatus] = useState("all"); + const [status, setStatus] = useState("all"); const latestRelease = useMemo( () => (releases ? getLatestStableRelease(releases) : undefined), @@ -84,8 +92,7 @@ export function DownloadsContent() { r.tag_name.toLowerCase().includes(q) || (r.body || "").toLowerCase().includes(q); const matchS = - status === "all" || - (status === "prerelease" ? r.prerelease : !r.prerelease); + status === "all" || classifyRelease(r) === status; return matchQ && matchS; }); }, [releases, query, status]); @@ -103,7 +110,7 @@ export function DownloadsContent() { as="h1" badge="Verified Binaries" title="Downloads & Releases" - subtitle="Production builds engineered for Android devices with split ABI support. Always free, reproducible, and open source." + subtitle="Production builds engineered for Android devices with split ABI support. Always free and open source." /> @@ -206,22 +213,16 @@ export function DownloadsContent() {
    - {(["all", "stable", "prerelease"] as Status[]).map( - (s) => ( - - ) - )} + {FILTER_CHANNELS.map((item) => ( + + ))}
    {hasControls && ( @@ -230,7 +231,7 @@ export function DownloadsContent() { {filtered.length} {" "} - result{filtered.length === 1 ? "" : "s"} + {filtered.length === 1 ? "release" : "releases"}
    diff --git a/src/components/downloads/ReleaseList.tsx b/src/components/downloads/ReleaseList.tsx index 434a2e8..cd29a76 100644 --- a/src/components/downloads/ReleaseList.tsx +++ b/src/components/downloads/ReleaseList.tsx @@ -3,20 +3,35 @@ import { Download, Calendar, ExternalLink } from "lucide-react"; import { ArchDownload } from "@/components/downloads/ArchDownload"; import { formatNumber, formatDate } from "@/lib/utils"; +import { classifyRelease, isYankedRelease } from "@/hooks/useGithubRelease"; import type { GithubRelease } from "@/types/github"; import Link from "next/link"; -export function ReleaseList({ releases }: { releases: GithubRelease[] }) { +export function ReleaseList({ + releases, + onResetFilters, +}: { + releases: GithubRelease[]; + onResetFilters?: () => void; +}) { if (releases.length === 0) { return (

    - No releases match your search + No releases match your filter

    - Try searching for a different version tag or clearing the - filter. + Try adjusting your search query or channel filter.

    + {onResetFilters && ( + + )}
    ); } @@ -28,6 +43,9 @@ export function ReleaseList({ releases }: { releases: GithubRelease[] }) { (sum, a) => sum + a.download_count, 0 ); + const channel = classifyRelease(release); + const isYanked = isYankedRelease(release); + return (
    @@ -36,13 +54,14 @@ export function ReleaseList({ releases }: { releases: GithubRelease[] }) { {release.name || release.tag_name} - {release.prerelease ? ( - pre - ) : ( - - stable + {isYanked && ( + + yanked )} + + {channel} +

    { return githubFetch(API_ENDPOINTS.repoInfo); } +export type ReleaseChannel = "stable" | "beta" | "alpha"; + +/** + * Check if a release was marked as yanked in its title or tag. + * Follows the repository's convention: e.g. `[YANKED RELEASE]` or `[Yanked Released]`. + */ +export function isYankedRelease(release: GithubRelease): boolean { + const text = `${release.name || ""} ${release.tag_name || ""}`.toLowerCase(); + return text.includes("yanked"); +} + +/** + * Classify a release into Stable, Beta, or Alpha channels based on metadata and naming conventions. + * Primary classification: + * - inspect explicit alpha/beta markers in release title or tag name + * - if not explicitly marked, inspect GitHub `prerelease` flag + * - fallback for pre-releases without alpha/beta keyword defaults to beta + */ +export function classifyRelease(release: GithubRelease): ReleaseChannel { + const text = `${release.name || ""} ${release.tag_name || ""}`.toLowerCase(); + + // Explicit alpha marker in tag or release title (e.g. "v3.0.0 - Alpha", "v1.1.2-alpha") + if (text.includes("alpha")) { + return "alpha"; + } + + // Explicit beta marker in tag or release title (e.g. "v2.1.1 - Beta", "v2.0.0-beta") + if (text.includes("beta")) { + return "beta"; + } + + // Explicit stable marker in title + if (text.includes("stable")) { + return "stable"; + } + + // Pre-release flag fallback: if marked as prerelease on GitHub without explicit keyword, + // classify as beta (safest preview channel fallback) + if (release.prerelease) { + return "beta"; + } + + // Default non-prerelease is stable + return "stable"; +} + /** * Find the latest stable release from a list of releases. * Follows GitHub's /releases/latest semantics: * - not a draft - * - not a prerelease + * - classified as stable (not prerelease, not alpha/beta, not yanked) * - newest according to created_at * Pure function that does not mutate the source array. */ @@ -89,7 +135,8 @@ export function getLatestStableRelease( releases: GithubRelease[] ): GithubRelease | undefined { return releases.reduce((latest, release) => { - if (release.draft || release.prerelease) return latest; + if (release.draft || isYankedRelease(release)) return latest; + if (classifyRelease(release) !== "stable") return latest; if (!latest) return release; return new Date(release.created_at).getTime() > From 5c538a58a4cad7da42a00b44b1b4c08cb9c10ad8 Mon Sep 17 00:00:00 2001 From: harsha-vardhan-burra Date: Mon, 7 Sep 2026 20:13:32 +0530 Subject: [PATCH 05/11] feat(site): complete release sync and visual refinements Signed-off-by: harsha-vardhan-burra --- .../passcodes-docs-trigger.yml | 48 + .../passcodes-release-trigger.yml | 37 + .../workflows/nextjs-build-githubpages.yml | 20 +- package.json | 2 + public/favicon.svg | 8 +- scripts/sync-releases.mjs | 467 +++ src/app/changelog/[slug]/page.tsx | 93 +- src/app/changelog/page.tsx | 137 +- src/components/downloads/DownloadsContent.tsx | 8 +- src/components/home/Hero.tsx | 6 +- src/components/home/LatestUpdates.tsx | 2 +- src/components/visuals/AmbientBackground.tsx | 6 +- src/data/normalized-releases.json | 712 ++++ src/data/raw-changelog.md | 630 +++ src/data/raw-release-notes.md | 451 +++ src/data/raw-releases.json | 3510 +++++++++++++++++ src/lib/changelog.ts | 249 +- src/styles/globals.css | 82 +- 18 files changed, 6175 insertions(+), 293 deletions(-) create mode 100644 .github/upstream-workflows/passcodes-docs-trigger.yml create mode 100644 .github/upstream-workflows/passcodes-release-trigger.yml create mode 100644 scripts/sync-releases.mjs create mode 100644 src/data/normalized-releases.json create mode 100644 src/data/raw-changelog.md create mode 100644 src/data/raw-release-notes.md create mode 100644 src/data/raw-releases.json diff --git a/.github/upstream-workflows/passcodes-docs-trigger.yml b/.github/upstream-workflows/passcodes-docs-trigger.yml new file mode 100644 index 0000000..c4f4165 --- /dev/null +++ b/.github/upstream-workflows/passcodes-docs-trigger.yml @@ -0,0 +1,48 @@ +# Upstream Trigger Workflow for: PasscodesApp/Passcodes-Docs +# Place this file at: .github/workflows/notify-website.yml in PasscodesApp/Passcodes-Docs +# +# Purpose: +# Automatically notifies PasscodesApp/Passcodes-Website whenever the official user changelog +# or release-notes files are modified and pushed, triggering an automated website rebuild. +# +# Path Filtering: +# Only triggers on changes to: +# - docs/user-docs/changelog.md +# - docs/user-docs/release-notes.md +# Unrelated docs changes (e.g. guide edits, installation notes) will NOT trigger website builds. +# +# Required Secret: +# WEBSITE_DISPATCH_TOKEN: A GitHub Fine-grained Personal Access Token (or Classic PAT with `repo` scope) +# configured in Passcodes-Docs repository secrets with permissions: +# - Contents: Read and write (for triggering repository_dispatch on PasscodesApp/Passcodes-Website) + +name: 🚀 Notify Passcodes-Website on Changelog Update + +on: + push: + branches: + - main + - master + paths: + - 'docs/user-docs/changelog.md' + - 'docs/user-docs/release-notes.md' + +jobs: + notify-website: + name: Dispatch Website Rebuild Event + runs-on: ubuntu-latest + + steps: + - name: 📡 Dispatch Event to Passcodes-Website + env: + GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} + run: | + echo "Notifying Passcodes-Website of changelog update in commit ${{ github.sha }}..." + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/PasscodesApp/Passcodes-Website/dispatches \ + -f event_type='passcodes-docs-updated' \ + -F client_payload[commit]='${{ github.sha }}' \ + -F client_payload[ref]='${{ github.ref_name }}' diff --git a/.github/upstream-workflows/passcodes-release-trigger.yml b/.github/upstream-workflows/passcodes-release-trigger.yml new file mode 100644 index 0000000..fece0ff --- /dev/null +++ b/.github/upstream-workflows/passcodes-release-trigger.yml @@ -0,0 +1,37 @@ +# Upstream Trigger Workflow for: PasscodesApp/Passcodes +# Place this file at: .github/workflows/notify-website.yml in PasscodesApp/Passcodes +# +# Purpose: +# Automatically notifies PasscodesApp/Passcodes-Website whenever a new GitHub Release +# is published or updated, triggering an automated website rebuild and GitHub Pages deployment. +# +# Required Secret: +# WEBSITE_DISPATCH_TOKEN: A GitHub Fine-grained Personal Access Token (or Classic PAT with `repo` scope) +# configured in Passcodes repository secrets with permissions: +# - Contents: Read and write (for triggering repository_dispatch on PasscodesApp/Passcodes-Website) + +name: 🚀 Notify Passcodes-Website on Release + +on: + release: + types: [published, edited] + +jobs: + notify-website: + name: Dispatch Website Rebuild Event + runs-on: ubuntu-latest + + steps: + - name: 📡 Dispatch Event to Passcodes-Website + env: + GH_TOKEN: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} + run: | + echo "Notifying Passcodes-Website of release ${{ github.event.release.tag_name }} (${{ github.event.action }})..." + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/PasscodesApp/Passcodes-Website/dispatches \ + -f event_type='passcodes-release-updated' \ + -F client_payload[tag]='${{ github.event.release.tag_name }}' \ + -F client_payload[action]='${{ github.event.action }}' diff --git a/.github/workflows/nextjs-build-githubpages.yml b/.github/workflows/nextjs-build-githubpages.yml index c6ee21a..36bba25 100644 --- a/.github/workflows/nextjs-build-githubpages.yml +++ b/.github/workflows/nextjs-build-githubpages.yml @@ -1,4 +1,8 @@ -# This CI workflow is not use anymore, but is just here! +# Passcodes Website — Automated GitHub Pages Deployment & Upstream Release Sync +# Triggers: +# 1. Direct commits to main +# 2. repository_dispatch events from Passcodes (on release) and Passcodes-Docs (on changelog edit) +# 3. Manual workflow_dispatch runs name: 🚀 Deploy Next.js Based Website to GitHub Pages @@ -7,6 +11,18 @@ on: branches: ["main"] workflow_dispatch: + inputs: + force_sync: + description: "Force live synchronization with upstream sources" + required: false + default: true + type: boolean + + repository_dispatch: + types: + - passcodes-release-updated + - passcodes-docs-updated + - upstream-sync env: BASE_PATH: "/${{ github.event.repository.name }}" @@ -49,6 +65,8 @@ jobs: # npm test - name: 🛠️ Build and Export Site + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | npm run build diff --git a/package.json b/package.json index 0e4e8a2..5dd0e66 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "scripts": { "dev": "next dev", "build": "next build", + "prebuild": "node scripts/sync-releases.mjs", + "sync:releases": "node scripts/sync-releases.mjs", "start": "next start", "lint": "next lint", "preview": "npm run build && serve out", diff --git a/public/favicon.svg b/public/favicon.svg index a3c0431..a44f4e6 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,11 +1,11 @@ - + - + - + - + diff --git a/scripts/sync-releases.mjs b/scripts/sync-releases.mjs new file mode 100644 index 0000000..93ad41a --- /dev/null +++ b/scripts/sync-releases.mjs @@ -0,0 +1,467 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, ".."); + +const DATA_DIR = path.join(rootDir, "src", "data"); +const RAW_RELEASES_FILE = path.join(DATA_DIR, "raw-releases.json"); +const RAW_CHANGELOG_FILE = path.join(DATA_DIR, "raw-changelog.md"); +const RAW_RELEASE_NOTES_FILE = path.join(DATA_DIR, "raw-release-notes.md"); +const OUTPUT_FILE = path.join(DATA_DIR, "normalized-releases.json"); + +const GITHUB_RELEASES_URL = "https://api.github.com/repos/PasscodesApp/Passcodes/releases"; +const CHANGELOG_DOCS_URL = "https://raw.githubusercontent.com/PasscodesApp/Passcodes-Docs/main/docs/user-docs/changelog.md"; +const RELEASE_NOTES_DOCS_URL = "https://raw.githubusercontent.com/PasscodesApp/Passcodes-Docs/main/docs/user-docs/release-notes.md"; + +async function fetchWithTimeout(url, options = {}, timeoutMs = 4000) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const res = await fetch(url, { ...options, signal: controller.signal }); + clearTimeout(timeout); + return res; + } catch (err) { + clearTimeout(timeout); + throw err; + } +} + +async function getSourceData() { + let releases = null; + let changelogMd = null; + let releaseNotesMd = null; + + const isOffline = + process.argv.includes("--offline") || process.env.OFFLINE === "true"; + + if (!isOffline) { + console.log("[sync-releases] Checking GitHub API and Docs repository..."); + + try { + const headers = { + "User-Agent": "Passcodes-Website-Sync", + Accept: "application/vnd.github.v3+json", + }; + const ghToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; + if (ghToken) { + headers["Authorization"] = `Bearer ${ghToken}`; + } + const ghRes = await fetchWithTimeout(GITHUB_RELEASES_URL, { headers }); + if (ghRes.ok) { + const json = await ghRes.json(); + if (Array.isArray(json) && json.length > 0) { + releases = json; + console.log(`[sync-releases] Fetched ${releases.length} releases live from GitHub API.`); + } + } else { + console.warn(`[sync-releases] GitHub API returned status ${ghRes.status}`); + } + } catch (err) { + console.warn(`[sync-releases] Live GitHub API fetch skipped: ${err.message}`); + } + + try { + const clRes = await fetchWithTimeout(CHANGELOG_DOCS_URL); + if (clRes.ok) { + changelogMd = await clRes.text(); + console.log("[sync-releases] Fetched changelog.md live from Passcodes-Docs."); + } + } catch (err) { + console.warn(`[sync-releases] Live changelog.md fetch skipped: ${err.message}`); + } + + try { + const rnRes = await fetchWithTimeout(RELEASE_NOTES_DOCS_URL); + if (rnRes.ok) { + releaseNotesMd = await rnRes.text(); + console.log("[sync-releases] Fetched release-notes.md live from Passcodes-Docs."); + } + } catch (err) { + console.warn(`[sync-releases] Live release-notes.md fetch skipped: ${err.message}`); + } + } else { + console.log("[sync-releases] Offline mode active, skipping network fetch."); + } + + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + + if (!releases && fs.existsSync(RAW_RELEASES_FILE)) { + console.log("[sync-releases] Using local snapshot: raw-releases.json"); + const raw = fs.readFileSync(RAW_RELEASES_FILE, "utf8"); + releases = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw); + } else if (releases) { + fs.writeFileSync(RAW_RELEASES_FILE, JSON.stringify(releases, null, 2), "utf8"); + } + + if (!changelogMd && fs.existsSync(RAW_CHANGELOG_FILE)) { + console.log("[sync-releases] Using local snapshot: raw-changelog.md"); + const raw = fs.readFileSync(RAW_CHANGELOG_FILE, "utf8"); + changelogMd = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw; + } else if (changelogMd) { + fs.writeFileSync(RAW_CHANGELOG_FILE, changelogMd, "utf8"); + } + + if (!releaseNotesMd && fs.existsSync(RAW_RELEASE_NOTES_FILE)) { + console.log("[sync-releases] Using local snapshot: raw-release-notes.md"); + const raw = fs.readFileSync(RAW_RELEASE_NOTES_FILE, "utf8"); + releaseNotesMd = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw; + } else if (releaseNotesMd) { + fs.writeFileSync(RAW_RELEASE_NOTES_FILE, releaseNotesMd, "utf8"); + } + + if (!releases || !changelogMd || !releaseNotesMd) { + throw new Error("[sync-releases] Missing source data. Neither live fetch nor local raw snapshots were available."); + } + + return { releases, changelogMd, releaseNotesMd }; +} + +function parseInternalDetails(text) { + const match = text.match(/```([^`]+)```/); + if (!match) return undefined; + const block = match[1]; + const details = {}; + + const pkg = block.match(/Package Name\s*=\s*"([^"]+)"/); + if (pkg) details.packageName = pkg[1]; + + const expo = block.match(/Expo SDK\s*=\s*(\d+)/); + if (expo) details.expoSdk = parseInt(expo[1], 10); + + const minA = block.match(/Min Android\s*=\s*([^\r\n]+)/); + if (minA) details.minAndroid = minA[1].trim(); + + const maxA = block.match(/Max Android\s*=\s*([^\r\n]+)/); + if (maxA) details.maxAndroid = maxA[1].trim(); + + const vc = block.match(/Version Code\s*=\s*(\d+)/); + if (vc) details.versionCode = parseInt(vc[1], 10); + + const vn = block.match(/Version Name\s*=\s*"([^"]+)"/); + if (vn) details.versionName = vn[1]; + + const db = block.match(/Master Database (?:Schema )?Version\s*=\s*"([^"]+)"/); + if (db) details.masterDbVersion = db[1]; + + return Object.keys(details).length > 0 ? details : undefined; +} + +// Safeguard: removes or rewrites known unsupported security/product claims before presentation. +function sanitizeClaimSafety(text) { + return text + .replace(/\bAES-256(-GCM)?\b/gi, "secure") + .replace(/\bArgon2id?\b/gi, "hashing") + .replace(/\bencrypted\s+(local\s+)?(database|storage|backup|credentials?|room|sqlite)\b/gi, "$1$2") + .replace(/\breproducible\s+builds?\b/gi, "open builds") + .replace(/\b1,?000\+\s+downloads?\b/gi, "active downloads") + .replace(/\bPIN\s+fallback\b/gi, "device authentication") + .replace(/\byour\s+keys\b/gi, "your credentials"); +} + +function inferCategory(sections) { + const titles = sections.map((s) => s.title.toLowerCase()); + if (titles.some((t) => t.includes("breaking") || t.includes("architecture"))) { + return "Architecture"; + } + if (titles.some((t) => t.includes("add") || t.includes("preview"))) { + return "Features"; + } + if (titles.length > 0 && titles.every((t) => t.includes("fix"))) { + return "Fixes"; + } + return "Improvements"; +} + +function cleanMarkdownItem(item) { + const cleaned = item + .replace(/\[\[@([^\]]+)\]\]\([^)]+\)/g, "(@$1)") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/\{:[^}]+\}/g, "") + .replace(/\s+/g, " ") + .trim(); + return sanitizeClaimSafety(cleaned); +} + +function parseChangelogSections(sectionBody) { + const lines = sectionBody.split("\n"); + const sections = []; + let currentTitle = null; + let currentItems = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (line.startsWith("### ")) { + if (currentTitle && currentItems.length > 0) { + sections.push({ title: currentTitle, items: currentItems }); + } + currentTitle = line.replace(/^###\s+/, "").trim(); + currentItems = []; + } else if (line.startsWith("#### ")) { + if (currentTitle && currentItems.length > 0) { + sections.push({ title: currentTitle, items: currentItems }); + } + currentTitle = line.replace(/^####\s+/, "").trim(); + currentItems = []; + } else if (line.startsWith("- ") || line.startsWith("* ")) { + let itemText = line.replace(/^[-*]\s+/, ""); + while ( + i + 1 < lines.length && + lines[i + 1].trim().length > 0 && + !lines[i + 1].trim().startsWith("- ") && + !lines[i + 1].trim().startsWith("* ") && + !lines[i + 1].trim().startsWith("#") + ) { + i++; + itemText += " " + lines[i].trim(); + } + const cleaned = cleanMarkdownItem(itemText); + if ( + !cleaned.startsWith("Checkout Release") && + !cleaned.startsWith("Release Notes") && + !cleaned.startsWith("Full Changelog") && + !cleaned.startsWith("---") + ) { + currentItems.push(cleaned); + } + } + } + + if (currentTitle && currentItems.length > 0) { + sections.push({ title: currentTitle, items: currentItems }); + } + + return sections; +} + +const EDITORIAL_METADATA = { + "v3.2.1": { + title: "v3.2.1 — Split ABIs & Screenshot Prevention", + category: "Improvements", + summary: "Support for split Android ABIs for smaller download sizes, tab navigation back-stack fixes, screenshot prevention toggle, and UI consistency.", + }, + "v3.2.0": { + title: "v3.2.0 — Modern Material UI & Dark Mode", + category: "Features", + summary: "Complete Material 3 UI overhaul, system dark mode support, gesture navigation, and screen capture prevention.", + }, + "v3.1.0": { + title: "v3.1.0 — Biometric Auth & Google Passwords Import/Export", + category: "Features", + summary: "Biometric authentication (fingerprint/face unlock), Google Passwords CSV import & export, password detail viewer, and data recovery safeguards.", + }, + "v3.0.0": { + title: "v3.0.0 — Complete Architecture Rewrite (Expo / React Native)", + category: "Architecture", + isMajor: true, + summary: "Major from-scratch architectural rewrite transitioning Passcodes from the legacy native Android codebase to Expo and React Native, establishing a clean foundation for core password management.", + }, + "v2.1.1": { + title: "v2.1.1 — Biometric Auth & Navigation Scaffolding", + category: "Improvements", + summary: "Biometric authentication integration, dual navigation scaffolding, and autofill service adjustments in the pre-rewrite native architecture.", + }, + "v2.1.0": { + title: "v2.1.0 — Dual Navigation & Android 17 Support", + category: "Architecture", + summary: "Dual navigation architecture supporting both classic and modern interfaces, official Android 17 compatibility, and announcement of the upcoming transition to Expo.", + }, + "v2.0.0": { + title: "v2.0.0 — Compose Multiplatform Design System", + category: "Features", + isMajor: true, + summary: "Brand new Compose Multiplatform design system, SQLite database v2 migration, and preview Android autofill service in the native codebase.", + }, + "v1.2.1": { + title: "v1.2.1 — Version Name Fix & Release Alignment", + category: "Fixes", + summary: "Targeted maintenance release correcting version naming and code metadata from the yanked v1.2.0 release to ensure consistent bug reporting and updates.", + }, + "v1.2.0": { + title: "v1.2.0 — Jetpack Compose Preview UI (Yanked)", + category: "Improvements", + isYanked: true, + yankedReason: "Yanked release: version name was mistakenly kept as v1.1.2-Alpha during release packaging. Superseded immediately by v1.2.1.", + summary: "Early preview of Jetpack Compose UI for basic password operations and improved rendering performance. Explicitly yanked due to version metadata duplication.", + }, + "v1.1.2": { + title: "v1.1.2 — Architecture Cleanup & Multi-Module Refactor", + category: "Architecture", + summary: "Extensive internal code quality refactor, modular multi-module Gradle project structure, and settings schema migration while preserving all local password data untouched.", + }, + "v1.1.1": { + title: "v1.1.1 — CSV Import File Fix", + category: "Fixes", + summary: "Fixes file picker MIME type resolution bug that prevented selecting CSV files during credential import.", + }, + "v1.1.0": { + title: "v1.1.0 — UI Improvements & GitHub Organization Transition", + category: "Features", + summary: "Visual UI refinements on the credential viewer, in-app feature flagging system for preview features, basic update checker, and official transition to the PasscodesApp GitHub organization.", + }, + "v1.0.0": { + title: "v1.0.0 — First Official Stable Release", + category: "Features", + isMajor: true, + summary: "The inaugural stable release of the original native Android application. Offline-first local password storage on device via Room SQLite with zero cloud dependencies.", + }, + "v0.1.0": { + title: "v0.1.0 — Initial Prototype Alpha (Yanked)", + category: "Features", + isMajor: true, + isYanked: true, + yankedReason: "Yanked prototype release: very early premature prototype published under com.passwordmanager. Kept strictly for documentation and project history.", + summary: "The very first proof-of-concept prototype for Passcodes, published under com.passwordmanager. Offline-first local password storage prototype.", + }, +}; + +const ARCHITECTURE_MILESTONE = { + slug: "end-of-android-only", + version: "Architecture Transition", + title: "End of Android-Only Codebase — Migration to Expo & React Native", + date: "2026-05-20", + releaseType: "Stable", + category: "Architecture", + isMilestone: true, + isMajor: true, + summary: "Passcodes officially concluded development on the native Android (Kotlin / Compose) codebase and transitioned to a unified Expo / React Native architecture with local SQLite and Drizzle ORM.", + highlights: [ + "Concluded 20 months of native Android development (v0.1.0 through v2.1.1).", + "Transitioned foundation to Expo and React Native for modern cross-platform capability.", + "Replaced native Room/SQLite schema with expo-sqlite and Drizzle ORM.", + "Maintained 100% offline-first local device credential storage.", + ], + sections: [ + { + title: "Architectural Shift", + items: [ + "Officially retired the legacy Kotlin Multiplatform / native Android codebase.", + "Adopted Expo SDK and React Native runtime for declarative, fluid UI.", + "Migrated local persistence to SQLite using Drizzle ORM.", + "Established unified design language across all supported form factors.", + ], + }, + ], +}; + +async function main() { + const { releases, changelogMd, releaseNotesMd } = await getSourceData(); + + const ghMap = new Map(); + for (const r of releases) { + ghMap.set(r.tag_name, r); + } + + const clSections = changelogMd.split(/(?=^## )/m).filter((s) => s.startsWith("## ")); + const rnSections = releaseNotesMd.split(/(?=^## )/m).filter((s) => s.startsWith("## ")); + + const rnMap = new Map(); + for (const s of rnSections) { + const firstLine = s.split("\n")[0].trim(); + const tagMatch = firstLine.match(/v[\d\.]+/); + if (tagMatch) { + rnMap.set(tagMatch[0], s); + } + } + + const normalizedEntries = []; + + for (const s of clSections) { + const firstLine = s.split("\n")[0].trim(); + + if (firstLine.includes("End Of Android Only")) { + normalizedEntries.push(ARCHITECTURE_MILESTONE); + continue; + } + + const tagMatch = firstLine.match(/v[\d\.]+/); + if (!tagMatch) continue; + + const version = tagMatch[0]; + const ghRelease = ghMap.get(version); + const rnSection = rnMap.get(version) || ""; + + const headerMatch = firstLine.match(/^##\s+(v[\d\.]+)\s*-\s*([A-Za-z]+)(?:\s*\(([^)]+)\))?(?:\s*\[([^\]]+)\])?/i); + const releaseType = headerMatch ? headerMatch[2] : "Alpha"; + const isYankedHeader = firstLine.toLowerCase().includes("yanked"); + + const editorial = EDITORIAL_METADATA[version] || {}; + const isYanked = editorial.isYanked ?? isYankedHeader; + const slug = `${version.replace(/\./g, "-")}-${releaseType.toLowerCase()}${isYanked ? "-yanked" : ""}`; + + let date = ghRelease ? ghRelease.published_at.slice(0, 10) : ""; + if (!date && headerMatch && headerMatch[3]) { + const parsedD = new Date(headerMatch[3]); + if (!isNaN(parsedD.getTime())) { + date = parsedD.toISOString().slice(0, 10); + } + } + if (!date) date = "2026-01-01"; + + let tldr = ""; + const tldrMatch = rnSection.match(/\*\*`?TL;DR`?:?\s*([^]*?)\*\*/i); + if (tldrMatch) { + tldr = tldrMatch[1].replace(/`+/g, "").trim(); + } + + const internalDetails = parseInternalDetails(s) || (rnSection ? parseInternalDetails(rnSection) : undefined); + const parsedSections = parseChangelogSections(s); + + let highlights = []; + for (const sec of parsedSections) { + for (const item of sec.items) { + if (highlights.length < 5) { + highlights.push(item); + } + } + } + + let compareUrl = undefined; + const compareMatch = s.match(/https:\/\/github\.com\/PasscodesApp\/Passcodes\/compare\/[^\s\)]+/); + if (compareMatch) { + compareUrl = compareMatch[0]; + } + + const assetsCount = ghRelease && ghRelease.assets ? ghRelease.assets.length : 0; + const downloadCount = ghRelease && ghRelease.assets ? ghRelease.assets.reduce((sum, a) => sum + (a.download_count || 0), 0) : 0; + + const category = editorial.category || inferCategory(parsedSections); + const isMajor = editorial.isMajor ?? (version.endsWith(".0.0") || releaseType === "Stable"); + + const entry = { + slug, + version, + title: editorial.title || `${version} — ${tldr || "Release Updates"}`, + date, + releaseType, + category, + summary: editorial.summary || (tldr ? `Passcodes ${version}: ${tldr}` : `Passcodes ${version} release update.`), + highlights: highlights.length > 0 ? highlights : undefined, + sections: parsedSections.length > 0 ? parsedSections : undefined, + internalDetails, + tldr: tldr || undefined, + githubUrl: ghRelease ? ghRelease.html_url : `https://github.com/PasscodesApp/Passcodes/releases/tag/${version}`, + compareUrl, + isMajor, + isYanked: isYanked || false, + yankedReason: editorial.yankedReason || undefined, + assetsCount: assetsCount > 0 ? assetsCount : undefined, + downloadCount: downloadCount > 0 ? downloadCount : undefined, + }; + + normalizedEntries.push(entry); + } + + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(normalizedEntries, null, 2), "utf8"); + console.log(`[sync-releases] Successfully generated ${OUTPUT_FILE} with ${normalizedEntries.length} entries.`); +} + +main().catch((err) => { + console.error("[sync-releases] Fatal error:", err); + process.exit(1); +}); diff --git a/src/app/changelog/[slug]/page.tsx b/src/app/changelog/[slug]/page.tsx index 50f15f1..dfd861b 100644 --- a/src/app/changelog/[slug]/page.tsx +++ b/src/app/changelog/[slug]/page.tsx @@ -9,6 +9,7 @@ import { GitCompare, GitCommit, CheckCircle2, + AlertTriangle, } from "lucide-react"; import { CHANGELOG_ENTRIES, @@ -75,16 +76,26 @@ export default function ChangelogEntryPage({

    - - {entry.releaseType} - + {entry.isYanked ? ( + + Yanked Release + + ) : entry.isMilestone ? ( + + Architecture Milestone + + ) : ( + + {entry.releaseType} + + )} @@ -150,8 +161,70 @@ export default function ChangelogEntryPage({ )}
    + + {/* Yanked Notice */} + {entry.isYanked && ( +
    +
    + + Yanked Release Notice +
    +

    + {entry.yankedReason || + "This release has been explicitly yanked from active distribution. It is preserved here strictly for documentation and project history."} +

    +
    + )} + + {/* Technical Build Specifications */} + {entry.internalDetails && ( +
    +

    + Build & Runtime Specifications +

    +
    + {entry.internalDetails.packageName && ( +
    + Package + {entry.internalDetails.packageName} +
    + )} + {entry.internalDetails.expoSdk && ( +
    + Expo SDK + v{entry.internalDetails.expoSdk} +
    + )} + {entry.internalDetails.minAndroid && ( +
    + Min Android + {entry.internalDetails.minAndroid} +
    + )} + {entry.internalDetails.maxAndroid && ( +
    + Max Android + {entry.internalDetails.maxAndroid} +
    + )} + {entry.internalDetails.versionCode && ( +
    + Version Code + {entry.internalDetails.versionCode} +
    + )} + {entry.internalDetails.masterDbVersion && ( +
    + Database Schema + {entry.internalDetails.masterDbVersion} +
    + )} +
    +
    + )} + {/* 2. Article Content (Highlights + Release Sections) */}
    diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index 321f1ff..40dc3f1 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -40,16 +40,22 @@ export default function ChangelogPage() { const matchesVersion = entry.version .toLowerCase() .includes(searchLower); + const matchesTldr = entry.tldr?.toLowerCase().includes(searchLower); const matchesHighlights = entry.highlights?.some((h) => h.toLowerCase().includes(searchLower) ); + const matchesSections = entry.sections?.some((s) => + s.items.some((item) => item.toLowerCase().includes(searchLower)) + ); return ( matchesCategory && (matchesTitle || matchesSummary || matchesVersion || - matchesHighlights) + matchesTldr || + matchesHighlights || + matchesSections) ); }); }, [search, activeCategory]); @@ -176,6 +182,72 @@ export default function ChangelogPage() { {filteredEntries.map((entry, index) => { const isLatest = index === 0 && activeCategory === "All" && !search; + + if (entry.isMilestone) { + return ( + +
    +
    + + Architecture Milestone + + + May 2026 + +
    + +

    + + {entry.title} + +

    + +

    + {entry.summary} +

    + + {entry.highlights && + entry.highlights.length > 0 && ( +
    + {entry.highlights.map( + (h, hIdx) => ( +
    + + + {h} + +
    + ) + )} +
    + )} + +
    + + + Read architectural overview + + + +
    +
    +
    + ); + } + return (
    -
    - - {entry.releaseType} - +
    + {entry.isYanked ? ( + + Yanked + + ) : ( + + {entry.releaseType} + + )} {isLatest && ( Latest )} + {entry.isMajor && !isLatest && ( + + Major + + )}
    @@ -246,6 +329,17 @@ export default function ChangelogPage() {
    + {entry.internalDetails && ( +
    + {entry.internalDetails.expoSdk && ( + Expo SDK {entry.internalDetails.expoSdk} + )} + {entry.internalDetails.minAndroid && ( + Android {entry.internalDetails.minAndroid.split(" ")[0]}+ + )} +
    + )} + {entry.githubUrl && (
    + {entry.isYanked && entry.yankedReason && ( +
    + Release Notice: + {entry.yankedReason} +
    + )} +

    {entry.summary}

    diff --git a/src/components/downloads/DownloadsContent.tsx b/src/components/downloads/DownloadsContent.tsx index e3dd0f4..84befab 100644 --- a/src/components/downloads/DownloadsContent.tsx +++ b/src/components/downloads/DownloadsContent.tsx @@ -181,10 +181,7 @@ export function DownloadsContent() {
    -
    +