# Northstar ASO — private cPanel admin panel

Northstar is a clean-room, single-user ASO workspace based on the project blueprint. It is built with PHP 8+, HTML, responsive CSS, and vanilla JavaScript. It does not copy another service's code, database, private API, login session, token, or visual assets.

## What is included

- Private admin-style dashboard for one app and its competitors.
- ASO health scorecard with explainable components.
- Keyword lab with local keyword table, inspector, free related-keyword discovery, manual rank snapshots, history, and CSV export.
- Metadata lab with character counters, coverage preview, and local draft saving.
- Competitor comparison cards and keyword-overlap view.
- Reports page and local activity timeline.
- Protected JSON storage so the first upload can run without MySQL.
- Optional server-side Gemini brief for the local keyword/metadata snapshot.
- ASO Generator for iOS, Google Play, or both stores with separate output fields and local policy checks.
- Ranked-keyword workflow: load the private local keyword plan into the generator, prioritize ranked terms, and see per-store keyword coverage in generated drafts.
- Bulk keyword workflow: paste up to 1,000 keyword rows, optionally with manual ranks, then sort, select, and copy ranked terms for the next metadata step.
- Text Analyzer for character/word counts, found keywords, repetitions, neutral stuffing signals, and optional competitor gaps.
- App Report for public store import, up to five competitors, metadata/media comparison, visibility snapshots, and transparent market-signal availability.
- App Store Connect settings panel with server-side `.p8` upload, JWT connection test, and credential removal.
- Gemini settings panel with server-side key storage, status, and removal.

## cPanel upload

1. Upload the contents of this folder into a PHP 8.1+ domain or subdomain directory.
2. Confirm `storage/` is writable by PHP. Normally `755` is enough; use `775` only if your host requires it.
3. Open `index.php` in the browser. The local workspace initializes its storage file automatically.
4. Protect the directory with cPanel Directory Privacy if the site is not meant to be public.
5. Keep the included `storage/.htaccess` in place. It prevents direct downloads of the local data file on Apache hosts.
6. Do not upload your local `storage/aso-data.json` — the panel creates a fresh one on first load.

Your owner name and the greeting come from the `OWNER_NAME` constant in `config.php`.

If the host is nginx or LiteSpeed without `.htaccess` support, `storage/` is not protected by those rules. In that case move the storage folder above the web root and point `STORAGE_DIR` in `config.php` at it.

### Apple App Store Connect setup

Open **Settings → Apple API credentials** after your App Store Connect API access is approved. Enter the Issuer ID and Key ID, upload the downloaded `.p8` private key, and choose **Save credentials**. The key is stored in the protected `storage/` folder and is never returned to browser JavaScript. Use **Test connection** to make a read-only `GET /v1/apps?limit=1` request.

Keep this cPanel directory private with Directory Privacy or equivalent server authentication before uploading an Apple key. This starter panel is designed for the owner's internal workspace; it must not be exposed as a public third-party API service. The key can be removed from the panel at any time. The Apple key is not included in the deployment zip.

## Gemini setup

The Gemini key is used only in `api.php` on the server. It is never inserted into browser JavaScript.

The preferred setup is **Settings → Gemini API credentials**. Paste the key, choose **Save Gemini key**, and the panel stores it in the protected `storage/` folder. The older `config.local.php` and `GEMINI_API_KEY` environment-variable methods remain available as fallbacks.

### Option A — local config file

Copy `config.local.example.php` to `config.local.php`, then replace the placeholder key. `config.local.php` is denied by `.htaccess`.

### Option B — environment variable

Set `GEMINI_API_KEY` in the hosting environment if your cPanel plan supports PHP environment variables.

The integration uses Google's REST `interactions` endpoint with the `x-goog-api-key` header and `gemini-3.6-flash` model. Gemini analyzes only the local snapshot supplied by this app. It does not replace an App Store data source and it should not be sent private user credentials or tokens.

## ASO Generator

Open **ASO generator** from the admin navigation, select iOS, Google Play, or both, and provide the app name, product summary, features, audience, and ranking keywords. The generator returns separate drafts:

