- Update .env.example with documentation and default values - Add .resources to gitignore (dummy test files) - Add APK parsing fix implementation plan document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
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 _<version> 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
// 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
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:
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:
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
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:
// 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
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:
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:
'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<string[]>([]);
// 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<Apk[]>(
`/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 (
<div className="text-center py-8 text-gray-500">
加载中...
</div>
);
}
if (error) {
return (
<div className="text-center py-8 text-red-600">
加载失败,请重试
</div>
);
}
if (!apks || apks.length === 0) {
return (
<div className="bg-white rounded-lg shadow-md p-12 text-center">
<p className="text-gray-500">未找到匹配的 APK 文件</p>
</div>
);
}
// Group APKs by environment
const groupedApks = apks.reduce((acc, apk) => {
acc[apk.environment] = [...(acc[apk.environment] || []), apk];
return acc;
}, {} as Record<ApkEnvironment, Apk[]>);
return (
<div>
{/* Tab Navigation */}
<div className="bg-white rounded-t-lg border-b border-gray-200">
<nav className="flex space-x-8 px-6" aria-label="Tabs">
{TABS.map((tab) => {
const count = groupedApks[tab]?.length || 0;
return (
<button
key={tab}
onClick={() => {}}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
false // activeTab logic removed for simplicity
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab} ({count})
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
<div className="mt-4">
<ApkTable apks={groupedApks['product'] || []} />
</div>
</div>
);
}
Wait, I need to preserve the activeTab state. Let me revise:
'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<string[]>([]);
const [activeTab, setActiveTab] = useState<ApkEnvironment>('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<Apk[]>(
`/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 (
<div className="text-center py-8 text-gray-500">
加载中...
</div>
);
}
if (error) {
return (
<div className="text-center py-8 text-red-600">
加载失败,请重试
</div>
);
}
if (!apks || apks.length === 0) {
return (
<div className="bg-white rounded-lg shadow-md p-12 text-center">
<p className="text-gray-500">未找到匹配的 APK 文件</p>
</div>
);
}
// Group APKs by environment
const groupedApks = apks.reduce((acc, apk) => {
acc[apk.environment] = [...(acc[apk.environment] || []), apk];
return acc;
}, {} as Record<ApkEnvironment, Apk[]>);
return (
<div>
{/* Tab Navigation */}
<div className="bg-white rounded-t-lg border-b border-gray-200">
<nav className="flex space-x-8 px-6" aria-label="Tabs">
{TABS.map((tab) => {
const count = groupedApks[tab]?.length || 0;
return (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === tab
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab} ({count})
</button>
);
})}
</nav>
</div>
{/* Tab Content */}
<div className="mt-4">
<ApkTable apks={groupedApks[activeTab] || []} />
</div>
</div>
);
}
Step 3: Run build to verify
Run: npm run build
Expected: Build succeeds
Step 4: Commit
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 |