# App Configuration Settings — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Create a centralized admin settings page that controls mobile app behavior (content rules, ads, feature flags, appearance, performance, feed algorithm) without requiring app rebuilds.

**Architecture:** Admin CGI saves to `app_settings` table → Node.js API endpoint reads with 60s cache → Mobile React Context fetches on launch with 5min cache → Screens consume via `useAppConfig()` hook. Consolidates existing `feed_settings.cgi` into a tab.

**Tech Stack:** Perl CGI (admin), Node.js/Express (API), React Native/Expo (mobile), MySQL `app_settings` table, Tailwind CSS (admin UI)

---

### Task 1: Seed Default Settings in Database

**Files:**
- No file changes — SQL only

**Step 1: Insert default `app_*` settings**

```sql
INSERT INTO app_settings (setting_key, setting_value, updated_by) VALUES
('app_video_max_duration', '60', 'system'),
('app_video_max_loops', '2', 'system'),
('app_caption_max_length', '2000', 'system'),
('app_password_min_length', '6', 'system'),
('app_report_reasons', '["Inappropriate","Harassment","Spam"]', 'system'),
('app_ad_interval', '10', 'system'),
('app_ad_zone_feed', '55', 'system'),
('app_ad_zone_jobs', '56', 'system'),
('app_ad_zone_members', '57', 'system'),
('app_ad_zone_shop', '58', 'system'),
('app_feature_marketplace', '1', 'system'),
('app_feature_nearby', '1', 'system'),
('app_feature_messaging', '1', 'system'),
('app_feature_video_posts', '1', 'system'),
('app_feature_registration', '1', 'system'),
('app_feature_jobs', '1', 'system'),
('app_color_primary', '#0069ed', 'system'),
('app_color_accent', '#c8102e', 'system'),
('app_color_accent_gold', '#d4a843', 'system'),
('app_color_header_bg', '#000000', 'system'),
('app_logo_light', 'https://hbcuconnect.com/hbcu-app/assets/HBCUConnect840.png?v=2', 'system'),
('app_logo_dark', 'https://hbcuconnect.com/hbcu-app/assets/HBCUConnect540.png?v=2', 'system'),
('app_welcome_title', 'Welcome to the Feed!', 'system'),
('app_welcome_text', 'Follow members and post photos to see content here.', 'system'),
('app_poll_interval', '30000', 'system'),
('app_nearby_radius_default', '25', 'system'),
('app_nearby_radius_options', '[5,10,25,50,100]', 'system'),
('app_view_delay_video', '3000', 'system'),
('app_view_delay_photo', '1000', 'system')
ON DUPLICATE KEY UPDATE setting_key = setting_key;
```

**Step 2: Verify**

Run: `mysql ... -e "SELECT setting_key FROM app_settings WHERE setting_key LIKE 'app_%' ORDER BY setting_key"`
Expected: 29 rows

---

### Task 2: Create API Endpoint — GET /api/app/config

**Files:**
- Create: `hbcu-app/api/routes/appConfig.js`
- Modify: `hbcu-app/api/server.js` (add route mount)

**Step 1: Create `appConfig.js`**