- iOS: app name, subtitle, keyword field, and description.
- Google Play: title, short description, and full description.

The local checker applies the current documented limits: Apple name/subtitle 30 characters, Apple keyword field 100 characters, Apple description 4,000 characters; Google Play title 30 characters, short description 80 characters, and full description 4,000 characters. It also warns about promotional/ranking claims, repeated punctuation, app-name duplication in the Apple keyword field, and possible keyword stuffing.

The generator is an assistant, not an App Review guarantee. Final copy must be checked against the latest official policies:

Use **Load current app** to bring imported metadata into the generator, then choose **Use ranked keywords**. The tool orders keywords by the saved rank snapshot (best rank first), followed by unranked opportunities. Gemini is instructed to use relevant supplied terms naturally; the local fallback includes the terms in the appropriate description fields. Each output shows coverage and missing terms. Coverage is a planning aid, not a guarantee of store indexing.

In **Keyword Lab → Bulk workflow**, paste one keyword per line or `keyword, rank` rows. Preview first, then import. The rank plan keeps lower rank numbers first; Top 10/50/100 or selected rows can be copied as newline-separated text for the generator or another analysis tool.

Use **Delete** beside a tracked keyword to remove that term and its local rank snapshots. Use **Clear all** in the bulk workflow to clear the complete tracked-keyword list and rank history; both actions ask for confirmation and do not affect app metadata or API credentials.

A rank is only read when an explicit delimiter separates it — `keyword, 12`, a tab, a pipe, or a semicolon. A bare trailing number stays part of the keyword, so `financial planner 2024` imports as a keyword rather than as rank 2024.

**Clean up rows** repairs older imports: it splits any row that stored a whole comma-separated list into separate keywords and trims a leading `Country:` label. The rank on the first term of each row is kept.

The importer also accepts a comma-separated keyword list. If an older import created one malformed combined row, enable **Replace existing tracker before import**, preview the corrected rows, and import again. This explicitly clears the old local keyword/rank list before saving the new one.

