# PLAN-HARPGAMER-DOWNLOADS — HC Launcher: HarpGamer Download Catalog Integration

## Context

HC Launcher manages a structured directory tree for game artifacts (PLAN-FILEORGANIZATION), but currently has no way to discover or list what's available for download. The primary source of community-shared content is [harpgamer.com](https://harpgamer.com/harpforum/files/), which runs **Invision Community 5** with the **Downloads** app. TonyE has admin access to the site.

**Why:** Users currently have to manually browse harpgamer.com, download ZIPs, and sort files into the right directories. The launcher should be able to show a catalog of available downloads and help the user get them into the managed tree.

**Outcome:** The launcher displays a browsable catalog of HarpGamer downloads organized by category, with the ability to initiate downloads into the correct managed directory.

---

## HarpGamer Site Structure

### Downloads App — HC Categories

```
Harpoon Classic/HC/HCE/HUCE (738 files)
├── Databases (cat ID 5)                    → maps to: PlatformDBs
│   └── ~21 files (HCDB-*, HCCW-*, HC_*_DB, etc.)
├── Scenarios (cat ID 6)                    → maps to: Scenarios under BattleSets/PlatformDBs
│   ├── GIUK (162 files, paginated: 7 pages)
│   ├── WestPac
│   ├── NACV
│   ├── MEDC
│   ├── IOPG
│   ├── USNI
│   ├── Caribbean Basin
│   ├── South Atlantic
│   ├── South Africa
│   ├── Middle East
│   └── Custom
├── BattleSets (cat ID ?)                   → maps to: BattleSets
└── Tools/Mods/Docs (cat ID 3)             → maps to: ExportDLLs, Sounds, Graphics, Maps, misc.
```

### Per-File Metadata (visible on download pages)

| Field | Example |
|-------|---------|
| File ID | 445 (from URL: `/files/file/445-hc-launcher/`) |
| Title | HC Launcher 2025.024 |
| Version | 2025.024 |
| File size | 6.05 MB |
| Author | TonyE |
| Category | Tools/Mods/Docs |
| Upload date | September 1, 2009 |
| Last updated | June 15, 2025 |
| Downloads count | 1,844 |
| Views count | 12,713 |
| Description | Free text with changelog |
| Screenshots | Optional |
| Download URL | `/files/file/445-hc-launcher/?do=download&csrfKey=[token]` (login-gated) |

### Key Observations

- Download links require CSRF token and (possibly) login session — cannot be fetched directly by the launcher without authentication
- Pagination exists for large categories (GIUK has 7 pages)
- URL pattern: `/files/file/{id}-{slug}/` for individual files, `/files/category/{id}-{slug}/` for categories

---

## Approaches Evaluated

### Option A — Invision Community REST API

The IPS4 REST API includes Downloads endpoints. Authentication is via API key (created in AdminCP → System → REST & OAuth → API Keys).

**Expected endpoints** (exact spec available in AdminCP → System → REST API → API Reference):
- `GET /api/downloads/categories` — list category tree
- `GET /api/downloads/files?categories={id}&page={n}&perPage={n}` — list files with pagination
- `GET /api/downloads/files/{id}` — single file detail

**Pros:**
- Official, supported, JSON responses
- All metadata in one call (title, version, size, author, category, description)
- Pagination built in
- Survives theme changes (API is independent of frontend)
- Category tree discoverable programmatically

**Cons:**
- Requires admin to create API key on harpgamer.com
- Full endpoint documentation is only visible in AdminCP (not public)
- Rate limits unknown
- Download links may still require auth — API may return metadata but not a direct file stream
- API key must be kept secret (cannot embed in distributed launcher binary)

**Suitability:** Excellent for server-side catalog generation. Not suitable for direct use from the launcher binary (key exposure risk).

### Option B — Custom PHP Script on HarpGamer Server

A PHP script running on the harpgamer server that queries the IPS MySQL database directly and returns JSON.

**Pros:**
- Full control over query and response format
- Can generate direct download links or signed URLs
- No rate limits, fastest possible
- Can include custom fields not exposed by REST API

**Cons:**
- Requires server access (SSH/FTP) or admin cooperation
- Must be maintained when IPS upgrades (DB schema changes)
- Security: must be read-only, locked to specific tables, possibly rate-limited
- Tight coupling to harpgamer infrastructure

**Suitability:** Most powerful but most coupled. Good if admin access is available and schema stability is acceptable.

### Option C — Web Scraping

Parse the HTML of harpgamer.com download pages from the launcher.

**Pros:**
- No admin cooperation needed
- Works with any public page

**Cons:**
- Fragile — breaks on any theme/template/IPS version change
- HTML parsing in Pascal is painful (no mature Lazarus HTML parser)
- Pagination requires following links (multiple HTTP requests)
- Download links are CSRF-protected
- Likely violates site ToS
- Slow and unreliable

**Suitability:** Last resort only. Not recommended.

### Option D — Static Manifest File (Recommended)

A JSON manifest file generated server-side and hosted at a stable URL. The launcher fetches this single file to get the full catalog.

**Pros:**
- Launcher fetches one small file — dead simple
- Easy to parse in Pascal (`fpjson`)
- Manifest format is ours to control — stable contract
- No API key embedded in the launcher binary
- Generator can use Option A or B server-side
- Can be cached, CDN-hosted, or mirrored
- Survives site changes (manifest is independent of frontend and API version)

**Cons:**
- Not real-time (updated on schedule: hourly/daily)
- Still needs server-side infrastructure to generate the manifest
- Download links still need auth unless signed URLs are generated

**Suitability:** Best pragmatic balance. Complexity lives server-side; launcher stays simple.

---

## Decisions

- **Admin access**: TonyE has admin access to harpgamer.com — can create API keys, deploy PHP scripts, host files
- **Platform**: Invision Community 5 (IPS5)
- **Manifest hosting**: On harpgamer.com itself
- **Generator language**: PHP (runs on same server as IPS)
- **Download mechanism**: Direct download from the start (all HC downloads are public, no login required)
- **Future enhancement**: Integrate IPS5 authentication flow for forum features (comments, ratings, etc.)

---

## Architecture: Option D (Manifest) + Option A (IPS5 REST API for generation)

```
┌──────────────────────────────────────────────────────────────┐
│                    HARPGAMER SERVER (IPS5)                    │
│                                                              │
│  ┌─────────────┐    IPS5 REST API   ┌─────────────────────┐ │
│  │  IPS5 DB    │◄──────────────────│  hcl-catalog.php    │ │
│  │  + Downloads │    (API key auth)  │  (cron, daily)      │ │
│  └─────────────┘                    └────────┬────────────┘ │
│                                              │              │
│                                     writes   ▼              │
│                                    ┌─────────────────────┐  │
│                                    │   catalog.json      │  │
│                                    │   (public URL)      │  │
│                                    └─────────────────────┘  │
│                                              │              │
│  ┌───────────────────────────────────────────┘              │
│  │  Also serves download files directly                     │
│  │  (public, no auth needed for HC content)                 │
│  └──────────────────────────────────────────────────────────┘
└──────────────────────────────────────────────────────────────┘
                          │
             HTTPS GET    │  catalog.json + file downloads
                          ▼
┌──────────────────────────────────────────────────────────────┐
│                    HC LAUNCHER (client)                       │
│                                                              │
│  ┌──────────────────┐     ┌─────────────────────────────┐   │
│  │  Catalog Fetcher  │────│  Browse/Search UI            │   │
│  │  (fpjson parse)   │    │  (category tree + file list) │   │
│  └──────────────────┘     └──────────────┬──────────────┘   │
│                                          │                   │
│                              user clicks │ "Download"        │
│                                          ▼                   │
│                           ┌─────────────────────────────┐   │
│                           │  Direct download to cache\   │   │
│                           │  (fphttpclient + progress)   │   │
│                           └──────────────┬──────────────┘   │
│                                          │                   │
│                                          ▼                   │
│                           ┌─────────────────────────────┐   │
│                           │  Extract to managed dir      │   │
│                           │  (PLAN-FILEORGANIZATION)     │   │
│                           └─────────────────────────────┘   │
└──────────────────────────────────────────────────────────────┘
```