```javascript
const express = require('express');
const router = express.Router();
const { hbcuPool } = require('../config/database');

// Cache: 60-second TTL (same pattern as getFeedSettings in media.js)
let configCache = null;
let configCacheTime = 0;
const CONFIG_TTL = 60000;

async function getAppConfig() {
  const now = Date.now();
  if (configCache && (now - configCacheTime) < CONFIG_TTL) {
    return configCache;
  }
  const [rows] = await hbcuPool.query(
    "SELECT setting_key, setting_value FROM app_settings WHERE setting_key LIKE 'app_%'"
  );
  const raw = {};
  for (const row of rows) {
    raw[row.setting_key] = row.setting_value;
  }

  // Transform DB keys to camelCase config object with typed defaults
  const config = {
    // Content Rules
    videoMaxDuration: parseInt(raw.app_video_max_duration) || 60,
    videoMaxLoops: parseInt(raw.app_video_max_loops) || 2,
    captionMaxLength: parseInt(raw.app_caption_max_length) || 2000,
    passwordMinLength: parseInt(raw.app_password_min_length) || 6,
    reportReasons: parseJSON(raw.app_report_reasons, ['Inappropriate', 'Harassment', 'Spam']),

    // Ads
    adInterval: parseInt(raw.app_ad_interval) || 10,
    adZones: {
      feed: parseInt(raw.app_ad_zone_feed) || 55,
      jobs: parseInt(raw.app_ad_zone_jobs) || 56,
      members: parseInt(raw.app_ad_zone_members) || 57,
      shop: parseInt(raw.app_ad_zone_shop) || 58,
    },

    // Feature Flags
    features: {
      marketplace: raw.app_feature_marketplace !== '0',
      nearby: raw.app_feature_nearby !== '0',
      messaging: raw.app_feature_messaging !== '0',
      videoPosts: raw.app_feature_video_posts !== '0',
      registration: raw.app_feature_registration !== '0',
      jobs: raw.app_feature_jobs !== '0',
    },

    // Appearance
    colors: {
      primary: raw.app_color_primary || '#0069ed',
      accent: raw.app_color_accent || '#c8102e',
      accentGold: raw.app_color_accent_gold || '#d4a843',
      headerBg: raw.app_color_header_bg || '#000000',
    },
    logos: {
      light: raw.app_logo_light || 'https://hbcuconnect.com/hbcu-app/assets/HBCUConnect840.png?v=2',
      dark: raw.app_logo_dark || 'https://hbcuconnect.com/hbcu-app/assets/HBCUConnect540.png?v=2',
    },
    welcomeTitle: raw.app_welcome_title || 'Welcome to the Feed!',
    welcomeText: raw.app_welcome_text || 'Follow members and post photos to see content here.',

    // Performance
    pollInterval: parseInt(raw.app_poll_interval) || 30000,
    nearbyRadiusDefault: parseInt(raw.app_nearby_radius_default) || 25,
    nearbyRadiusOptions: parseJSON(raw.app_nearby_radius_options, [5, 10, 25, 50, 100]),
    viewDelayVideo: parseInt(raw.app_view_delay_video) || 3000,
    viewDelayPhoto: parseInt(raw.app_view_delay_photo) || 1000,
  };

  configCache = config;
  configCacheTime = now;
  return config;
}

function parseJSON(str, fallback) {
  try { return JSON.parse(str); }
  catch { return fallback; }
}

/**
 * GET /api/app/config
 * Public endpoint — no auth required
 * Returns all app configuration for the mobile client
 */
router.get('/config', async (req, res) => {
  try {
    const config = await getAppConfig();
    res.json(config);
  } catch (err) {
    console.error('App config error:', err);
    res.status(500).json({ error: 'Failed to load config' });
  }
});

module.exports = router;
```

**Step 2: Mount route in `server.js`**

Add after the health check route (line ~48), before the auth route:
```javascript
// App config (no auth required)
app.use('/api/app', require('./routes/appConfig'));
```

**Step 3: Restart API and test**

Run: `pm2 restart hbcu-api`
Run: `curl -s https://hbcuconnect.com/api/app/config | python3 -m json.tool | head -30`
Expected: JSON with videoMaxDuration, features, colors, etc.

**Step 4: chown + commit**

---

### Task 3: Create Mobile AppConfigContext

**Files:**
- Create: `hbcu-app/mobile/src/context/AppConfigContext.js`
- Modify: `hbcu-app/mobile/App.js` (wrap with provider)

**Step 1: Create `AppConfigContext.js`**

