chore: update env example and gitignore, add implementation plan
- 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>
This commit is contained in:
12
.env.example
12
.env.example
@@ -1,3 +1,9 @@
|
|||||||
AUTH_USERNAME=
|
# Authentication credentials (change in production!)
|
||||||
AUTH_PASSWORD=
|
AUTH_USERNAME=admin
|
||||||
RESOURCE_PATH=
|
AUTH_PASSWORD=changeme
|
||||||
|
|
||||||
|
# Path to APK storage directory
|
||||||
|
RESOURCE_PATH=./resources
|
||||||
|
|
||||||
|
# Comma-separated list of tab names for APK filtering
|
||||||
|
APK_TABS=dev,sandbox
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -40,3 +40,6 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# dumy files for download tests
|
||||||
|
.resources
|
||||||
|
|||||||
532
docs/plans/2026-03-04-apk-parsing-fix.md
Normal file
532
docs/plans/2026-03-04-apk-parsing-fix.md
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
# 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**
|
||||||
|
|
||||||
|
```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<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:
|
||||||
|
|
||||||
|
```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<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**
|
||||||
|
|
||||||
|
```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 |
|
||||||
Reference in New Issue
Block a user