### Download Flow

1. Launcher fetches `catalog.json` from harpgamer.com (cached locally)
2. User browses categories, selects a file
3. Launcher downloads the file directly to `<category>\cache\<filename>.zip` via HTTPS (`fphttpclient`)
4. Progress indicator during download
5. Once downloaded, user can extract/import into the managed directory tree

All HC downloads on harpgamer.com are public (no login required), so direct download works without authentication. A future enhancement will integrate the IPS5 authentication flow for features that require a logged-in user (commenting, rating, uploading).

---

## Catalog Format Specification (Draft v1.0)

The catalog format is **source-independent** — any site can publish a catalog for the launcher to consume. The launcher merges multiple catalogs into a unified view. File IDs are scoped to their source (source ID + file ID = globally unique).

### Design Principles

- **No IPS/Invision dependencies in the format** — fields are generic (no `ipsCategory`, no forum-specific concepts)
- **Source-scoped IDs** — every file and category ID is unique only within its source; the launcher uses `sourceId + fileId` as a composite key
- **Multiple catalogs merge cleanly** — same artifact type from different sources appears in the same UI category
- **Any site can generate a catalog** — the format is a documented JSON schema; the HarpGamer PHP generator is one implementation, but a static hand-edited JSON file, a GitHub Action, or a WordPress plugin could produce the same format

### Example

```json
{
  "schemaVersion": "1.0",
  "generated": "2026-04-14T12:00:00Z",
  "source": {
    "id": "harpgamer",
    "name": "HarpGamer.com",
    "url": "https://harpgamer.com/harpforum/files/",
    "contact": "admin@harpgamer.com",
    "downloadMethod": "ips-csrf"
  },
  "categories": [
    {
      "id": "harpgamer:5",
      "name": "Databases",
      "parentId": "harpgamer:2",
      "artifactType": "PlatformDB",
      "files": [
        {
          "id": "harpgamer:235",
          "title": "HCDB-250724 (1980-2015 era)",
          "version": "250724",
          "author": "CV32",
          "size": 12345678,
          "downloads": 18546,
          "updated": "2025-07-24",
          "description": "Platform database for 1980-2015 era scenarios",
          "pageUrl": "https://harpgamer.com/harpforum/files/file/235-hcdb-250724/",
          "downloadUrl": "https://harpgamer.com/harpforum/files/file/235-hcdb-250724/?do=download",
          "fileName": "HCDB-250724.zip",
          "sha256": "a1b2c3d4..."
        }
      ]
    },
    {
      "id": "harpgamer:6",
      "name": "Scenarios",
      "parentId": "harpgamer:2",
      "artifactType": "Scenario",
      "subcategories": [
        {
          "id": "harpgamer:7",
          "name": "GIUK",
          "artifactType": "Scenario",
          "fileCount": 162,
          "files": [ ... ]
        }
      ]
    }
  ]
}
```

### Catalog Fields

**Top-level:**

| Field | Type | Description |
|-------|------|-------------|
| `schemaVersion` | String | Catalog schema version (e.g., `"1.0"`) — for forward compatibility |
| `generated` | ISO 8601 | When this catalog was last generated |
| `source.id` | String | Short unique identifier for this source (e.g., `"harpgamer"`, `"hc-mods-uk"`) |
| `source.name` | String | Human-readable source name |
| `source.url` | String | URL to the source's main download page |
| `source.contact` | String | Contact email or URL for the source maintainer |

**Categories:**

| Field | Type | Description |
|-------|------|-------------|
| `categories[].id` | String | Source-scoped ID: `"<sourceId>:<localId>"` |
| `categories[].name` | String | Category display name |
| `categories[].parentId` | String | Source-scoped parent ID (or null for top-level) |
| `categories[].artifactType` | String | `PlatformDB`, `Scenario`, `BattleSet`, `ExportDLL`, `Sound`, `Graphic`, `Map`, `Tool` |
| `categories[].subcategories` | Array | Nested child categories (optional) |

**Files:**

