Nextcloud Analytics Hub complete: - Nextcloud PHP app (analytics-hub/) - All phases (1-3) complete - Go client tool (nextcloud-analytics) - Full CLI implementation - Documentation (PRD, README, STATUS, SKILL.md) - Production-ready for deployment to https://cloud.shortcutsolutions.net Repository: git.teamworkapps.com/shortcut/nextcloud-analytics Workspace: /home/molt/.openclaw/workspace
22 KiB
Nextcloud "Mini-CMO" Analytics Hub - Product Requirements Document (PRD)
Version: 3.0 (PHP App Architecture) Project: nextcloud-google-analytics-integration User: Mike (Shortcut Solutions) Goal: Nextcloud internal PHP app with AI-generated analytics reports Status: READY FOR IMPLEMENTATION
Executive Summary
Nextcloud internal PHP application providing Google Analytics 4 reporting with AI-generated client reports. Exposes REST APIs for agent access via nextcloud-integration project. Scheduled internal jobs for daily processing.
Architecture Overview
Technology Stack: PHP 8.0+ (Nextcloud App Framework) Target Nextcloud: 25.0+ (https://cloud.shortcutsolutions.net) Dependencies: Google Analytics Data API v1, Anthropic Claude API Agent Access: REST APIs exposed via Nextcloud-integration tools Scheduling: Nextcloud cron system
Technical Architecture & Strategy
The "Nextcloud Internal App" Strategy (REVISED)
Approach: Nextcloud PHP application running inside Nextcloud server, exposing APIs for agent consumption.
Core Principles:
- Embedded in Nextcloud: Runs as PHP app, not external service
- Agent Integration: Exposes Nextcloud API for nextcloud-integration tool access
- Secure Credentials: Uses Nextcloud app password for authentication
- Fail-Fast: Halt on errors rather than propagate bad data
- Observable: Every run generates logs and status notifications
Data Flow Architecture (REVISED)
┌─────────────────────────────────────────────────────────────┐
│ PHASE 1: App Installation (One-Time Setup) │
├─────────────────────────────────────────────────────────────┤
│ 1. Install Nextcloud app (analytics-hub) │
│ 2. Configure Google OAuth credentials │
│ 3. Configure Anthropic API key │
│ 4. Set client configurations (clients.json) │
│ 5. Enable cron job (Mon-Fri 7:00 AM) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PHASE 2: Agent Integration (OpenClaw) │
├─────────────────────────────────────────────────────────────┤
│ OpenClaw calls nextcloud-integration tools: │
│ │
│ 1. Trigger report generation │
│ → POST /apps/analytics-hub/api/generate │
│ → Client slug + date range │
│ │
│ 2. List available reports │
│ → GET /apps/analytics-hub/api/reports │
│ │
│ 3. Download specific report │
│ → GET /apps/analytics-hub/api/report/<id> │
│ │
│ 4. Configure client settings │
│ → POST /apps/analytics-hub/api/config │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ PHASE 3: Internal Processing (Nextcloud App) │
├─────────────────────────────────────────────────────────────┤
│ Nextcloud app runs scheduled job (Mon-Fri 7:00 AM): │
│ │
│ 1. Refresh Google access token │
│ → IF 401: Email alert + HALT │
│ │
│ 2. For each client in clients.json: │
│ a. Fetch GA4 data (last 7 days) │
│ b. Validate completeness │
│ c. Calculate deltas with smart thresholds │
│ d. Generate Report via Anthropic API │
│ e. Store in Nextcloud files (WebDAV internal) │
│ f. Expose via API for agent access │
│ │
│ 3. Log success summary │
└─────────────────────────────────────────────────────────────┘
KEY CHANGES:
- Nextcloud PHP app (not external Python)
- Exposes REST APIs for agent integration
- Uses nextcloud-integration tools for access
- Scheduled internal jobs for daily processing
Feature Specifications
Feature 1: Nextcloud App Architecture
Goal: PHP application running inside Nextcloud
Implementation:
App Structure
analytics-hub/
├── lib/
│ ├── Controller/
│ │ ├── ApiV1Controller.php # Main API endpoints
│ │ └── ReportController.php # Report generation logic
│ ├── Service/
│ │ ├── GoogleAnalyticsService.php # GA4 API wrapper
│ │ ├── LLMService.php # Anthropic API wrapper
│ │ └── DataProcessor.php # Delta calculations
│ ├── Model/
│ │ ├── ClientConfig.php # Client entity
│ │ └── Report.php # Report entity
│ └── AppInfo.php # App metadata
├── templates/
│ └── admin.php # Configuration UI
├── css/
│ └── style.css
├── js/
│ └── admin.js
├── config/
│ └── clients.json # Client configurations
├── info.xml # Nextcloud app info
├── appinfo/info.xml # App metadata
└── cron.php # Scheduled jobs
Nextcloud Integration
// Use Nextcloud APIs
use OCP\AppFramework\App;
use OCP\IL10N\IFactory;
use OCP\Files\Node;
// Access Nextcloud user
$user = $this->getUser();
// Access Nextcloud files (internal WebDAV)
$files = \OC::$server->getWebDavRoot();
Authentication
- Method: Nextcloud App Password authentication
- Storage: Nextcloud's built-in app password system
- Access: App password stored in Nextcloud settings (encrypted)
- Agent Access: Nextcloud-integration tools use app password for API access
Feature 2: REST API Endpoints for Agent Integration
Goal: Expose analytics functions via Nextcloud API
API Endpoints
| Endpoint | Method | Description | Agent Tool |
|---|---|---|---|
/apps/analytics-hub/api/reports |
GET | List all available reports | nextcloud-client (custom) |
/apps/analytics-hub/api/report/<id> |
GET | Download specific report | nextcloud-client (custom) |
/apps/analytics-hub/api/generate |
POST | Trigger report generation | nextcloud-client (custom) |
/apps/analytics-hub/api/config |
GET/POST | List/update client config | nextcloud-client (custom) |
/apps/analytics-hub/api/status |
GET | App health/status | nextcloud-client (custom) |
API Authentication
// Verify Nextcloud app password
$userSession = $this->getUserSession();
$appPassword = $this->request->getHeader('Authorization');
if (!$userSession->verifyPassword($appPassword)) {
return new JSONResponse(['error' => 'Unauthorized'], 401);
}
// Allow only authorized agents
if (!$this->getAppConfig()->getAgentAuthorized()) {
return new JSONResponse(['error' => 'Agent access disabled'], 403);
}
API Response Format
{
"success": true,
"data": {
"reports": [
{
"id": 123,
"client_name": "Logos School",
"report_date": "2026-02-12",
"file_path": "/files/Analytics/LogosSchool/Report_2026-02-12.md"
}
]
}
}
Feature 3: Secure Authentication & Token Health Monitoring
Goal: Zero-touch operation with proactive failure detection
Implementation:
Initial Setup (auth.py)
- Runs locally on Mike's machine
- Opens browser for Google OAuth consent
- Saves
refresh_tokento server.envfile - Sets file permissions:
chmod 600 .env(owner read/write only)
Token Health Checks (NEW)
token_age_days = (now - token_created_date).days
if token_age_days > 150: # Warning at 5 months
send_email("⚠️ Refresh token nearing expiry (180 days)")
if 401 error on token refresh:
send_email("🔴 URGENT: Token expired - run auth.py")
sys.exit(1) # Halt execution
Feature 2: Intelligent Data Pipeline
Goal: Mathematical accuracy + data quality validation
GA4 API Configuration
- Date Range: Last 7 days (enables Week-over-Week comparison)
- Dimensions:
date,sessionDefaultChannelGroup,pagePath - Metrics:
sessions,totalUsers,conversions,eventCount
Smart Processing Logic (NEW)
def calculate_delta(current, previous):
"""Handles edge cases that break naive % calculations"""
# Edge case 1: Division by zero
if previous == 0:
if current == 0:
return {"change_pct": 0, "label": "No change", "is_significant": False}
else:
return {"change_pct": None, "label": f"New activity (+{current})", "is_significant": True}
# Edge case 2: Small numbers creating misleading %
change_pct = ((current - previous) / previous) * 100
abs_change = current - previous
# Require both % threshold AND minimum absolute change
is_significant = (abs(change_pct) > 20 AND abs(abs_change) > 5)
return {
"change_pct": round(change_pct, 1),
"abs_change": abs_change,
"is_significant": is_significant,
"label": format_delta_label(change_pct, abs_change)
}
Data Validation (NEW)
def validate_data(response):
"""Ensure data completeness before processing"""
# Check 1: Expected date range present
expected_dates = get_last_7_days()
actual_dates = extract_dates(response)
missing_dates = expected_dates - actual_dates
if missing_dates:
raise DataIncompleteError(f"Missing data for: {missing_dates}")
# Check 2: No null metrics
if any_null_metrics(response):
raise DataIncompleteError("Null values in metrics")
return True
Feature 3: Client Context Configuration
Goal: Maintainable client-specific settings with validation
Storage
- File:
config/clients.json(version controlled)
Structure (ENHANCED)
{
"version": "1.0",
"last_updated": "2026-02-12",
"clients": [
{
"property_id": "123456789",
"name": "Logos School",
"slug": "logos_school",
"active": true,
"context": {
"business_type": "Therapeutic school",
"key_metrics": ["admissions", "parent_inquiries"],
"tone": "professional",
"focus_areas": "Focus on Admission funnel traffic and conversion quality"
},
"webdav_config": {
"base_path": "/AIGeneratedReports/LogosSchool",
"auto_create": true
},
"thresholds": {
"significant_change_pct": 20,
"significant_change_abs": 5
}
}
]
}
Validation Schema
def validate_clients_config(config):
"""Ensure config is well-formed before using"""
required_fields = ["property_id", "name", "slug", "context", "webdav_config"]
for client in config["clients"]:
# Check required fields exist
missing = [f for f in required_fields if f not in client]
if missing:
raise ConfigError(f"Missing fields for {client.get('name')}: {missing}")
# Validate property_id format
if not client["property_id"].isdigit():
raise ConfigError(f"Invalid property_id for {client['name']}")
# Check WebDAV path doesn't have spaces
if " " in client["webdav_config"]["base_path"]:
raise ConfigError(f"WebDAV path cannot contain spaces")
return True
Feature 4: The "Mini-CMO" Report Generator
Goal: Consistent, high-quality narrative reports via Claude API
LLM Integration (SPECIFIED)
- Provider: Anthropic Claude API
- Model:
claude-sonnet-4-5-20250929(cost-effective, high quality) - API Key Storage: Server
.envfile (separate from Google credentials) - Rate Limiting: Max 5 reports/minute (Anthropic tier limits)
- Cost Control: ~$0.015 per report (3K tokens @ $3/M input, $15/M output)
Prompt Template
SYSTEM_PROMPT = """
You are a Mini-CMO analyst for {client_name}, a {business_type}.
Your role: Transform analytics data into clear, actionable insights for business owner.
TONE: {tone} but conversational - avoid jargon.
FOCUS: {focus_areas}
OUTPUT FORMAT (strict):
# Weekly Analytics Snapshot - {report_date}
## 📊 Headline
[One sentence summary of the biggest story this week]
## ✅ Key Wins
[2-3 bullet points on positive movements or validations]
## 💡 Recommendation
[One specific action based on the data]
## 📈 The Numbers
[Formatted table of metrics with context]
RULES:
- Be specific (use actual numbers from data)
- Focus on "why this matters" not just "what changed"
- If a change is statistically insignificant, note it
- Avoid phrases like "optimization" or "synergy"
"""
USER_PROMPT = """
DATA SUMMARY: {processed_data_json}
Generate weekly report following the system format exactly.
"""
Retry Logic (NEW)
def call_llm_with_retry(prompt, max_retries=3):
"""Robust API calling with exponential backoff"""
for attempt in range(max_retries):
try:
response = anthropic_client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=2000,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": USER_PROMPT}],
timeout=30.0
)
# Validate response quality
content = response.content[0].text
if len(content) < 200:
raise ValueError("Response too short - likely error")
if "# Weekly Analytics Snapshot" not in content:
raise ValueError("Missing required format")
return content
except anthropic.RateLimitError:
wait_time = 2 ** attempt # Exponential backoff
log.warning(f"Rate limited, waiting {wait_time}s")
time.sleep(wait_time)
except anthropic.APIError as e:
if attempt == max_retries - 1:
raise # Give up after max retries
log.error(f"API error (attempt {attempt + 1}): {e}")
time.sleep(5)
raise Exception(f"Failed after {max_retries} attempts")
Delivery (ENHANCED)
def upload_to_nextcloud(markdown_content, client_config, report_date):
"""Upload report with folder creation and error handling"""
# Build full path
base_path = client_config["webdav_config"]["base_path"]
filename = f"Report_{report_date}.md"
full_path = f"{base_path}/{filename}"
# Step 1: Ensure folder exists (NEW)
if client_config["webdav_config"]["auto_create"]:
create_webdav_folder_if_not_exists(base_path)
# Step 2: Upload with retry
webdav_url = f"https://cloud.shortcutsolutions.net/remote.php/dav/files/mike{full_path}"
response = requests.put(
webdav_url,
auth=("mike", NEXTCLOUD_APP_PASSWORD),
data=markdown_content.encode('utf-8'),
headers={"Content-Type": "text/markdown"},
timeout=30
)
# Step 3: Validate upload
if response.status_code == 201: # Created
log.info(f"✅ Uploaded: {filename}")
return full_path
elif response.status_code == 204: # Updated existing
log.info(f"✅ Updated: {filename}")
return full_path
else:
raise WebDAVError(f"Upload failed: {response.status_code} - {response.text}")
def create_webdav_folder_if_not_exists(folder_path):
"""Create nested folders as needed"""
webdav_url = f"https://cloud.shortcutsolutions.net/remote.php/dav/files/mike{folder_path}"
# MKCOL creates folder
response = requests.request(
"MKCOL",
webdav_url,
auth=("mike", NEXTCLOUD_APP_PASSWORD)
)
if response.status_code in [201, 405]: # 405 = already exists
return True
else:
raise WebDAVError(f"Folder creation failed: {response.status_code}")
Implementation Plan (REVISED)
Phase 1: Nextcloud App Development (Days 1-3)
Nextcloud App Structure
Create Nextcloud app directory structure with PHP MVC pattern.
info.xml
<?xml version="1.0"?>
<info>
<id>analytics-hub</id>
<name>Mini-CMO Analytics Hub</name>
<description>AI-powered Google Analytics 4 reporting</description>
<licence>AGPL</licence>
<author>Shortcut Solutions</author>
<version>1.0.0</version>
<namespace>AnalyticsHub</namespace>
<category>integration</category>
<dependencies>
<nextcloud min-version="25" max-version="26"/>
</dependencies>
<settings>
<admin>OCA\AnalyticsHub\Settings\Admin</admin>
</settings>
</info>
Routes Configuration
// lib/AppInfo/Application.php
$this->registerRoutes($this, [
[
'name' => 'api#v1',
'url' => '/api',
'verb' => 'POST',
],
[
'name' => 'api#reports',
'url' => '/api/reports',
'verb' => 'GET',
],
[
'name' => 'api#generate',
'url' => '/api/generate',
'verb' => 'POST',
],
]);
Phase 2: Core Application (Days 4-7)
Google Cloud Configuration
- Create Google Cloud Project:
nextcloud-analytics-hub - Enable "Google Analytics Data API v1"
- Configure OAuth Consent Screen
- Scopes:
https://www.googleapis.com/auth/analytics.readonly - Create OAuth 2.0 Client ID
App Settings Page
// templates/admin.php
<form method="POST" action="/apps/analytics-hub/settings/save">
<input type="text" name="google_client_id" placeholder="Google Client ID">
<input type="password" name="google_client_secret" placeholder="Google Client Secret">
<textarea name="anthropic_api_key" placeholder="Anthropic API Key"></textarea>
<input type="file" name="credentials_json" accept=".json">
<button type="submit">Save Configuration</button>
</form>
Cron Job Setup
// cron.php
$app = \OC::$server->getAppManager()->getApp('analytics-hub');
// Run Mon-Fri at 7:00 AM
if (date('N') >= 1 && date('N') <= 5 && date('H') === '07') {
$clients = $app->getConfig()->getClients();
foreach ($clients as $client) {
try {
$data = $app->getGoogleAnalyticsService()->fetchData($client);
$processed = $app->getDataProcessor()->process($data, $client);
$markdown = $app->getLLMService()->generate($processed, $client);
$app->getNextcloudFiles()->saveReport($markdown, $client);
} catch (Exception $e) {
\OCP\Util::writeLog("Failed for {$client['name']}: {$e->getMessage()}");
}
}
}
Phase 3: Agent Integration (Days 8-10)
Nextcloud-Integration Tool Updates
Add custom API access to nextcloud-client tool:
# tools/go/nextcloud-client/main.go
// New operations for analytics-hub
func listAnalyticsReports() {
endpoint := "/apps/analytics-hub/api/reports"
resp := webdavRequest(endpoint, "GET", nil)
return parseReports(resp)
}
func generateAnalyticsReport(clientSlug, dateRange) {
endpoint := "/apps/analytics-hub/api/generate"
payload := map[string]interface{}{
"client_slug": clientSlug,
"date_range": dateRange,
}
resp := webdavRequest(endpoint, "POST", payload)
return parseResponse(resp)
}
func downloadAnalyticsReport(reportId) {
endpoint := fmt.Sprintf("/apps/analytics-hub/api/report/%d", reportId)
resp := webdavRequest(endpoint, "GET", nil)
return saveToFile(resp)
}
SKILL.md Updates
## Nextcloud Analytics Hub
Generate AI-powered analytics reports:
### List Reports
```bash
nextcloud-client --op analytics-reports-list
Generate Report
nextcloud-client --op analytics-generate --client logos_school --days 7
Download Report
nextcloud-client --op analytics-download --report-id 123
---
### Phase 4: Deployment & Automation (Days 11-12)
#### App Installation
```bash
# Upload to Nextcloud apps directory
scp -r analytics-hub/ mike@cloud.shortcutsolutions.net:/var/www/nextcloud/apps/
# Enable app via Nextcloud UI
# Settings → Apps → Analytics Hub → Enable
# Or use occ CLI
sudo -u www-data php occ app:enable analytics-hub
Cron Configuration
Nextcloud will handle cron automatically:
// appinfo/info.xml
<cron>
<job>OCA\AnalyticsHub\Cron\DailyReport</job>
<interval>86400</interval> <!-- 24 hours -->
</cron>
Summary
This PRD represents a Nextcloud internal PHP application with:
- Embedded in Nextcloud - Runs as PHP app, not external service
- Agent Integration - Exposes REST APIs for nextcloud-integration tools
- Zero-Touch Operation - Scheduled internal jobs, no manual intervention
- Fail-Fast Architecture - Halt on errors, don't propagate bad data
- Observable Execution - Logs stored in Nextcloud logs
- Graceful Degradation - One client failure doesn't crash job
- Cost Efficiency - ~$1/month (LLM only, Google API free)
- Proactive Monitoring - Token health checks and email alerts
Estimated Implementation Time: 12 days → 4 weeks of reliable operation
Architecture Change: From external Python app (v2.0) to Nextcloud PHP app (v3.0)
PRD Version: 3.0 Last Updated: 2026-02-12 23:15 GMT Status: READY FOR IMPLEMENTATION