wsj-filenaming/api.php
DJP c9379fa89f Update TechLive events per client feedback
- Tech Live: TECL → TL
- TechLive Qatar: added (TLQ)
- TechLive Cyber: added (TLCYB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 13:14:58 -05:00

565 lines
22 KiB
PHP

<?php
header('Content-Type: application/json');
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/sheet_helpers.php';
// ---------------------------------------------------------------
// PUBLIC ENDPOINT: create_session
// Must be handled BEFORE the auth guard
// ---------------------------------------------------------------
$action = $_GET['action'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'create_session') {
$input = json_decode(file_get_contents('php://input'), true);
$idToken = $input['id_token'] ?? '';
if (empty($idToken)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing id_token']);
exit;
}
// Decode JWT payload (middle segment, base64url)
$parts = explode('.', $idToken);
if (count($parts) !== 3) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Malformed token']);
exit;
}
$payloadJson = base64_decode(strtr($parts[1], '-_', '+/') . str_repeat('=', (4 - strlen($parts[1]) % 4) % 4));
$payload = json_decode($payloadJson, true);
if (!$payload) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Cannot decode token payload']);
exit;
}
// Validate claims
$iss = $payload['iss'] ?? '';
$aud = $payload['aud'] ?? '';
$exp = $payload['exp'] ?? 0;
if (strpos($iss, AZURE_TENANT_ID) === false) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Invalid issuer']);
exit;
}
if ($aud !== AZURE_CLIENT_ID) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Invalid audience']);
exit;
}
if (time() > $exp) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Token expired']);
exit;
}
$email = $payload['preferred_username'] ?? $payload['email'] ?? '';
if (empty($email)) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'No email claim in token']);
exit;
}
session_regenerate_id(true);
$_SESSION['user_email'] = strtolower(trim($email));
echo json_encode(['success' => true]);
exit;
}
// ---------------------------------------------------------------
// AUTH GUARD — all other actions require a valid session
// ---------------------------------------------------------------
if (empty($_SESSION['user_email'])) {
http_response_code(401);
echo json_encode(['success' => false, 'message' => 'Unauthorised']);
exit;
}
$CURRENT_USER = $_SESSION['user_email'];
$dataFile = getUserDataFile($CURRENT_USER);
$logFile = 'activity.log';
// ---------------------------------------------------------------
// LOGOUT (server-side session destroy; client handles MSAL)
// ---------------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'logout') {
$_SESSION = [];
session_destroy();
echo json_encode(['success' => true]);
exit;
}
// Helper to log activity
function logActivity($message, $type = 'INFO') {
global $logFile, $CURRENT_USER;
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] [$type] [$CURRENT_USER] $message\n";
file_put_contents($logFile, $logEntry, FILE_APPEND);
}
// Helper to read data
function getData() {
global $dataFile;
if (!file_exists($dataFile)) return [];
$content = file_get_contents($dataFile);
return json_decode($content, true) ?? [];
}
// Helper to save data
function saveData($data) {
global $dataFile;
file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT));
}
// Helper to generate filename from a row
function generateFilename($item) {
$omgid = trim($item['OMGID'] ?? '');
$domain = trim($item['Domain'] ?? '');
$subteam = trim($item['Subteam'] ?? '');
$brand = trim($item['Brand'] ?? '');
$event = trim($item['Event'] ?? '');
$initiative = trim($item['Initiative'] ?? '');
$yy = trim($item['YY'] ?? '');
$seq = trim($item['Sequence'] ?? '');
$assetName = trim($item['AssetName'] ?? '');
$version = trim($item['Version'] ?? '');
if ($domain === 'EVNT') {
$parts = ['EVNT'];
if ($event) $parts[] = $event;
if ($yy) $parts[] = $yy;
if ($seq) $parts[] = $seq;
$middle = implode('-', $parts);
} else {
$parts = [];
if ($domain) $parts[] = $domain;
if ($subteam) $parts[] = $subteam;
if ($brand) $parts[] = $brand;
if ($initiative) $parts[] = $initiative;
if ($yy) $parts[] = $yy;
if ($seq) $parts[] = $seq;
$middle = implode('-', $parts);
}
$suffix = '';
if ($assetName) $suffix .= '_' . $assetName;
if ($version) $suffix .= '_v' . $version;
if (empty($omgid) && empty($middle)) return '';
return ($omgid ? $omgid . ' - ' : '') . $middle . $suffix;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if ($action === 'load') {
echo json_encode(getData());
exit;
}
// Sheet Management Endpoints
if ($action === 'list_sheets') {
$sheets = getUserSheets($CURRENT_USER);
echo json_encode(['success' => true, 'sheets' => $sheets]);
exit;
}
if ($action === 'load_sheet') {
$sheetId = $_GET['id'] ?? '';
$data = loadSheetData($CURRENT_USER, $sheetId);
if ($data !== null) {
echo json_encode(['success' => true, 'data' => $data]);
} else {
echo json_encode(['success' => false, 'message' => 'Sheet not found']);
}
exit;
}
// Load Campaign
if ($action === 'load_campaign') {
$name = $_GET['name'] ?? '';
$file = __DIR__ . '/campaigns/' . preg_replace('/[^a-z0-9_-]/i', '', $name) . '.json';
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
echo json_encode(['success' => true, 'data' => $data]);
} else {
echo json_encode(['success' => false, 'message' => 'Campaign not found']);
}
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if ($action === 'save') {
if (isset($input['data']) && is_array($input['data'])) {
saveData($input['data']);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Invalid data format']);
}
exit;
}
// Save Campaign
if ($action === 'save_campaign') {
$name = preg_replace('/[^a-z0-9_-]/i', '', $input['name'] ?? 'untitled');
$data = $input['data'] ?? [];
$file = __DIR__ . '/campaigns/' . $name . '.json';
if (!is_dir(__DIR__ . '/campaigns/')) {
mkdir(__DIR__ . '/campaigns/', 0755, true);
}
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
echo json_encode(['success' => true]);
exit;
}
// Sheet Management POST Endpoints
if ($action === 'save_sheet') {
$name = $input['name'] ?? '';
$data = getData();
$sheet = createSheet($CURRENT_USER, $name, $data);
echo json_encode(['success' => true, 'sheet' => $sheet]);
exit;
}
if ($action === 'duplicate_sheet') {
$sheetId = $input['id'] ?? '';
$sheet = duplicateSheet($CURRENT_USER, $sheetId);
if ($sheet) {
echo json_encode(['success' => true, 'sheet' => $sheet]);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to duplicate sheet']);
}
exit;
}
if ($action === 'delete_sheet') {
$sheetId = $input['id'] ?? '';
deleteSheet($CURRENT_USER, $sheetId);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'rename_sheet') {
$sheetId = $input['id'] ?? '';
$newName = $input['name'] ?? '';
$success = renameSheet($CURRENT_USER, $sheetId, $newName);
echo json_encode(['success' => $success]);
exit;
}
if ($action === 'update_sheet') {
$data = $input['data'] ?? [];
saveData($data);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'command') {
$data = getData() ?? [];
$command = trim($input['command']);
logActivity("Command received: $command", 'COMMAND');
$commandLower = strtolower($command);
// Pre-processing: Convert number words to digits
$numberMap = [
'one' => '1', 'two' => '2', 'three' => '3', 'four' => '4', 'five' => '5',
'six' => '6', 'seven' => '7', 'eight' => '8', 'nine' => '9', 'ten' => '10',
'eleven' => '11', 'twelve' => '12', 'twenty' => '20', 'thirty' => '30'
];
foreach ($numberMap as $word => $digit) {
$commandLower = preg_replace('/\b' . $word . '\b/', $digit, $commandLower);
}
if (isset($GEMINI_API_KEY) && $GEMINI_API_KEY !== 'YOUR_API_KEY_HERE') {
$currentDate = date('Y-m-d');
$currentYear2Digit = date('y');
$dataContext = json_encode($data);
$yoloMode = isset($input['yolo_mode']) && $input['yolo_mode'] ? 'TRUE' : 'FALSE';
$conversationHistory = isset($input['history']) ? $input['history'] : '';
$campaignName = isset($input['campaign_name']) ? trim($input['campaign_name']) : '';
$globalContext = '';
if (!empty($campaignName)) {
$globalContext .= "\nGLOBAL CAMPAIGN NAME: $campaignName";
}
$prompt = "
You are an intelligent assistant managing a JOB NAMING TOOL for Dow Jones.
Current Date: $currentDate
YOLO MODE: $yoloMode
$globalContext
CONVERSATION HISTORY:
$conversationHistory
CURRENT DATA (Context for your actions):
$dataContext
=== DOW JONES JOB NAMING CONVENTION ===
FORMAT: [OMGID] - [Domain]-[Subteam]-[Brand]-[Initiative]-[YY]-[Sequence]_[AssetName]_v[Version]
EVENT FORMAT: [OMGID] - EVNT-[EventAbbrev]-[YY]-[Sequence]_[AssetName]_v[Version]
EXAMPLES:
- 000000 - PMKT-ACQ-WSJ-BIC-26-01_MetaBanner_v1
- 000001 - PMKT-ENGRT-WSJ-BIC-26-01_Email_v1
- 000002 - EVNT-GFF-26-02_Agenda_v3
=== VALID VALUES ===
DOMAIN options: PMKT (Performance Marketing), BRND (Brand), EVNT (Event), B2B
SUBTEAM options (used when Domain is NOT EVNT):
- ACQ (Acquisition), B2B, CMKT (Content Marketing), ENGRT (Engagement/Retention), ENT (Enterprise)
BRAND options (used when Domain is NOT EVNT):
- WSJ (Wall Street Journal), WSJ+ (Wall Street Journal+), BAR (Barron's), MW (MarketWatch)
- DF (Dragonfly), DJE (Dow Jones Energy), FAC (Factiva), FE (Free Expression)
- GRI (Global Risk Insights), NWS (Newswires), OA (Oxford Analytica), RSK (Risk), RSKC (Risk Center)
- DJRJ (Risk Journal), R&C (Risk & Compliance), WECR (World ECR)
EVENT options (used ONLY when Domain is EVNT, replaces Subteam+Brand):
- GH (Global Horizons), DJRJS (Risk Journal Summit), WECR (World ECR)
- FEOE (Free Expressions Opinion Event), FOH (Future of Health), GFF (Global Food Forum)
- JH (Journal House), TL (TechLive), TLQ (TechLive Qatar), TLCYB (TechLive Cyber), FOE (The Future of Everything)
- WSJIL (WSJ Invest Live), BODC (Board of Directors Council)
- CCOC (CCO Council), CEOC (CEO Council), CFOC (CFO Council), CMOC (CMO Council)
- CPOC (CPO Council), TECC (Technology Council), WSJLI (WSJ Leadership Institute)
OMGID: 6-digit numeric string (e.g., 000000, 000001)
YY: 2-digit year (e.g., 26 for 2026). Default to current year: $currentYear2Digit
Sequence: 2-digit sequence number (e.g., 01, 02, 03)
Initiative: Free text abbreviation (e.g., BIC, DEMO, LAUNCH)
AssetName: Descriptive name with no spaces (e.g., MetaBanner, Email, Agenda, LinkedInAd)
Version: Integer version number (e.g., 1, 2, 3)
=== DATA SCHEMA ===
Each item has these fields:
- OMGID (String, 6 digits with leading zeros)
- Domain (String, one of: PMKT, BRND, EVNT, B2B)
- Subteam (String, one of: ACQ, B2B, CMKT, ENGRT, ENT) - leave empty for EVNT domain
- Brand (String, one of the brand abbreviations above) - leave empty for EVNT domain
- Event (String, one of the event abbreviations above) - ONLY used for EVNT domain, leave empty otherwise
- Initiative (String, free text)
- YY (String, 2 digits)
- Sequence (String, 2 digits with leading zero)
- AssetName (String, no spaces, PascalCase)
- Version (String, integer)
- Filename (Auto-generated, READ-ONLY - DO NOT create this field)
=== SUPPORTED OPERATIONS ===
1. 'create': Create new job name entries
Output: { \"operation\": \"create\", \"items\": [ { \"OMGID\": \"000000\", \"Domain\": \"PMKT\", \"Subteam\": \"ACQ\", \"Brand\": \"WSJ\", \"Event\": \"\", \"Initiative\": \"BIC\", \"YY\": \"26\", \"Sequence\": \"01\", \"AssetName\": \"MetaBanner\", \"Version\": \"1\" } ] }
2. 'update': Update existing entries by OMGID
Output: { \"operation\": \"update\", \"target_ids\": [\"000000\"], \"values\": { \"Version\": \"2\" } }
3. 'batch_update': Update multiple items with DIFFERENT values
Output: { \"operation\": \"batch_update\", \"updates\": [ { \"OMGID\": \"000000\", \"values\": { \"AssetName\": \"LinkedInAd\" } } ] }
4. 'question': Ask for clarification (ONLY if YOLO MODE is FALSE)
Output: { \"operation\": \"question\", \"text\": \"What domain is this for?\" }
=== CRITICAL RULES ===
1. **MULTIPLE ITEMS**: When user says 'create 5 jobs', create 5 separate items in the array. Auto-increment Sequence (01, 02, 03...) and OMGID for each.
2. **EVENT DOMAIN**: When Domain is EVNT, do NOT populate Subteam or Brand. Use the Event field instead. The user might say the full event name - map it to the abbreviation.
3. **NON-EVENT DOMAINS**: When Domain is PMKT, BRND, or B2B, populate Subteam and Brand. Leave Event empty.
4. **OMGID AUTO-INCREMENT**: If user doesn't specify OMGID, look at existing data and auto-increment from the highest existing OMGID. If no data exists, start at 000000.
5. **SEQUENCE AUTO-INCREMENT**: Within a batch of items sharing the same Domain+Subteam+Brand+Initiative+YY (or Domain+Event+YY for events), auto-increment sequence starting from 01.
6. **ASSET NAME**: Should be PascalCase with no spaces (e.g., MetaBanner, LinkedInAd, EmailHeader). If user gives a name with spaces, convert it.
7. **NAME RESOLUTION**: When user says full names, map to abbreviations:
- 'Performance Marketing' -> PMKT, 'Acquisition' -> ACQ, 'Wall Street Journal' -> WSJ
- 'Global Food Forum' -> GFF, 'Tech Live' -> TECL, etc.
8. **YOLO MODE**: If TRUE, NEVER ask questions. Guess missing info:
- Missing Domain -> PMKT
- Missing Subteam -> ACQ
- Missing Brand -> WSJ
- Missing Initiative -> GEN
- Missing YY -> $currentYear2Digit
- Missing AssetName -> Asset
- Missing Version -> 1
9. **CLARIFICATION RECOVERY**: User's input may be answering a previous question. Combine with conversation history and EXECUTE.
10. **CONTEXT**: Use CURRENT DATA to resolve references like 'change all version numbers to 2' or 'update the GFF events'.
CRITICAL: Respond with ONLY valid JSON. No explanations, no markdown, no conversational text.
Your response must be a single JSON object starting with { and ending with }.
User Command: \"$command\"
";
// Call Gemini API
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" . $GEMINI_API_KEY;
$dataPayload = [
"contents" => [
[
"parts" => [
["text" => $prompt]
]
]
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($dataPayload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo json_encode(['success' => false, 'message' => 'Curl error: ' . curl_error($ch)]);
exit;
}
curl_close($ch);
$responseObj = json_decode($response, true);
if (isset($responseObj['error'])) {
$errorMsg = $responseObj['error']['message'] ?? 'Unknown Error';
logActivity("Gemini API Error: $errorMsg", 'ERROR');
echo json_encode([
'success' => false,
'message' => "Gemini API Error: " . $errorMsg,
'debug_raw' => $response
]);
exit;
}
$llmText = $responseObj['candidates'][0]['content']['parts'][0]['text'] ?? '';
if (empty($llmText)) {
echo json_encode([
'success' => false,
'message' => "AI returned an empty response.",
'debug_raw' => $response
]);
exit;
}
// Robust JSON Extraction
$start = strpos($llmText, '{');
$end = strrpos($llmText, '}');
if ($start !== false && $end !== false) {
$responseText = substr($llmText, $start, $end - $start + 1);
} else {
$responseText = $llmText;
}
$llmAction = json_decode($responseText, true);
$debugInfo = [
'debug_llm' => $llmText,
'debug_extracted' => $responseText,
'debug_json_error' => json_last_error_msg()
];
if (json_last_error() !== JSON_ERROR_NONE || !$llmAction) {
echo json_encode(array_merge([
'success' => false,
'message' => "Invalid JSON from AI",
], $debugInfo));
exit;
}
// Execute Action
if ($llmAction['operation'] === 'create') {
$newItems = $llmAction['items'];
$count = 0;
foreach ($newItems as $item) {
if (isset($item['AssetName'])) {
$item['AssetName'] = str_replace(' ', '', $item['AssetName']);
}
$item['Filename'] = generateFilename($item);
$data[] = $item;
$count++;
}
saveData($data);
logActivity("Created $count job names via AI", 'SUCCESS');
echo json_encode(array_merge(['success' => true, 'message' => "Created $count job names.", 'count' => $count], $debugInfo));
} elseif ($llmAction['operation'] === 'update') {
$updates = $llmAction['values'];
$targetIds = $llmAction['target_ids'] ?? [];
$count = 0;
foreach ($data as &$row) {
$match = false;
if (!empty($targetIds)) {
if (in_array($row['OMGID'] ?? '', $targetIds)) {
$match = true;
}
} else {
$match = true;
}
if ($match) {
foreach ($updates as $key => $val) {
$row[$key] = $val;
}
$row['Filename'] = generateFilename($row);
$count++;
}
}
saveData($data);
logActivity("Updated $count items via AI", 'SUCCESS');
echo json_encode(array_merge(['success' => true, 'message' => "Updated $count items.", 'count' => $count], $debugInfo));
} elseif ($llmAction['operation'] === 'batch_update') {
$updates = $llmAction['updates'];
$count = 0;
foreach ($updates as $update) {
$id = $update['OMGID'];
$values = $update['values'];
foreach ($data as &$row) {
if (($row['OMGID'] ?? '') === $id) {
foreach ($values as $key => $val) {
$row[$key] = $val;
}
$row['Filename'] = generateFilename($row);
$count++;
break;
}
}
}
saveData($data);
logActivity("Batch updated $count items via AI", 'SUCCESS');
echo json_encode(array_merge(['success' => true, 'message' => "Batch updated $count items.", 'count' => $count], $debugInfo));
} elseif ($llmAction['operation'] === 'question') {
logActivity("AI asked question: " . $llmAction['text'], 'QUESTION');
echo json_encode(array_merge(['success' => true, 'question' => $llmAction['text']], $debugInfo));
} else {
echo json_encode(array_merge(['success' => false, 'message' => 'Unknown operation: ' . $llmAction['operation']], $debugInfo));
}
exit;
}
}
}
?>