| Field | Type | Description |
|-------|------|-------------|
| `files[].id` | String | Source-scoped ID: `"<sourceId>:<localId>"` |
| `files[].title` | String | File title |
| `files[].version` | String | Version string |
| `files[].author` | String | Uploader/author name |
| `files[].size` | Integer | File size in bytes |
| `files[].downloads` | Integer | Download count (optional, source-specific metric) |
| `files[].updated` | String | Last update date (YYYY-MM-DD) |
| `files[].description` | String | Brief description |
| `files[].pageUrl` | String | URL to the file's detail/info page on the source site |
| `files[].downloadUrl` | String | `direct` sources: the download URL. `ips-csrf` sources: a link to one specific file of the entry, `<pageUrl>?do=download&r=<record_id>` (IPS keys an entry's files by record_id), to which the client appends its session's `csrfKey`; used only if on the same origin as `pageUrl`. Empty = build from `pageUrl`. |
| `files[].fileName` | String | Original filename (e.g., `HCDB-250724.zip`) |
| `files[].sha256` | String | SHA-256 hash of the download file; empty when unknown. On harpgamer it is hashed from the stored file, verified identical to what a download returns. |
| `files[].artifactType` | String | Optional: set only when the file is more specific than its category (e.g. an `ExportDLL` in the `Tool` category); otherwise inherit the category's. |
| `files[].externalLink` | Boolean | Optional: `true` when the entry's download is a link to another site, not a file. Open `pageUrl` in a browser rather than downloading. |
| `files[].versionLabel` | String | Optional: display name of the current version: `version` when set, else `fileName` without its extension (`HCDB2-170909`). Few authors fill in `version`, but HC files usually carry it in the filename. |
| `files[].companions` | Array | Optional: files the entry needs from other entries, `[{"id": "harpgamer:328", "provides": "commondb.rsr"}]`. `provides` is a bare file name. Curated in the generator (no IPS metadata records it). |
| `files[].versions` | Array | Optional: earlier versions the source still offers, newest first; absent when none. Each has `version` (the source's field, often empty), `label` (as `versionLabel`), `date` (`YYYY-MM-DD`, when that version was **published**), `changelog` (plain text, at most 1000 characters), `fileName`, `size`, `sha256`, and `downloadUrl` (IPS: `<pageUrl>?do=download&version=<b_id>&r=<record_id>`). |
| `files[].screenshotUrl` | String | URL to primary screenshot thumbnail (optional) |
| `files[].rating` | Number | Average rating (0 if unrated) |
| `files[].dependencies` | Object | Extracted from IPS5 custom fields (optional, source-specific): |
| `files[].dependencies.platformDB` | String | Required PlatformDB version (e.g., `"HCDB2-170909"`) |
| `files[].dependencies.gameVersion` | String | Required GE version |
| `files[].dependencies.battleSet` | String | Associated BattleSet region (e.g., `"GIUK"`) |
| `files[].dependencies.battleSetVariant` | String | BattleSet variant detail (e.g., `".scm - EC2003 - EC2003"`) |

**Source-level:**

| Field | Type | Description |
|-------|------|-------------|
| `source.downloadMethod` | String | `"direct"` (downloadUrl is a direct link) or `"ips-csrf"` (requires session+CSRF flow via pageUrl). Launcher uses this to select download strategy. |

---

## Open Questions (remaining)

1. **Update frequency**: Full catalog regeneration every 4 hours. Future enhancement: on-demand diff endpoint that returns only files changed since a given timestamp, so the launcher can do lightweight incremental updates between full refreshes.

2. **Category-to-artifact mapping**: "Tools/Mods/Docs" (cat ID 3) contains ExportDLLs, Sounds, Graphics, Maps, and misc docs. The PHP generator will inspect the ZIP contents to determine artifact type (look for `.dll` → ExportDLL, `.wav`/`SND_*` → Sound, `.bmp`/`GR_*` → Graphic, `.tif` → Map, etc.). Files that don't match a known pattern get `artifactType: "Tool"` as fallback. This inspection happens at generation time, not in the launcher.

3. **Download URL format**: The IPS5 REST API has a `GET /downloads/files/{id}/download` endpoint that returns a download link. Need to verify: (a) does it work for guest/public downloads without auth? (b) does it return a direct file URL or a redirect? (c) can we construct the URL pattern without calling the API per-file? The PHP generator should test this during development.

4. **Manifest URL path**: Exact location on harpgamer.com. Proposed: `https://harpgamer.com/harpforum/uploads/hclauncher/catalog.json` — need to verify this path is web-accessible and writable by the PHP script.

---

## Phases

### Phase 0 — IPS5 REST API Discovery ✓ COMPLETE

**Goal:** Determine the exact API endpoints and response formats available on harpgamer.com's IPS5 installation.

- Create an API key in AdminCP → System → REST & OAuth → API Keys
- Browse AdminCP → System → REST API → API Reference to document:
  - `GET /api/downloads/categories` — response fields, pagination
  - `GET /api/downloads/files` — filter params (categories, page, perPage), response fields
  - `GET /api/downloads/files/{id}` — single file detail, download URL
  - `GET /api/downloads/files/{id}/download` — direct download link behavior for public files
- Test a few calls with `curl` to confirm behavior for guest-accessible downloads
- Document findings in this plan

**Acceptance:** We know the exact API calls needed and the response JSON structure for the manifest generator.

### Phase 0 Findings (2026-04-14)

API key authentication works via query parameter: `?key=<apikey>`

**1. Categories — `GET /api/downloads/categories`**
- Returns flat paginated list (all 25 categories in one page)
- Fields: `id` (int), `name`, `url`, `parentId` (0 = top-level), `class`, `permissions`, `club`
- HC parent: `id=1` ("Harpoon Classic/HC/HCE/HUCE"), children: `id=3` (Databases), `id=5` (Scenarios), `id=15` (BattleSets), `id=12` (Tools/Mods/Docs)
- Scenarios `id=5` has regional subcategories: `id=6` (GIUK), `id=7` (WestPac), `id=8` (NACV), `id=9` (MEDC), `id=10` (IOPG), `id=11` (USNI), `id=16` (Custom), `id=17` (Caribbean Basin), `id=18` (South Atlantic), `id=19` (South Africa), `id=20` (Middle East)

**2. File listing — `GET /api/downloads/files?categories=<id>&perPage=<n>&page=<n>`**
- Paginated: `page`, `perPage`, `totalResults`, `totalPages`
- Per-file fields used by catalog:
  - `id` (int), `title`, `version`, `description` (HTML), `changelog` (HTML)
  - `date` (ISO 8601, original upload), `updated` (ISO 8601), `published` (ISO 8601)
  - `downloads` (int), `views` (int), `comments` (int), `reviews` (int)
  - `url` (file page URL)
  - `author.name` (string), `author.id` (int)
  - `category.id`, `category.name`, `category.parentId`
  - `tags` (array), `customFields` (object)
  - `screenshots`, `primaryScreenshot`
- **NOT returned by API**: file size, filename, download URL
- Author object is very verbose (~200 lines per file including badges, custom fields, etc.) — generator should extract only `author.name`

**3. Download endpoint — `GET /api/downloads/files/{id}/download`**
- Returns **403 Forbidden** even with valid API key
- Not usable for direct download or metadata extraction

**4. Actual download flow (web, not API)**
- Step 1: `GET /files/file/{id}-{slug}/` → receive session cookie (`ips4_IPSSessionFront`) + extract `csrfKey` from HTML
- Step 2: `GET /files/file/{id}-{slug}/?do=download&csrfKey=<key>` with session cookie → receive file
- Response headers provide missing metadata:
  - `Content-Disposition: attachment; filename="hcstrikerfictdb_091008.zip"` → filename
  - `Content-Length: 1104234` → file size in bytes
  - `Content-Type: application/zip`

**5. Implications for manifest generator**
- File listing via API provides most metadata (title, version, author, description, download count, dates)
- File size and filename must be obtained separately via the download flow (HEAD request or full download)
- Two options for the generator:
  - **Option A**: Generator does a HEAD-like request (step 1+2) per file to get size/filename. Slow for 738 files but only needs to run for new/updated files.
  - **Option B**: Generator omits size/filename from manifest. Launcher gets them from response headers when user initiates download. Less data in manifest but worse UX (can't show size before downloading).
  - **Recommended**: Option A for the generator (cache results, only re-check updated files), with Option B as fallback.
- The launcher will also need to do the session+CSRF flow for direct downloads (not just the API key)

**6. Custom fields on files — metadata gold**

IPS5 custom fields on download files provide structured metadata for scenario→BattleSet→PlatformDB relationships:

| Field ID | Name | Type | Example Value |
|----------|------|------|---------------|
| 1 | DB Used | String | `"HCDB2-170909"` — PlatformDB version dependency |
| 2 | Authors | String | `"Enrique Mas"` — creator name(s) |
| 3 | Battleset-GIUK | Select | `".scm - EC2003 - EC2003"` — scenario type + BattleSet variant |
| 4 | Battleset-NACV | Select | (same pattern) |
| 5 | Battleset-MEDC | Select | |
| 6 | Battleset-IOPG | Select | |
| 7 | Battleset-WPac | Select | |
| 8 | Battleset-USNI | Select | |
| 9 | Battleset | String | Free-text BattleSet group name |
| 10 | Game Version | String | GE version(s) required |
| 11 | Battleset-Custom | Select | |
| 12 | Battleset-Caribbean Basin | Select | |
| 13 | Battleset-South Atlantic | Select | |
| 14 | Battleset-South Africa | Select | |
| 15 | Battleset-Middle East | Select | |

These are populated by uploaders (not all files have them, but scenarios typically do). This largely solves the "BattleSet-to-PlatformDB association is only in external metadata" problem from PLAN-FILEORGANIZATION — the metadata is already in IPS5.

**7. Other notable fields**

- `primaryScreenshot: { name, url, size }` — thumbnail available for UI
- `isPaid`, `isPurchasable`, `prices` — commerce fields (all false/null for HC)
- `rating` — numeric rating (0 if no reviews)
- `topic: { id, title }` — linked forum discussion topic

### Phase 1 — Manifest Generator (PHP, server-side) ✓ COMPLETE

**File:** `hcl-catalog.php` (deployed to harpgamer.com)

- PHP script that uses the IPS5 REST API (via `curl` or `file_get_contents` with API key header) to:
  1. Enumerate HC download categories (cat ID 2 = HC parent, recurse subcategories)
  2. For each category, paginate through all files
  3. For each file: extract id, title, version, author, size, downloads, updated, description, url, downloadUrl, fileName, screenshotUrl, rating, and dependency metadata from custom fields:
     - Custom field 1 ("DB Used") → `dependencies.platformDB`
     - Custom field 10 ("Game Version") → `dependencies.gameVersion`
     - Custom fields 3–8, 11–15 (Battleset-*) → `dependencies.battleSet` (whichever is non-null) + `dependencies.battleSetVariant` (the field value)
     - Custom field 9 ("Battleset") → `dependencies.battleSet` fallback (free-text)
     - Custom field 2 ("Authors") → `author` (override if different from uploader)
  4. Map categories to `artifactType`:
     - Databases → `PlatformDB`
     - Scenarios (all subcategories) → `Scenario`
     - BattleSets → `BattleSet`
     - Tools/Mods/Docs → inspect ZIP contents to determine type:
       - Contains `.dll` → `ExportDLL`
       - Contains `.wav` or `SND_*` → `Sound`
       - Contains `.bmp` and `GR_*` → `Graphic`
       - Contains `.tif` → `Map`
       - Otherwise → `Tool`
  5. For each file, obtain size and filename via the session+CSRF download flow:
     - GET the file page to establish session + extract csrfKey
     - HEAD (or GET with Range: bytes=0-0) the download URL with session + csrfKey
     - Parse `Content-Disposition` for filename, `Content-Length` for size
     - Cache these per file ID; only re-check files whose `updated` timestamp changed
  6. Compute SHA-256 hash of each download file (requires full download; do this incrementally — only hash new/updated files, store hashes persistently)
  7. Write `catalog.json` to a web-accessible location (atomic: write to `.tmp`, rename)
- Cron job: runs every 4 hours (e.g., 00:00, 04:00, 08:00, 12:00, 16:00, 20:00 UTC)
- Error handling: if API call fails, keep the previous manifest (don't overwrite with empty/partial data)
- API key stored in a config file outside the web root (e.g., `/home/user/hcl-config.php`)

**Serving the manifest:**

The manifest is not served as a static file. Instead, a PHP script (`hcl-catalog-serve.php`) serves the pre-generated `catalog.json` while collecting request metrics:

- Launcher version (from `User-Agent` or custom `X-HCL-Version` header)
- Client IP (for unique user estimation, not stored long-term)
- Timestamp
- `If-Modified-Since` / `304 Not Modified` support (avoids re-sending unchanged catalog)

Initially, metrics are appended to a simple log file (`hcl-catalog-access.log`). A future phase will persist them into a database for dashboards (active users, version adoption, popular categories).

**Privacy:** IPs are logged for unique-user estimation only. The log should be rotated and old entries purged (e.g., keep 30 days). No personally identifiable information beyond IP is collected. A brief privacy note should be visible in the launcher's about/settings screen.

**Rate limiting:** Basic abuse protection — max 60 requests per IP per hour. Return `429 Too Many Requests` if exceeded. Implemented via a simple file-based or in-memory counter (no DB needed initially).

The launcher fetches: `https://harpgamer.com/harpforum/uploads/hclauncher/hcl-catalog-serve.php`

**Acceptance:** `curl` returns valid JSON with all HC files and correct `Last-Modified` / `Content-Type` headers. Access log records the request.

### Phase 2 — Catalog Fetcher + Downloader (launcher-side) ✓ COMPLETE

**File:** `uHCLCatalog.pas` (new)

- `THCLCatalogFile` record: id, title, version, author, size, downloads, updated, description, url, downloadUrl, fileName, artifactType
- `THCLCatalogCategory` record: id, name, artifactType, files (array), subcategories (array)
- `IHCLCatalog` interface — represents a **merged** view of one or more catalog sources:
  ```pascal
  IHCLCatalog = interface
    function AddSource(const AManifestURL: String): Boolean;
    function LoadFromCache(const ACacheDir: String): Boolean;
    procedure SaveToCache(const ACacheDir: String);
    function RefreshAll: Boolean;
    function GetSources: THCLCatalogSourceArray;
    function GetCategories(const ASourceFilter: String = ''): THCLCatalogCategoryArray;
    function FindFile(const AFileID: String): THCLCatalogFile;  // source-scoped ID
    function SearchFiles(const AQuery: String): THCLCatalogFileArray;
  end;
  ```
- Multiple catalog URLs configured in INI (see Settings below)
- Each source cached separately: `<managed_root>\catalogs\<sourceId>.json`
- Merged view groups files by `artifactType` across sources
- Uses `fphttpclient` for HTTPS GET (`uCICOpenSSL3` for TLS on Linux)
- Sets `User-Agent: HCLauncher/<version>` and `X-HCL-Version: <version>` headers on all requests (enables server-side metrics)
- Uses `fpjson` for parsing
- Supports `If-Modified-Since` / `304 Not Modified` to skip re-downloading unchanged catalog
- Cache: saves last-fetched manifest to `<managed_root>\catalog.json`
- On startup: load from cache; if stale (>4h), fetch fresh copy in background

**File:** `uHCLDownloader.pas` (new)

- `IHCLDownloader` interface:
  ```pascal
  IHCLDownloader = interface
    function Download(const AURL, ADestPath: String; AOnProgress: TDownloadProgressEvent): Boolean;
    function Cancel: Boolean;
  end;
  ```
- Downloads to `<category>\cache\<fileName>` using `fphttpclient`
- For HarpGamer (and any IPS-based source): implements the session+CSRF download flow:
  1. GET the file's `pageUrl` to obtain session cookie + csrfKey
  2. GET `pageUrl + '?do=download&csrfKey=' + key` with session cookie to stream the file
  - This is source-specific; other catalog sources may provide direct download URLs that need no session
- Progress callback for UI update (bytes received / total bytes)
- **Resumable downloads**: Support HTTP `Range` header. If a partial `.part` file exists in cache, resume from where it left off. Rename `.part` → final filename on completion.
- **Integrity verification**: After download, compute SHA-256 of the file and compare against `sha256` field in the manifest. If mismatch, warn the user and offer to re-download. Delete corrupt file from cache.
- **Already-downloaded detection**: Before downloading, check if the file already exists in cache with matching SHA-256. If so, skip download. If file exists but SHA-256 differs (older version), prompt user: "A newer version is available. Download update?"

**Tests:** `tests/test_Catalog.pas` — parse mock JSON, search, find by ID
**Tests:** `tests/test_Downloader.pas` — mock HTTP server if feasible, or test with a small known file

**Acceptance:** Unit tests pass. Launcher can fetch catalog.json, parse it, and download a file to cache.

### Phase 3 — Browse/Download UI ✓ COMPLETE

- New tab on the main form: "HarpGamer Downloads"
- Left panel: category tree (`TTreeView`) populated from merged catalog
  - Top-level nodes by artifact type (BattleSets, Scenarios, PlatformDBs, etc.)
  - Under each: categories from all sources, grouped by source if multiple
  - Source icon/label to distinguish origin
- Right panel: file list (`TListView` in vsReport mode)
  - Columns: Title, Version, Author, Source, Size, Downloads, Updated
  - Sortable by column click
- Bottom panel: description/detail for selected file, including:
  - Dependencies: required PlatformDB, BattleSet, GE version (from `dependencies` fields)
  - "You have this DB" / "DB not installed" indicator (cross-reference with managed file tree)
  - Screenshot thumbnail if available
- "Download" button: downloads to appropriate `cache\` directory with progress bar
- "Open in Browser" button: opens the file's `pageUrl` in the default browser (works for any source, not HarpGamer-specific)
- "Refresh Catalog" button: re-fetches manifest
- Search/filter box: filters file list by title substring
- Status bar: "Catalog updated: 2026-04-14" / "738 files available"
- **New content notification**: Compare current catalog against the last-seen catalog (stored timestamp or file ID set). Show badge/indicator on the tab: "5 new files since last visit". Highlight new/updated files in the list.
- **Update available indicator**: Files already in the user's cache or managed tree that have a newer version on harpgamer are flagged in the list (e.g., icon or "Update available" label).
- Catalog sources configurable in INI (see below)
- "Manage Sources" button to add/remove catalog URLs

**INI format for multiple sources:**
```ini
[CatalogSources]
Count=1
Source0=https://harpgamer.com/harpforum/uploads/hclauncher/hcl-catalog-serve.php
; Users can add more:
; Source1=https://example.com/hc-mods/catalog.json
```

### Phase 4 — Extract and Import (crossover with PLAN-FILEORGANIZATION) ✓ COMPLETE

- After downloading a ZIP to cache, offer to extract it into the correct managed subdirectory
- Uses `zipper` unit (FPC built-in) or `TUnZipper` from `Paszlib`
- Auto-create the artifact subdirectory based on `artifactType` and file title
- For PlatformDBs: extract into `PlatformDBs\<version>\`
- For Scenarios: prompt for which BattleSet or PlatformDB to place under
- For BattleSets: extract into `BattleSets\<name>\`

**Implemented as:**

- `uHCLArchive.pas` — `IHCLArchive` wraps `TUnZipper` (paszlib) for `ListEntries`/`Extract`/`Cancel`/`IsExtracting`. Rejects path-traversal ("zip slip") entries via `HCLIsEntryNameSafe` (absolute path, drive letter/ADS `:`, or any `..` segment), reported in `THCLExtractResult.UnsafeEntries` rather than extracted. `THCLOverwriteMode = (omSkip, omOverwrite, omFail)`. Errors come back in a result record, never raised — matching the `IHCLDownloader` convention.
- `uHCLImport.pas` — `IHCLImport` places an archive into the managed tree, split into two calls: `PlanImport` is pure (resolves a destination, reports what's missing, touches no disk) and `Execute` performs the extraction. The split lets the UI prompt the user before anything is created. `HCLArtifactTypeToCategory` maps the catalog's `artifactType` to `THCLCategory`, closing the TODO that previously hard-coded `hclBattleSets` for every download. `HCLResolveWrapperPrefix` works out which leading directories of the ZIP are redundant packaging (a release folder, or a repeat of the category or artifact name) while never stripping a structural one (`GE`/`SE`, `res`/`rsr`, `Scenarios`, `SavedGames`); the prefix is passed to `IHCLArchive.Extract`, which strips it as each file is written. `HCLSanitizeDirName` produces Windows-safe directory names (including reserved device names like CON/LPT1). `HCLCategoryAcceptsImports` states the "Logs are never imported" rule as a tested predicate.
- ExportDLLs are targeted to `ExportDLLs\` (GE and SE), `ExportDLLs\GE\` or `ExportDLLs\SE\` via `THCLDLLTarget`. `HCLDetectDLLTarget` recognises an archive that already lays its DLLs out under `GE/`/`SE/` (`dtAsPackaged`, installed as packaged); otherwise `PlanImport` returns `RequiresDLLTarget` and the UI asks.
- `THCLCategory` (in `uHCLFileOrganization.pas`) gained `hclTools`, appended at the end of the enum to preserve existing ordinals (INI persistence safety) — the home for the catalog's `Tool` fallback artifact type.
- UI (`uMain1.pas`): after a successful ZIP download, `OfferImport` offers extraction (never automatic) into the resolved destination; Scenarios with no declared PlatformDB/BattleSet dependency prompt via `InputQuery` + a `MessageDlg`; ExportDLLs whose archive does not say GE/SE prompt via `QuestionDlg` (GE only / SE only / Both); unsafe archive entries raise a warning dialog.
- Tests: `tests/test_Archive.pas`, `tests/test_Import.pas`.

### Phase 5 — Deep ZIP Inspection for Metadata Enrichment (future)

**Scope reduced:** IPS5 custom fields (Phase 0 finding #6) already provide BattleSet region, PlatformDB version, and game version dependencies for most scenarios. Deep ZIP inspection is now a supplementary data source for:

- **Files with missing custom fields**: Older uploads or files where the uploader didn't fill in the metadata. ZIP inspection can fill the gaps.
- **Tools/Mods/Docs**: Still need ZIP inspection to classify artifact type (ExportDLL, Sound, Graphic, Map) — custom fields don't cover this.
- **Scenarios**: Read `.sc?` files to extract embedded BattleSet number (validates/enriches the custom field data). Detect `.BL?`/`.RD?` orders files and `.xml` sidecar. Determine embedded-DB vs commondb.
- **PlatformDBs**: Read `DatabaseStructureVersion.txt` from `res\` to extract exact DB version string. Detect `.mdb` source files. Count annex files.
- **BattleSets**: Detect embedded platform database, `.HCBS` BSBuilder settings, map region.
- **Cross-referencing**: Build a dependency graph in the manifest — supplements what custom fields already provide. Feeds the metadata/catalog layer in PLAN-FILEORGANIZATION Phase 4.

### Phase 6 — On-Demand Diff Endpoint (future)

- `hcl-catalog-diff.php?since=2026-04-14T12:00:00Z` — returns only files added/updated/removed since the given timestamp
- Launcher calls diff endpoint between full catalog fetches for lightweight incremental updates
- Response format: `{ "since": "...", "added": [...], "updated": [...], "removed": [id, id, ...] }`
- Reduces server load and bandwidth for frequent checks
- Generator stores last-modified timestamps per file to compute diffs efficiently

### Phase 7 — Metrics Persistence and Dashboard (future)

- Migrate `hcl-catalog-access.log` data into a database table (MySQL, same DB as IPS5)
- Track: unique launchers per day/week/month, version distribution, catalog fetch frequency
- Simple PHP dashboard page (admin-only) showing usage trends
- Optionally track per-file download initiation (launcher sends a lightweight ping when a download starts)

### Phase 8 — IPS5 Authentication Integration (future)

- OAuth2 flow: launcher opens browser to harpgamer auth endpoint, receives token via localhost callback
- Authenticated API calls: comment, rate, report, upload
- Display user's download history
- "My Uploads" management

---

## Risk Register

| Risk | Mitigation |
|------|-----------|
| IPS5 API doesn't support Downloads endpoints or changed format from IPS4 | Phase 0 discovery step; fall back to direct DB query PHP script |
| Download URL requires CSRF/session even for public files | Test in Phase 0; if needed, PHP generator can produce signed/direct URLs |
| Manifest URL changes or harpgamer goes down | Launcher caches last manifest; configurable manifest URL in `[Main] CatalogURL` |
| HTTPS/TLS issues in FPC | Use `fphttpclient` with OpenSSL; `uCICOpenSSL3` available in shared units for Linux |
| Large manifest (738+ files) | JSON compresses well; even uncompressed, ~500KB is trivial for a single fetch |
| Category structure changes on harpgamer | `artifactType` field in manifest decouples launcher from IPS category IDs |
| API key exposed in PHP source | Store key in config file outside web root (`/home/user/hcl-config.php`) |
| Partial manifest on API error during generation | Generator keeps previous manifest if API calls fail; atomic write (write to temp, rename) |
| ZIP extraction fails or unexpected archive structure | Show error, keep ZIP in cache for manual extraction; don't delete cache on failure |
| fphttpclient doesn't follow redirects for download URLs | Enable redirect following; test in Phase 2 |

---

## Implementation Log

- **2026-04-15**: Phases 0–3 implemented and deployed.
  - Phase 0: IPS5 REST API tested live. Categories endpoint works (25 cats). File listing returns all metadata except size/filename. Download endpoint returns 403 — must use session+CSRF flow. Custom fields 1–15 provide scenario→BattleSet→PlatformDB dependency metadata.
  - Phase 1: `server/hcl-catalog-generate.php` (842 lines) + `server/hcl-catalog-serve.php` (244 lines) deployed to harpgamer.com. Config in safePHP/ outside web root. Cron every 4h. First run produced 738 HC files.
  - Phase 2: `uHCLCatalog.pas` (multi-source catalog parser, 33 FPCUnit tests) + `uHCLDownloader.pas` (direct + IPS-CSRF download with resume/progress).
  - Phase 3: Downloads tab on main form — category tree, sorted file list, detail panel with dependencies, search, download with progress bar.
  - Deployment: PHP strict_types must be on same line as `<?php`; shebang line conflicts with strict_types in web context; config paths resolved for safePHP/ layout; catalog.json stored outside web root, served only via PHP script.
  - Bugs fixed:
    - `Move()` on records with managed String fields corrupts ref counts → replaced with element-by-element `:=` assignment
    - LCL fires OnSelectItem/OnSelectionChanged during Items.Clear and form destruction → nil handlers before clearing, explicit interface release order in FormDestroy
    - `Application.ProcessMessages` in download progress callback causes re-entrancy → replaced with `Repaint`, disable controls during download
    - TOCTOU on `lvFiles.Selected` → cache in local variable
    - FPC 3.2.2 needs OpenSSL 1.1 DLLs (`libssl-1_1.dll`, `libcrypto-1_1.dll`), not 3.x
  - All 139 tests pass (79 cmd-line + 1 settings + 26 file org + 33 catalog).

- **2026-09-21**: Phase 4 implemented.
  - `uHCLArchive.pas` (safe ZIP inspection/extraction via paszlib's `TUnZipper`) and `uHCLImport.pas` (plan/execute placement policy) added — both zero-UI-dependency, independently testable.
  - Artifact-type→category mapping (`HCLArtifactTypeToCategory`) closes the `uMain1.pas` TODO that previously hard-coded `hclBattleSets` for every download.
  - `THCLCategory` gained `hclTools`, appended at the end of the enum to preserve existing ordinals.
  - Extraction is offered after download, never automatic — the cached ZIP remains the authoritative copy.
  - New FPCUnit suites cover the archive and import layers.
  - The `zipper` unit is part of FPC's paszlib package.
- **2026-09-22**: Phase 4 follow-up — ExportDLL targeting and wrapper handling rework.
  - Bug found and fixed: the first cut removed a ZIP's wrapper directory by moving files up *after* extraction. For an ExportDLL packaged as `GE/Foo.dll` the "wrapper" was `GE` itself, so the DLL landed in the common `ExportDLLs\` root (loaded into the SE as well), and every GE-only DLL already in `ExportDLLs\GE\` was swept up with it. The same move could not merge into an existing directory. Replaced by strip-at-extract: `IHCLArchive.Extract` takes an optional strip prefix and writes each entry straight to its final path via a `TUnZipper.OnCreateStream` handler. `HCLFlattenSingleWrapper`/`HCLFlattenWrapperDir` were removed.
  - `OnCreateStream` hazards handled: `TUnZipper` ignores `OutputPath` once a custom stream handler is set and falls back to creating the file relative to the process's current directory if the handler returns `nil`, so cancelled or unplanned entries get a discard stream instead; timestamps are restored in `OnDoneStream` (the custom-stream path skips `SetAttributes`); file attributes are deliberately not copied, so a read-only flag in an archive can't block a later overwrite.
  - ExportDLL GE/SE targeting via `THCLDLLTarget`, `HCLDetectDLLTarget`, and a UI prompt when the archive does not say.
  - The unreachable `hclLogs` branch in `PlanImport` replaced by the `HCLCategoryAcceptsImports` predicate, plus a test that no catalog tag maps to a category that refuses imports.
- **2026-09-22**: Manual end-to-end run against the live catalog (sandboxed copy of `Launcher.exe` driven via UI Automation). Found that extraction was never offered for any live file, because of two Phase 2 bugs, both now fixed:
  - **Catalog:** the live JSON carries `artifactType` only on categories, never on files, and `ParseCategory` did not pass it down — every file arrived with an empty type, so every download went to `Tools\cache\` and `PlanImport` rejected it. `ParseCategory` now inherits category → subcategory → file; an explicit type on a file or subcategory still wins. Unit-test fixtures had put the type on each file, which is why this was missed.
  - **Downloader:** the live catalog has no `fileName`, and the placeholder `download.part.tmp` was assigned before the transfer, which suppressed the `Content-Disposition` lookup — every download was saved as `download.part.tmp`, which `OfferImport` skipped as "not a ZIP". Also, all unnamed downloads shared one `.part` name, so a cancelled download could be resumed (via `Range`) onto a *different* file. Now: the `.part` is keyed on a per-URL stem (`HCLDownloadStem`), the final name is requested → `Content-Disposition` → stem (`HCLResolveDownloadName`), and every name — including the catalog's and the server's — goes through `HCLSafeFileName` (no traversal, no absolute paths, no reserved device names), since `Content-Disposition` is untrusted and is now actually read. RFC 5987 `filename*=` is supported.
  - `OfferImport` now reports a non-ZIP download on the status bar instead of skipping silently.
  - Verified live after the fixes: "Caribean" → `BattleSets\cache\caribic.zip` → extracted to `BattleSets\Caribean\hdsu.res`/`.rsr`. The GE/SE prompt was verified with a sandbox-patched catalog record (a flat ExportDLL archive → "GE only" → `ExportDLLs\GE\lazHCEffects.dll` + `GE\lazHCEffects\`).
  - **Still open (server-side):** the live catalog has no `ExportDLL`/`Sound`/`Graphic`/`Map` categories — every Tools/Mods/Docs entry is `Tool`, including those titled "ExportDLL - …" — so the per-ZIP classification planned for the generator isn't happening, and the GE/SE prompt can't fire with live data. File `size` values also look like placeholders (≈65 KB each).
- **2026-09-22**: Generator fixed (written and tested; **not yet deployed**). Root causes of the empty `fileName`/≈65 KB `size`, found by replaying the generator's requests:
  - IPS refuses downloads to the exact UA `HCLauncher-CatalogGenerator` (any version) with error EX1406 — an HTTP 500 HTML page — while near-identical strings work, suggesting a server-side rule on that string. `httpGetFileMeta` never checked status or content type, so the error page's 65,860-byte `Content-Length` was recorded as every file's size.
  - Over HTTP/2, harpgamer.com answers a Range request with a malformed DATA frame; over HTTP/1.1 its 206 declares the whole file's `Content-Length` while sending one byte. Either makes curl fail after the headers arrive.
  - Results were cached permanently (failures included), keyed on the file's `updated` date, so fixing the lookup alone would have changed nothing until each file was re-uploaded.
  - Fixes: configurable UA (default `HCLauncher-CatalogBuilder/1.1`); all requests pinned to HTTP/1.1; headers captured by callback and the body aborted immediately; `parseFileMetaHeaders` accepts only a final 200/206 non-HTML response; cache entries versioned (v2), so every v1 entry is ignored on first run, with failures retried after `meta_retry_seconds` instead of cached forever.
  - Per-file classification: files in `inspect_artifact_types` categories (default `Tool`) are downloaded once per version and their ZIP listing classified by `classifyArchiveEntries` (`.exe`→Tool, `.dll`→ExportDLL, `.tif`→Map, `.wav`→Sound, `.bmp`→Graphic); a more specific result is emitted as a file-level `artifactType`, which the launcher's inheritance honours.
  - Tests: `server/tests/test-generator.php` (offline; `--live` adds guest-only integration checks against harpgamer.com). 48/48 on PHP 8.1.34 and 8.5.11.
  - Deploy note: the first run after deploying refetches metadata for every file (v1 cache invalid) and downloads each Tools/Mods/Docs file once for inspection. IPS will likely count these as downloads.
- **2026-09-25**: Generator **deployed** to `/safePHP/hcl-catalog-generate.php` on the harpgamer cPanel account via the new `server/deploy-generator.ps1` (WebDAV, `webdisk.tgp.net`). Live SHA-256 `1a49c271…3c4598` = r1107; the April version is kept as `hcl-catalog-generate.php.bak` for `-Rollback`. The first attempt exposed a cpdavd quirk — successful MOVE answers 200, not 201/204 — which the script now accepts (the hash read-backs decide success); the move itself had completed and was verified by hand. Public catalog at deploy time: generated 12:00:46 UTC, 740 files, 0 filenames, 0 file-level types. Confirm the fixes with `-VerifyCatalog` after the next cron run.
- **2026-09-25 (16:00 UTC run)**: the first run of the fixed generator got `meta_fetched=45 meta_failed=695` in 147 s. The successes were all at the start (20/21 Databases, then 25 of the first ~50 WestPac), after which every lookup failed — the signature of a limit hit mid-burst: 740 lookups from the server's own IP in 2.5 minutes. The same requests from outside succeeded, and the new UA is not banned; the refusing layer (IPS guest download throttling or a server security layer) is unconfirmed, and the generator recorded no reasons. Fixed in the generator:
  - **Diagnostics**: each failure records the step and response (`page:`/`download:` + HTTP status, content type, IPS error code), stored in the cache entry and tallied in the END log line (`failures=…`). Verified live: the old UA's refusal reads `download: HTTP 500 text/html EX1406`.
  - **Budget**: collect → plan → render. `planLookups` spends `max_lookups_per_run` (100) by priority — inspected categories (Tools) first, then never-looked-up files, then due retries; over-budget files are emitted without size/filename and make no request. `lookup_delay_ms` (2000) paces live lookups.
  - **Retries**: `meta_retry_seconds` 24 h → 3 h, under the 4-hour cron interval, so failures are retried each run within the budget.
  - Next step under consideration: the IPS REST endpoint `GET /api/downloads/files/{id}/download`, which returned 403 in Phase 0 — possibly just not enabled for the API key. If it returns name/size/URL, it would replace the guest download flow for metadata and inspection.
- **2026-09-25 (20:00 UTC run, r1110)**: budget and pacing worked. Of 100 planned lookups, 66 succeeded and 34 failed; backlog 595. 29 Tools archives were inspected and 18 files got a per-file type. `-VerifyCatalog` passed from outside: 111 filenames, 18 file-level types. All 34 failures read `download: HTTP 200 text/html` with no IPS error code, which fits the HTML file chooser IPS returns for multi-file entries rather than a throttle.
- **2026-09-25 (later)**: REST API ruled out, IPS database adopted.
  - REST, tested with the "HC Launcher Catalog Generator" key: `/downloads/files/{id}/download` returns 403 `NO_PERMISSION` (`2S291/7`) and is member-only (IPS leaves it out of the key's permission list; `/downloads/files/{id}` is already granted). `/downloads/files/{id}/history` returns earlier versions' changelogs only; `/downloads/files/{id}` has no archive name/size/URL (only screenshots), though its `tags` (e.g. `exportdll`) could hint at types.
  - **IPS database source** (generator runs on the IPS host): a SELECT-only user reads `ibf_downloads_files_records`: `record_type='upload' AND record_backup=0 AND record_hidden=0` for the IDs the REST listing returned. These are the same conditions IPS uses in `File.php`, and there's a matching index on `(record_type, record_file_id, record_backup)`. That gives the real filename and size with no HTTP requests, no throttling and no inflated public download counts. Only listed IDs are looked up, so visibility still follows IPS's public view.
  - **Stored files on disk**: IPS stores Downloads files with the FileSystem method at `/IPB/IDM/downloads/` (outside the web root; `monthly_YYYY_MM/<name>_zip.<md5>`, plus legacy IDM `*.ipb` files). The generator resolves the directory from `upload_settings` → `filestorage__downloads_Files` → `ibf_core_file_storage` (`{root}` = the IPS install at `public_html/harpforum`), with an `ips_downloads_dir` override. From disk it computes **SHA-256** (the catalog's `sha256` field, empty until now), verified byte-identical to what IPS serves a guest (lazHCEffects: `f384f8af…02f85e2`, 5,411,224 bytes), and classifies Tools archives, with no downloads either way. Paths are confined to the storage directory.
  - Multi-file entries: the catalog lists one file per entry (the default-flagged record, else the first `.zip`, else the oldest); `multi_file_entries` counts the rest. Such entries were likely among the web flow's failures, since `?do=download` answers them with an HTML file chooser.
  - The web flow (budget, pacing, diagnostics) remains the fallback for when the database is unavailable, or for files with no upload record.
- **2026-09-26 (00:00 UTC run, r1111)**: the database version worked. It logged `IPS database: 665 of 740 files have upload records; storage /home/tgpnet/sites/harpgamer.com/IPB/IDM/downloads`, with `db_resolved=665 sha256_computed=664 multi_file_entries=29 inspected_local=41 refined=23 stored_file_unreadable=1`, and `backlog_after_run=0`. `-VerifyCatalog`: 665 filenames, 23 file-level types. The 75 files without an upload record all failed in the web fallback with `download: HTTP 200 text/html`.
- **2026-09-26**: follow-ups to the database version.
  - **Upload-less files**: when the database is available it accounts for every listed file. Files without a current upload are classified from all their records (`ipsFetchRecordSummary` → `classifyNoUploadRecords`: `external_link`, `no_current_upload` for superseded/pending only, or `no_records`), logged by kind, and never retried on the web, where they were costing ~75 wasted guest requests a run. External links get `externalLink: true` in the catalog. The launcher opens those in the browser instead of downloading a web page.
  - **Unreadable stored files**: `resolveStoredFile` reports why (storage unknown/missing, missing on disk, outside storage, not a file, not readable). The log gains `WARN: N stored file(s) unreadable: file <id> "<title>" (<location>): <why>`.
  - **Multi-file entries**: database-resolved files publish `downloadUrl = <pageUrl>?do=download&r=<record_id>`. IPS keys an entry's files by `record_id`, so `r` returns exactly the record that was described and hashed, instead of the HTML file chooser (IPS source `view.php`: one file, or `r` given, returns the file). A non-matching `r` would silently return the entry's last file; the catalog hash catches that. The launcher's `IHCLDownloader.Download` takes the `downloadUrl` (new trailing parameter). `HCLBuildIpsDownloadUrl` appends the session's csrfKey only when it is on the page's own origin (scheme, host and port; userinfo refused); otherwise it builds the previous URL exactly. The request carries the forum session cookie.
- **2026-09-26 (04:00-12:00 UTC runs, r1112)**: upload-less files split as 0 external links, 20 no current upload, 55 no records; no web lookups; runs take ~45 s instead of ~260 s. Guest checks: Toolbox (file 809, 2 files) via `r=4636` returned `ToolBoxV.zip` matching the catalog SHA-256, while the plain link still returns the HTML chooser. Predator or Prey (a "no records" entry): IPS's own chooser says "0 files". The one unreadable file, Bismarck '92 (384), is not lost: the record says `384-1232895869-bismarck_\'92.ipb`, but on disk it is `384-1232895869-bismarck_/'92.ipb` (the backslash became a directory at some point), so IPS itself serves guests a 0-byte ZIP. Fix on the site, not in the generator.
- **2026-09-26**: **previous versions**. Scenarios name exact platform database builds (`HCDB2-170714`, `HCDB2-170611`, `HCDB2-161204`, ...), but the catalog offered only the current HCDB2 (`HCDB2-170909`). IPS keeps every earlier version in `downloads_filebackup` (`b_version`, `b_changelog`, `b_updated` = when that version was published, `b_backup` = when it was replaced, `b_records` = its record IDs, `b_hidden`), and HCDB2 alone has 26, all visible to guests. `?do=download&version=1082` returned `HCDB2-170611.zip` to a guest. The IPS page's version menu shows `b_backup` dates, which are when a version was *replaced*, so the 07/15/2017 entry holds 170611.
  - Generator: `ipsFetchVersions` reads the visible (`b_hidden=0`) versions for listed files, newest first, with their upload records (records from another file are ignored). `buildVersionObjects` emits each version with a file: label (`versionLabel()`), published date (`b_updated`, else `record_time`, else `b_backup`), plain-text changelog, name, size, SHA-256, and `?do=download&version=<b_id>&r=<record_id>`. A record's file never changes, so hashes are cached per record (`cache/records/<id>.json`, keyed by location) and computed once. Versions whose files are all gone are dropped (`versions_without_file`); unreadable ones are listed without a hash and named in a `WARN` line. The current version also gets `versionLabel`. An entry with no current upload can still list older versions.
  - Launcher: `THCLCatalogFile` gains `VersionLabel`, `Versions` and `IsPreviousVersion`. `HCLSelectVersion` returns the file as a chosen version would be downloaded; `HCLVersionCaption` names versions for the picker. The Downloads details panel gets a **Version** dropdown (current first, then earlier ones with dates); choosing one shows its size, date, file name and changelog, and Download fetches that version. The file list's Version column shows the label and `(+N older)`. An earlier version downloads to `cache\versions\<name>\` and imports to a directory of its own (`HCLVersionedName`: the label when it contains a letter, e.g. `PlatformDBs\HCDB2-170611`, else the base name plus the label), so it never overwrites the current version.
  - Next: match a scenario's free-text `platformDB` ("HCDB2-170714 1980-2025 New Standard DB", "HCDB 150928", "HCDB@ 170909") to a specific version.
- **2026-09-26**: Bismarck '92 fixed on the site: record 230's `record_location` updated (phpMyAdmin) to `384-1232895869-bismarck_/'92.ipb`, where the file actually is; a guest now gets the 3,921-byte ZIP (`BISMARK.SC3/.BL3/.RD3`, `README.TXT`) instead of a 0-byte one. A twin, `383-1232895830-bismarck_/'92.ipb`, belongs to a file not in the public catalog and was left alone.
- **2026-09-26**: **platform database collections and companion files**. René Haar's hc-timelines-db collection (605, `hce_databases.zip`) holds `hce-databases/hce-timeline-dbs/<db>/commondb.res` plus a manual named for the build (`HC-80to95-DB_110101.pdf`) for 50to65, 65to80, 80to95 and 95to10, and empty placeholders (`hc-10toXX-DB`, `hc-35to50-db`, `hc-media-file`, `hce-stock-db`). The 50to65 and 80to95 files are byte-identical to entries 552 and 276; 65to80 and 95to10 are older builds than entries 257 (110227) and 597 (110112). None of René's databases ship a `commondb.rsr`: their pictures are the single 51 MB `commondb.rsr` in "picturefile for HCxxtoxxdb" (328).
  - Launcher import: `HCLFindBundledDBs` finds every directory holding `commondb.res`, named by the alphabetically first dated (six-digit run) file beside it, else the directory name. More than one means a collection: the user confirms a list, and each goes to `PlatformDBs\<name>\` via `IHCLImport.PlanBundledDB`, which extracts only its own directory (new `AOnlyUnderPrefix` on `IHCLArchive.Extract`). Checked against the real archive: four databases, two files each.
  - Pictures: the catalog's new `companions` (`[{"id":"harpgamer:328","provides":"commondb.rsr"}]`) comes from a curated list in the generator (`companions` option: 605, 257, 276, 597 → 328; a config value replaces it). Only entries that name a specific picture file are listed: 328's description names the 65to80, 80to95 and 95toxx databases and the collection asks for "the mediafile"; 552 (50to65) names none. Every other database, HCDB2 included, uses the **stock** `commondb.rsr` in the game directory and needs nothing (database folders normally hold only `commondb.res`). After a PlatformDB import with a named companion, any database directory with `commondb.res` but no `commondb.rsr` or `rsr\` (`HCLPlatformDBNeedsRsr`) gets one: the launcher downloads the companion (cached), extracts its `commondb.rsr` once to `cache\extracted\<zip>\`, and copies it into each directory (`HCLPlaceCompanionFile`), so every such folder is complete on its own (4 × 51 MB for the collection; confirmed as wanted). Existing pictures are never replaced. Checked with the real 328 archive: all four copies byte-identical.

---

## References

- HarpGamer Downloads: https://harpgamer.com/harpforum/files/
- IPS REST API docs: https://invisioncommunity.com/developers/rest-api/index/
- IPS REST API guide: https://invisioncommunity.com/4guides/developing-plugins-and-applications/rest-api_370
- IPS API key setup: AdminCP → System → REST & OAuth → API Keys
- IPS API Reference (per-site): AdminCP → System → REST API → API Reference
- **2026-09-26 (16:00 UTC run, r1114)**: 985 previous versions across 281 entries; 981 listed (4 with no file left), 500 hashed (cap), 472 deferred to the next run, 47 s. Bismarck '92 now resolves (hash `29969eb7…`). Guest downloads of HCDB2 170611 (`version=1082&r=3402`) and 170714 (`version=1094&r=3469`) matched the catalog's SHA-256. Nine previous-version files are unreadable: seven genuinely absent from storage; Rourke's Drift (250) is stored as `rourke's_drift.ipb` while its record says `rourke\'s_drift.ipb` (the Bismarck pattern, without the folder split).
- **2026-09-26**: **repeated labels keep the newest**. HCDB2 had two uploads labelled `HCDB2-170714` (published 07-15 and 07-19; same container format, the later one rebuilt four days on) and a first `HCDB2-161204.zip` (version field "161204") whose `commondb.res` was actually the previous HCDB-151001 with the older 36-byte DATABASE_VERSION record, replaced a day later. Clients key cache and install folders on the label, so `buildVersionObjects` now offers only the newest version per label, the current version's label included (`versions_superseded_same_label`). `versionLabel` prefers the file name when it contains the version field (`161204` + `HCDB2-161204.zip` → `HCDB2-161204`), so that upload matches on the label; a file name reused across releases (`Toolbox.zip`, versions 1.0/1.1) still keeps the version field and stays distinct.
  - Decision (Tony, 2026-09-26): keep **only the newest** upload per label, including same-named revisions with different content (~272 of the 530 dropped, mostly scenarios revised in place). The alternative (drop exact repeats only, date-suffix the rest) was considered and declined.
- **2026-09-26**: **scenario placement**. Pinger Charlie (harpgamer:68, catalog dependency just "HCDB") was cached in `BattleSets\cache` and installed under `PlatformDBs\HCDB\`. Two fixes:
  - Cache follows install: `HCLDownloadCategory` sends a scenario that declares a platform database to `PlatformDBs\cache` (others to `BattleSets\cache`); if the scenario file reveals a database the catalog did not declare, the ZIP is moved there before import.
  - Exact database from the scenario file: a versioned `.SC?` starts `$FFFF`, u16 format version, then `DATABASE_VERSION.RESFile.flavor` (32 bytes, NUL-terminated) at byte 4 (HC `Common/SRC/Initscen.c`). `PINGER.SCm` says `HCDB-060103`; later bytes in newer files are lineage history. `HCLScenarioDatabase` reads it from the archive in memory (`IHCLArchive.ReadEntry`) and the import installs under `PlatformDBs\<that name>\Scenarios\`; scenarios that disagree, or version-0 battleset scenarios (no `$FFFF`), fall back to the catalog's dependency or the prompt. The confirmation says when that database is not installed yet.
  - Custom battlesets: the extension's last letter maps to a battleset slot in the GE (`.SCm` → `HDSH`), but BSBuilder slots HDSM–HDSU hold whatever a player installed, so the letter is never used to choose a battleset.
  - Next: offer to download the exact database when it is not installed (it may be among an entry's previous versions), i.e. the scenario-to-database matching step, now with an exact key.
- **2026-09-26**: **scenario databases are fetched automatically** (Tony: "auto-download the DB if it is available; if not, a newer DB in the family"). `uHCLDatabaseMatch` reduces names to family + date and picks, for the database a scenario names (scenario file first, catalog dependency second): installed (any folder whose name reduces to it and holds `commondb.res`) → exact build from the catalog (current or earlier version) → nearest newer build of the same family, installed or downloaded. The confirmation says which and why; on Yes the database is downloaded (or taken from the cache), installed with its picture companion, and the scenario placed under it. Against the live catalog (284 candidates): Pinger Charlie's HCDB-060103 → HCDB-070414 (entry 214, nearest newer); HCDB2-170714 → exact, an earlier version of 934; HCDB-150502 → exact, an earlier version of 235; HCWW-101110 → exact. Several installed scenarios name HCDB-150929, which no upload has (the HCDB-150928 download is HCDB-150928 inside too; the game ships HCDB-250724), so they get HCDB-250724. That is correct: databases are forward compatible within a family, so only newer builds may substitute, never an older one however close (Tony, 2026-09-26).
  - **Downloaded indicator** (Tony): the file list's new **Have** column shows "Yes" when the current version is in a cache, "Older" when only an earlier version is; the details line adds "Downloaded"; version-picker entries add " - downloaded". A copy counts in any category's cache when its size matches the catalog, so downloads made under earlier placement rules are found. Refreshed after each download.
- **2026-09-26**: **Install / Download again / Play** (Tony). A downloaded item keeps its button, relabelled **Install**: it imports from the cached ZIP without touching the network (re-import after deleting a folder, pick up a missing database). Right-click **Download again** deletes the cached copy and fetches a fresh one. **Play** (button, right-click, or double-click an installed scenario) starts the GE on it: the Game Engine tab's options, with `-c` the installed `.SC?` (`HCLFindInstalledScenario`: `PlatformDBs\*\Scenarios\<name>\`, then `BattleSets\*\Scenarios\<name>\`) and `-d`/`-r` that PlatformDB folder's `commondb.res`/`commondb.rsr` when present; battleset, scenario-number and saved-game selections are cleared for that launch.