```javascript
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import api from '../services/api';

// Hardcoded defaults — app works even if API fails
const DEFAULTS = {
  videoMaxDuration: 60,
  videoMaxLoops: 2,
  captionMaxLength: 2000,
  passwordMinLength: 6,
  reportReasons: ['Inappropriate', 'Harassment', 'Spam'],
  adInterval: 10,
  adZones: { feed: 55, jobs: 56, members: 57, shop: 58 },
  features: { marketplace: true, nearby: true, messaging: true, videoPosts: true, registration: true, jobs: true },
  colors: { primary: '#0069ed', accent: '#c8102e', accentGold: '#d4a843', headerBg: '#000000' },
  logos: {
    light: 'https://hbcuconnect.com/hbcu-app/assets/HBCUConnect840.png?v=2',
    dark: 'https://hbcuconnect.com/hbcu-app/assets/HBCUConnect540.png?v=2',
  },
  welcomeTitle: 'Welcome to the Feed!',
  welcomeText: 'Follow members and post photos to see content here.',
  pollInterval: 30000,
  nearbyRadiusDefault: 25,
  nearbyRadiusOptions: [5, 10, 25, 50, 100],
  viewDelayVideo: 3000,
  viewDelayPhoto: 1000,
};

const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes

const AppConfigContext = createContext(DEFAULTS);

export function AppConfigProvider({ children }) {
  const [config, setConfig] = useState(DEFAULTS);
  const timer = useRef(null);

  useEffect(() => {
    fetchConfig();
    timer.current = setInterval(fetchConfig, REFRESH_INTERVAL);
    return () => clearInterval(timer.current);
  }, []);

  async function fetchConfig() {
    try {
      const res = await api.get('/app/config');
      if (res.data && typeof res.data === 'object') {
        setConfig({ ...DEFAULTS, ...res.data });
      }
    } catch (err) {
      // Keep defaults on failure — app still works
    }
  }

  return (
    <AppConfigContext.Provider value={config}>
      {children}
    </AppConfigContext.Provider>
  );
}

export function useAppConfig() {
  return useContext(AppConfigContext);
}
```

**Step 2: Wrap App.js with `AppConfigProvider`**

In `App.js`, add import and wrap inside `AuthProvider`:
```javascript
import { AppConfigProvider } from './src/context/AppConfigContext';

// In the render:
<AuthProvider>
  <AppConfigProvider>
    <StatusBar style="light" />
    <AppNavigator />
  </AppConfigProvider>
</AuthProvider>
```

`AppConfigProvider` goes inside `AuthProvider` so it can use the api instance (which may need auth headers for other calls, though this endpoint is public).

---

### Task 4: Wire Mobile Screens to Config

**Files to modify** (replace hardcoded constants with `useAppConfig()` values):

#### 4a. `FeedScreen.js`
- Replace `const MAX_LOOPS = 2;` → use `config.videoMaxLoops`
- Replace `3000` / `1000` view delays → use `config.viewDelayVideo` / `config.viewDelayPhoto`
- Replace hardcoded report reasons → use `config.reportReasons`
- Replace hardcoded welcome text → use `config.welcomeTitle` / `config.welcomeText`
- Replace `AD_ZONES.Feed` → use `config.adZones.feed`
- Replace `insertAds(posts, AD_ZONES.Feed)` → use `insertAds(posts, config.adZones.feed, config.adInterval)`

Add at top of component: `const config = useAppConfig();`
Add import: `import { useAppConfig } from '../../context/AppConfigContext';`

#### 4b. `CreatePostScreen.js`
- Replace `const MAX_CAPTION = 2000;` → use `config.captionMaxLength`
- Replace `duration > 60` → use `config.videoMaxDuration`
- Replace `"60 seconds"` text → use dynamic text from config value
- If `!config.features.videoPosts`, hide video option in media picker

Add at top of component: `const config = useAppConfig();`

#### 4c. `PostDetailScreen.js`
- Replace `const MAX_LOOPS = 2;` → use `config.videoMaxLoops`

#### 4d. `adHelpers.js`
- Keep `AD_ZONES` and `insertAds` as-is for backward compat
- Screens pass config values explicitly: `insertAds(items, config.adZones.feed, config.adInterval)`

#### 4e. `NearbyScreen.js`
- Replace `const RADIUS_OPTIONS = [5, 10, 25, 50, 100];` → use `config.nearbyRadiusOptions`
- Replace `useState(25)` → use `config.nearbyRadiusDefault`