- [Apple App information](https://developer.apple.com/help/app-store-connect/reference/app-information/app-information)
- [Apple platform version information](https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information)
- [Google Play store listing best practices](https://support.google.com/googleplay/android-developer/answer/13393723)
- [Google Play Metadata policy](https://support.google.com/googleplay/android-developer/answer/9898684)

## Text Analyzer

Open **Text analyzer** from the admin navigation. Select Apple App Store or Google Play, paste a description/review/competitor text, and optionally paste one ranking keyword per line. The tool calculates:

- characters with spaces and without spaces;
- total words, unique words, sentences, and reading time;
- found keyword count, density, local rank, traffic proxy, and opportunity score;
- repeated terms with count, density, and neutral repetition signals;
- an explainable review signal for unusual repetition, not a policy-risk verdict;
- optional gaps when a competitor description contains a selected keyword that your text does not.

The analyzer is intentionally heuristic. Repetition is a review signal, not an automatic policy violation, and traffic/rank values are only shown when they exist in the local workspace data.

## Free data mode

The Keyword Lab includes two free workflows:

- **Related keyword finder:** combines your imported app/competitor metadata with Apple’s public iTunes Search response for the selected country. Results are related-term mentions and a deterministic ranking-potential proxy, not official search volume or organic rank.
- **Manual rank snapshots:** search the store yourself, enter the observed position, and save a dated country/store snapshot. The next snapshot calculates the change for the same keyword, platform, and country.

App imports use Apple’s public lookup response or a public Google Play listing page. Google Play public metadata is useful for app text and ratings, but this free build does not claim official Play keyword rank, downloads, revenue, or category rank. This keeps the panel honest and avoids private endpoints, account-cookie scraping, or anti-bot workarounds.

## Google Play Console live connector

The private Settings panel can connect one of your own Android apps to the official Google Play Developer API. Set up a Google Cloud project, enable the API, create a service account, and invite that service-account email in Google Play Console Users and permissions. Upload the JSON key and enter the app package name; the key stays in the server-side `storage` folder.

The connector can sync recent reviews, localized Play listings, and current release tracks. It creates only a temporary read snapshot and never commits a store edit. It does not provide competitor keyword rank or Play Store search-volume data because the official Developer API does not expose those ASO metrics.

## App Report

Open **App report** from the admin navigation. Choose iOS or Google Play, select a country, and import the primary app using an App Store ID, package name, or public store link. Then add up to five competitor IDs/links.

The current public-snapshot importer supports:

- Apple public lookup metadata, ratings, version date, icon, screenshots, category, price, and size where returned;
- Google Play public-page metadata such as title, description, icon, and rating when the hosting server can read the page;
- local keyword visibility and Top 1/5/10 distribution from the workspace snapshots;
- metadata length comparison, media comparison, country snapshot, and update timeline;
- explicit unavailable states for competitor downloads, revenue, category history, worldwide availability, and similar-app history.

The app report does not invent downloads, revenue, or category ranks. Those require a permitted first-party or licensed data source. Imports are one-at-a-time public snapshots with a clear source and fetch timestamp.

## Testing without Gemini

The admin panel works without a Gemini key. The “Ask Gemini” button will explain that the connector is not configured. All other local features continue to work.

## Keyword research

**Keyword research** is the starting point. Pick the store and country, type up to twenty comma-separated keywords, and press Search. Each keyword returns:

- **Popularity** — the term's position inside Apple's own search suggestions, which are ordered by how often people search. A demand proxy, not licensed search volume.
- **Difficulty** — measured, not guessed. For the top 5, top 10, and top 20 separately, the panel states the ratings of a typical app in that range, the ratings of the weakest app you would have to out-rank, how many sit under 1K ratings, how many put the keyword in their own title, and how many broke in during the last year. The score weights the median rating count (logarithmically, because 1K → 10K is the same kind of jump as 10K → 100K) against title usage, and the top five count for half the overall number.
- **Opportunity** — popularity scaled by how beatable the incumbents are.
- **The apps holding each position**, with ratings, stars, whether the keyword is in their title, and a link.
- **The keyword pool those apps rank with**, with a count of how many of them use each term in their name. Click a term to research it next.

- **Keyword clusters** — the pool grouped around its shared words, so sixty phrases read as a handful of themes. Each cluster copies on its own.
- **Estimated downloads by position** — a modelled range per position 1–20, from a demand proxy and a position curve calibrated against the ranges commercial tools publish. Directional shape, never a forecast, and labelled that way on the chart.

From each result card: **Copy all keywords**, **Send to Text analyzer**, or **Add to Keyword lab**. Every search is kept in the history table with its scores.

## Competitor keyword gap

**Competitors → Find gaps** compares your imported listing against every competitor listing and returns the phrases they use that yours does not.

Three rules keep the list honest. Word-combination coverage is checked against your name and subtitle only, because those are the fields Apple joins words across — a word buried in a 4,000 character description does not make a phrase covered. A single word only counts when a rival put it in their app *name*, since anywhere else it is ordinary prose. And product names are removed by cross-listing evidence: a word in one app's name that appears nowhere in any rival's store text is that app's brand, so "rocket" drops out while "expense" stays.

Select the gaps worth closing and add them straight to the Keyword lab.

Google Play has no public ranked search endpoint, so Play difficulty is refused rather than estimated from nothing.

## Apple performance (your own app)

**Settings → Apple performance** pulls real numbers for your app through the App Store Connect API. Choose **Load my apps**, pick the app, add the vendor number, save, then **Sync performance**. Two different Apple systems are used:

- **Downloads** come from Sales and Trends (`GET /v1/salesReports`). They are available immediately, one gzipped report per day, and the last seven available days are summed. Update installs are excluded, so the number is first-time downloads. This needs the vendor number from App Store Connect → Payments and Financial Reports.
- **Impressions and product page views** come from Analytics Reports. This system is asynchronous: the first sync creates an ongoing report request, Apple then starts collecting, and the data appears roughly one to two days later. Sync again after that. Tap-through rate is page views ÷ impressions, and conversion rate is downloads ÷ impressions — each is shown only when both of its inputs actually arrived.

Anything Apple has not returned yet is reported as a note instead of being filled with a placeholder.

## Keyword discovery

**Keyword lab → Discover keywords from Apple** combines two independent public signals. Each row shows which signals it came from, and a term found by both is the strongest candidate available.

- **App Store search suggestions** are the terms the store offers as a user types, in Apple's own popularity order. That order is the closest honest proxy for demand without a paid data provider. Increasing the sweep depth also queries `seed a`, `seed b`, … which surfaces the long tail. These come from the store client's suggestion endpoint rather than a documented public API, so they are best-effort: responses are cached for six hours, calls are spaced out, and if Apple starts rate limiting mid-sweep the panel says so instead of pretending the term has no suggestions.
- **Competitor listings** are mined through the documented iTunes Search API. Up to 200 apps ranking for the seed are read, and every 1–3 word phrase in their names and descriptions is scored by how many of them use it, weighted heavily toward phrases that appear in app *names* — a name slot is the most expensive space on the store, so a phrase there was chosen deliberately.

The panel separates **targetable** terms from **app names**. Apple's suggestions mix generic searches with competitor brand completions like "budget planner – cirka"; a brand is detected by matching the fetched app names and by spotting words that no listing in the category uses. Branded terms stay visible, because knowing competitors are searched by name is useful, but they never outrank a term you could actually win.

Neither signal is official search volume or organic rank, and the panel says so on every run.

## Keyword placement engine

The generator does not just "use" your keywords — it decides, per store, which field each term belongs in, then writes around that plan. Open **ASO generator**, load the ranked plan with **Use ranked keywords**, and press **Preview placement** to see the allocation before any text is written.

Terms are ordered by observed rank first (position 1 outranks position 40), then by opportunity score. That order drives every placement.

### Apple

Apple indexes only three fields you control: the app name, the subtitle, and the 100-character keyword field. The description is **not** indexed. Apple also joins words *across* those fields to build phrases, which the engine uses directly:

- The strongest term goes into the name, the next ones into the subtitle.
- The keyword field is then built mechanically from the final name and subtitle. Every word those fields already index is skipped, because repeating it would only burn characters. So "budget" in the name plus "planner" in the keyword field already covers "budget planner".
- Stop words, plural duplicates (`planner` / `planners`), and words like "app" or "free" are dropped — Apple ignores them.
- Words are joined with commas and no spaces, since App Store Connect counts the space.

Coverage is reported with the same model: a phrase counts as covered when every one of its words is indexed somewhere, not only when the exact phrase appears. The panel shows which field each term landed in — for example `keywords + subtitle` for a phrase assembled from both.

### Google Play

Play indexes the title, the short description, and the full description as ordinary text, so each term has to appear literally. The engine puts the strongest term in the title, the next few in the 80-character short description, and the rest in the full description, and warns when the focus term is missing or repeated more than about six times.

### The honest limit

100 characters do not hold hundreds of keywords. With a large tracked list the panel will report something like "153 of 792 indexed" and list the words that did not fit. That is the real Apple constraint, not a tool limitation — narrow the list to the terms you actually want to compete for, and the number rises.

## ASO health scorecard

The scorecard is recalculated on every save from the stored workspace — it is never a static number. Five components, each explained in the row note:

- **Indexation** — how many tracked terms have a saved rank snapshot (full marks at 60% coverage).
- **Keyword coverage** — how many tracked terms actually appear in your title, subtitle, or description (full marks at 35%).
- **Metadata quality** — presence and length discipline across the store fields.
- **Competitive space** — how many competitor snapshots are imported (full marks at four).
- **Rating health** — public rating weighted by how many people left one.

Visibility is the average rank quality of your ranked terms, where position 1 is worth far more than position 40. The keyword-visibility chart on the dashboard is drawn from your saved manual rank snapshots, grouped by day, with the dashed line showing the range average.

## Data safety

- Every write goes to a temporary file and is then renamed into place, so an interrupted write cannot truncate the workspace.
- The previous version is kept as `storage/aso-data.bak.json` and is used automatically if the main file is ever unreadable.
- Requests are serialized with a lock file, so two tabs saving at once cannot overwrite each other.
- Every action that changes stored data requires POST. A crawler, link prefetcher, or stray image tag cannot wipe the workspace by loading a URL.
- **Settings → Backup and reset** downloads the complete workspace as JSON, plus rank snapshots as CSV. Backups never contain Apple or Gemini credentials.
- Provider failures (Gemini, Apple, public store pages) are recorded in `storage/app.log`, which is denied by `.htaccess` and trimmed automatically.

## Personal workspace data

The first load starts blank with no sample app, ratings, keywords, or competitors. If an older upload already created the previous starter dataset, open **Settings → Reset workspace → Clear demo data**. This resets local workspace data but leaves Apple and Gemini credentials unchanged. Import the real app from **App report** to populate the dashboard.

## Data and connector policy

The starter storage is intentionally a local workspace snapshot so the interface can be tested immediately. Replace or extend `lib/data.php` with connectors for sources you are allowed to use. Before adding a connector, record its source URL, timestamp, method, terms, and rate limit. Do not add private endpoints, login-cookie scraping, token reuse, CAPTCHA bypasses, or anti-bot evasion.

## Database and automatic Apple collection

The app now has a non-destructive database path. The existing JSON workspace remains available for local preview and backup; when MySQL/MariaDB credentials are configured, every saved workspace is also copied to `aso_workspaces`, while dated Apple keyword observations, app metadata snapshots, collection runs, and collector logs are stored in normalized tables.

The SQL file is `database/schema.sql`. Import it once into the cPanel MySQL database. Then add the database values to the private `config.local.php` (or environment variables):

```php
'db_dsn' => 'mysql:host=localhost;dbname=CPANEL_DATABASE;charset=utf8mb4',
'db_user' => 'CPANEL_DATABASE_USER',
'db_pass' => 'DATABASE_PASSWORD',
'db_workspace' => 'default',
```

Run the one-time JSON migration from the application directory:

```bash
php database/migrate_json.php
```

If you specifically want one uploadable SQL data file after preparing the local workspace, generate it with:

```bash
php database/export_json_sql.php database/northstar-workspace-export.sql
```

Import `schema.sql` first, then the generated `northstar-workspace-export.sql`. The export contains the workspace data only; Apple `.p8`, Gemini, and Google service-account credentials are never included.

The Apple collector is `cron/collect_apple.php`. It reads the tracked keywords and selected/imported public app IDs, calls Apple’s public iTunes Search/Lookup endpoints at a controlled pace, and records dated observations. It does not claim those result positions are an official organic-rank feed. It is safe to run repeatedly: a lock prevents overlapping runs and history is append-only in the database.

For cPanel Cron Jobs, use a command similar to this, replacing the path with the real account path:

```bash
/usr/local/bin/php /home/CPANEL_USER/public_html/aso/cron/collect_apple.php
```

Set `ASO_COLLECTION_COUNTRIES=US,GB,CA,AU,DE` and optionally `ASO_COLLECTION_KEYWORDS=100` in the hosting environment to control the daily scope. If the host does not provide environment variables, the collector uses the workspace country and App Report country. The Google Play connector remains in the codebase and can stay configured for the owner’s app, but it is not required by this Apple-first collector.

The database is not a historical data provider: it starts recording from the first successful collector run. It cannot recreate years of past rankings that were never captured.

## PHP checks

Run this from the application folder when PHP CLI is available:

```bash
php -l index.php
php -l api.php
php -l config.php
php -l lib/helpers.php
php -l lib/data.php
php -l lib/keywords.php
php -l lib/appstore.php
php -l lib/discovery.php
```

## Verification modes

Before upload, run a global check: PHP lint, JavaScript syntax check, homepage/API smoke tests, public connector availability, cache-version check, and a deployment-archive secret scan.

Run a separate private check for this owner-only workspace: confirm Apple and Gemini credentials are stored only in protected server storage, are absent from browser JavaScript and the ZIP archive, and test real provider connections only after credentials are entered in Settings.

## Next engineering step

When a permitted first-party or licensed source becomes available, add it as a source-specific worker that normalizes allowed app metadata, keyword observations, reviews, or top-chart snapshots into the existing JSON shape. The free local workflow can continue alongside it. Move to MySQL only when the local history becomes large enough to need relational filtering.
