# APK Parsing Fix Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Fix APK filename parsing to use version number as delimiter and support configurable environment tabs. **Architecture:** Parse APK filenames by splitting on `_` to extract prefix. If prefix has no underscore → product. If prefix has underscore → extract environment from last segment. Match against configurable tabs (env var), unmatched → other. **Tech Stack:** Next.js, TypeScript, environment variables --- ### Task 1: Add APK_TABS Environment Variable **Files:** - Modify: `lib/constants.ts` **Step 1: Add APK_TABS constant** ```typescript // Add to lib/constants.ts export const APK_TABS = (process.env.APK_TABS || 'dev,sandbox') .split(',') .map(t => t.trim()) .filter(Boolean); ``` **Step 2: Verify no build errors** Run: `npm run build` Expected: Build succeeds **Step 3: Commit** ```bash git add lib/constants.ts git commit -m "feat: add APK_TABS environment variable for configurable tabs" ``` --- ### Task 2: Rewrite APK Parser Logic **Files:** - Modify: `lib/apk-parser.ts` - Modify: `lib/__tests__/apk-parser.test.ts` **Step 1: Write failing tests for new parsing logic** Replace `lib/__tests__/apk-parser.test.ts`: ```typescript import { parseApkFilename, detectEnvironment, getApkTabs } from '../apk-parser'; describe('parseApkFilename', () => { it('should parse product APK (no environment)', () => { const result = parseApkFilename('ft_0_44_bfeddf2d0.apk', '0_44'); expect(result.environment).toBe('product'); expect(result.commit).toBe('bfeddf2d0'); expect(result.isValid).toBe(true); }); it('should parse dev APK', () => { const result = parseApkFilename('ft_dev_0_44_bfeddf2d0.apk', '0_44'); expect(result.environment).toBe('dev'); expect(result.commit).toBe('bfeddf2d0'); expect(result.isValid).toBe(true); }); it('should parse sandbox APK', () => { const result = parseApkFilename('ft_sandbox_0_44_bfeddf2d0.apk', '0_44'); expect(result.environment).toBe('sandbox'); expect(result.commit).toBe('bfeddf2d0'); expect(result.isValid).toBe(true); }); it('should map unknown environment to other', () => { const result = parseApkFilename('ft_lan_0_44_bfeddf2d0.apk', '0_44'); expect(result.environment).toBe('other'); expect(result.commit).toBe('bfeddf2d0'); }); it('should map timeshift to other', () => { const result = parseApkFilename('ft_timeshift_0_42_57ef3a60d.apk', '0_42'); expect(result.environment).toBe('other'); }); it('should handle invalid format without commit', () => { const result = parseApkFilename('ft_dev_0_44.apk', '0_44'); expect(result.environment).toBe('dev'); expect(result.commit).toBeNull(); expect(result.isValid).toBe(false); }); it('should handle completely invalid filename', () => { const result = parseApkFilename('invalid_filename.apk', '0_44'); expect(result.environment).toBe('other'); expect(result.commit).toBeNull(); expect(result.isValid).toBe(false); }); }); describe('detectEnvironment', () => { it('should detect product when no underscore in prefix', () => { expect(detectEnvironment('ft', ['dev', 'sandbox'])).toBe('product'); }); it('should detect configured environment', () => { expect(detectEnvironment('ft_dev', ['dev', 'sandbox'])).toBe('dev'); expect(detectEnvironment('ft_sandbox', ['dev', 'sandbox'])).toBe('sandbox'); }); it('should return other for unconfigured environment', () => { expect(detectEnvironment('ft_lan', ['dev', 'sandbox'])).toBe('other'); expect(detectEnvironment('ft_timeshift', ['dev', 'sandbox'])).toBe('other'); }); it('should return other for empty prefix', () => { expect(detectEnvironment('', ['dev', 'sandbox'])).toBe('other'); }); }); ``` **Step 2: Run tests to verify they fail** Run: `npm test -- lib/__tests__/apk-parser.test.ts` Expected: Tests fail with missing `getApkTabs` and wrong behavior **Step 3: Implement new parsing logic** Replace `lib/apk-parser.ts`: ```typescript import { APK_TABS } from './constants'; export type ApkEnvironment = 'product' | 'dev' | 'sandbox' | 'other'; export interface ParsedApkMetadata { environment: ApkEnvironment; commit: string | null; isValid: boolean; } export function getApkTabs(): string[] { return APK_TABS; } export function detectEnvironment(prefix: string, configuredTabs: string[]): ApkEnvironment { // Empty prefix → other if (!prefix) { return 'other'; } // No underscore means product (e.g., "ft") if (!prefix.includes('_')) { return 'product'; } // Extract environment from last segment after underscore const lastUnderscoreIndex = prefix.lastIndexOf('_'); const env = prefix.substring(lastUnderscoreIndex + 1); // Check if environment is configured if (configuredTabs.includes(env)) { return env as ApkEnvironment; } return 'other'; } export function parseApkFilename(filename: string, version: string): ParsedApkMetadata { // Use version as delimiter (with leading underscore) const delimiter = `_${version}`; const delimiterIndex = filename.indexOf(delimiter); let prefix: string; if (delimiterIndex > 0) { prefix = filename.substring(0, delimiterIndex); } else { // Fallback: can't find version delimiter prefix = ''; } const environment = detectEnvironment(prefix, APK_TABS); // Extract commit ID const commitMatch = filename.match(/([a-f0-9]{7,})\.apk$/); const commit = commitMatch ? commitMatch[1] : null; return { environment, commit, isValid: commit !== null, }; } ``` **Step 4: Run tests to verify they pass** Run: `npm test -- lib/__tests__/apk-parser.test.ts` Expected: All tests pass **Step 5: Commit** ```bash git add lib/apk-parser.ts lib/__tests__/apk-parser.test.ts git commit -m "feat: rewrite APK parser to use version as delimiter" ``` --- ### Task 3: Update API Route to Pass Version **Files:** - Modify: `app/api/apks/route.ts` - Modify: `app/api/__tests__/apks.test.ts` **Step 1: Check existing API test** Read: `app/api/__tests__/apks.test.ts` **Step 2: Update API route to pass version** Modify `app/api/apks/route.ts` line 22: ```typescript // Change from: const parsed = parseApkFilename(apk.name); // To: const parsed = parseApkFilename(apk.name, version); ``` **Step 3: Verify build succeeds** Run: `npm run build` Expected: Build succeeds **Step 4: Commit** ```bash git add app/api/apks/route.ts git commit -m "feat: pass version parameter to APK parser" ``` --- ### Task 4: Update ApkList to Use Configured Tabs **Files:** - Modify: `lib/constants.ts` - Modify: `app/download/ApkList.tsx` - Add: `app/api/config/route.ts` **Step 1: Create config API endpoint** Create `app/api/config/route.ts`: ```typescript import { NextResponse } from 'next/server'; import { APK_TABS } from '@/lib/constants'; export async function GET() { return NextResponse.json({ tabs: APK_TABS, }); } ``` **Step 2: Update ApkList to fetch and use configured tabs** Modify `app/download/ApkList.tsx`: ```typescript 'use client'; import { useState, useEffect } from 'react'; import useSWR from 'swr'; import { type ApkEnvironment } from '@/lib/apk-parser'; import ApkTable from './ApkTable'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); interface Apk { name: string; environment: ApkEnvironment; commit: string | null; size: number; modifiedAt: string; downloadUrl: string; } interface ApkListProps { project: string; version: string; commitId: string; } export default function ApkList({ project, version, commitId }: ApkListProps) { const [configuredTabs, setConfiguredTabs] = useState([]); // Fetch config on mount useEffect(() => { fetch('/api/config') .then(res => res.json()) .then(data => setConfiguredTabs(data.tabs)) .catch(() => setConfiguredTabs(['dev', 'sandbox'])); // fallback }, []); const { data: apks, isLoading, error } = useSWR( `/api/apks?project=${project}&version=${version}&commit=${commitId}`, fetcher ); // Build tabs: product → configured → other const TABS: ApkEnvironment[] = ['product', ...configuredTabs.filter(t => t !== 'product' && t !== 'other') as ApkEnvironment[], 'other']; if (isLoading) { return (
加载中...
); } if (error) { return (
加载失败,请重试
); } if (!apks || apks.length === 0) { return (

未找到匹配的 APK 文件

); } // Group APKs by environment const groupedApks = apks.reduce((acc, apk) => { acc[apk.environment] = [...(acc[apk.environment] || []), apk]; return acc; }, {} as Record); return (
{/* Tab Navigation */}
{/* Tab Content */}
); } ``` Wait, I need to preserve the activeTab state. Let me revise: ```typescript 'use client'; import { useState, useEffect } from 'react'; import useSWR from 'swr'; import { type ApkEnvironment } from '@/lib/apk-parser'; import ApkTable from './ApkTable'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); interface Apk { name: string; environment: ApkEnvironment; commit: string | null; size: number; modifiedAt: string; downloadUrl: string; } interface ApkListProps { project: string; version: string; commitId: string; } export default function ApkList({ project, version, commitId }: ApkListProps) { const [configuredTabs, setConfiguredTabs] = useState([]); const [activeTab, setActiveTab] = useState('product'); // Fetch config on mount useEffect(() => { fetch('/api/config') .then(res => res.json()) .then(data => setConfiguredTabs(data.tabs)) .catch(() => setConfiguredTabs(['dev', 'sandbox'])); // fallback }, []); const { data: apks, isLoading, error } = useSWR( `/api/apks?project=${project}&version=${version}&commit=${commitId}`, fetcher ); // Build tabs: product → configured → other const TABS: ApkEnvironment[] = ['product', ...configuredTabs.filter(t => t !== 'product' && t !== 'other') as ApkEnvironment[], 'other']; if (isLoading) { return (
加载中...
); } if (error) { return (
加载失败,请重试
); } if (!apks || apks.length === 0) { return (

未找到匹配的 APK 文件

); } // Group APKs by environment const groupedApks = apks.reduce((acc, apk) => { acc[apk.environment] = [...(acc[apk.environment] || []), apk]; return acc; }, {} as Record); return (
{/* Tab Navigation */}
{/* Tab Content */}
); } ``` **Step 3: Run build to verify** Run: `npm run build` Expected: Build succeeds **Step 4: Commit** ```bash git add app/api/config/route.ts app/download/ApkList.tsx git commit -m "feat: add config API and update ApkList to use configured tabs" ``` --- ### Task 5: Final Verification **Step 1: Run all tests** Run: `npm test` Expected: All tests pass **Step 2: Run build** Run: `npm run build` Expected: Build succeeds **Step 3: Manual test with dev server** Run: `npm run dev` Test: Navigate to download page, select project and version, verify tabs display correctly --- ## Summary | File | Change | |------|--------| | `lib/constants.ts` | Add `APK_TABS` env var | | `lib/apk-parser.ts` | Rewrite parsing logic with version delimiter | | `lib/__tests__/apk-parser.test.ts` | Update tests | | `app/api/apks/route.ts` | Pass version to parser | | `app/api/config/route.ts` | New endpoint for tab config | | `app/download/ApkList.tsx` | Use configured tabs from API |