#### 4f. `NotificationBell.js`
- Replace `30000` interval → use `config.pollInterval`
- Note: NotificationBell doesn't have easy access to context (it's a component used in headers). Pass `pollInterval` as a prop from the screen that renders it, OR use the context directly.

#### 4g. `MainTabs.js`
- Replace `30000` poll interval → use `config.pollInterval`
- Use `config.features` to conditionally render tabs:
  - If `!features.messaging` → hide MessagesTab
  - If `!features.jobs` → hide JobsTab
  - If `!features.marketplace` → hide ShopTab

#### 4h. `MemberSearchScreen.js`
- Replace `30000` poll interval → use `config.pollInterval`
- Replace `AD_ZONES.Members` → use `config.adZones.members`

#### 4i. `SettingsScreen.js`
- Replace `newPassword.length < 6` → use `config.passwordMinLength`
- Replace `"at least 6 characters"` text → use dynamic text from config

#### 4j. `theme.js`
- No changes needed — `AppConfigContext` provides color overrides, and screens that need dynamic colors read from config. The static `theme.js` serves as the compile-time default.

---

### Task 5: Create Admin Page — app_settings.cgi

**Files:**
- Create: `admin/app/app_settings.cgi`
- Modify: `admin/app/index.cgi` (update tool card links)

**Structure:** Single Perl CGI page with 6 tabs, AJAX load/save per tab. Uses existing adminUI.pm Tailwind pattern.

**AJAX actions:**
- `get_settings` — Returns all `app_*` and `feed_*` keys
- `save_settings` — Accepts JSON hash, saves with ON DUPLICATE KEY UPDATE (scoped to `app_*` and `feed_*` prefixes)
- `reset_defaults` — Accepts `tab` param, resets that tab's keys to defaults

**Tab layout:**
1. **Content Rules** — number inputs for video duration/loops/caption/password, editable tag list for report reasons
2. **Ads & Monetization** — number inputs for ad interval + zone IDs
3. **Feature Flags** — toggle switches (green/red) for each feature
4. **Appearance** — color picker inputs, URL text inputs, textarea for welcome messages
5. **Performance** — number inputs for poll interval, nearby defaults, view delays. JSON editor for radius options
6. **Feed Algorithm** — exact same UI as current `feed_settings.cgi` (algorithm selector, sliders, score inputs, preview table)

**Admin page index.cgi update:**
- Replace `feed_settings.cgi` link with `app_settings.cgi`
- Update card title to "App Settings"
- Update description to "Configure app behavior, appearance, features, ads, and feed algorithm"

---

### Task 6: Build, Test, Deploy

**Step 1: Syntax check admin CGI**
Run: `cd /var/www/vhosts/hbcuconnect.com/httpdocs/admin && perl -c app/app_settings.cgi`

**Step 2: Restart API**
Run: `pm2 restart hbcu-api`

**Step 3: Rebuild Expo preview**
Run: `cd /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/mobile && npx expo export --platform web --output-dir ../preview`

**Step 4: Fix ownership**
Run: `chown -R hbcuconnect:psacln /var/www/vhosts/hbcuconnect.com/httpdocs/admin/app/app_settings.cgi /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/api/routes/appConfig.js /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/mobile/src/context/AppConfigContext.js /var/www/vhosts/hbcuconnect.com/httpdocs/hbcu-app/preview/`

**Step 5: Manual verification**
1. Visit `https://hbcuconnect.com/admin/app/app_settings.cgi` — all 6 tabs load with correct defaults
2. Change a setting (e.g., ad interval to 15), save, reload — persists
3. Visit `https://hbcuconnect.com/api/app/config` — returns updated value
4. Open mobile app preview — settings take effect (within 5 min or on fresh load)
5. Toggle a feature flag off (e.g., marketplace) → tab disappears from app
6. Feed Algorithm tab: verify algorithm mode toggle and scoring weights work same as old page
