diff --git a/FEATURE_SHUTDOWN_EARLY_WARNING.md b/FEATURE_SHUTDOWN_EARLY_WARNING.md
new file mode 100644
index 0000000..38231f3
--- /dev/null
+++ b/FEATURE_SHUTDOWN_EARLY_WARNING.md
@@ -0,0 +1,346 @@
+# Internet Shutdown Early Warning System
+
+## Overview
+
+The Internet Shutdown Early Warning System is an advanced anomaly detection feature that monitors user signup patterns to identify potential internet shutdowns or spam attacks. This feature is critical because **RelaySMS is a preparedness tool** - people sign up when they anticipate connectivity issues.
+
+## Key Concept
+
+Unlike traditional analytics where increased signups indicate growth, in RelaySMS:
+- **Sudden signup spikes** = Potential impending internet shutdown
+- **High retention after spike** = Legitimate shutdown preparation
+- **Low retention after spike** = Possible spam/bot attack
+
+## Features
+
+### 1. Signup Spike Detection
+- Monitors signups by country and date
+- Compares current period to 7-day baseline
+- Triggers alerts when signups increase by **200%+ or 100%+ with 50+ new signups**
+- Considers both percentage change and absolute numbers
+
+### 2. Retention Validation
+Distinguishes between real concerns and spam by analyzing user retention:
+
+```
+High Retention (≥50%) = Legitimate users preparing for shutdown
+Low Retention (<50%) = Possible spam/bot attack
+```
+
+### 3. Alert Levels
+
+#### Critical (Red) - Spam Detection
+- **Criteria**: Signup spike + retention rate <50%
+- **Meaning**: Possible spam or bot attack
+- **Action**: Investigate accounts, increase signup verification
+- **Confidence**: ~75%
+
+#### High Risk (Orange) - Likely Shutdown
+- **Criteria**: Signup spike + retention rate ≥70%
+- **Meaning**: Legitimate shutdown preparation
+- **Action**: Monitor closely, prepare infrastructure
+- **Confidence**: 80-95%
+
+#### Medium Risk (Blue) - Monitor
+- **Criteria**: Signup spike + retention rate 50-69%
+- **Meaning**: Moderate activity, unclear intent
+- **Action**: Watch for pattern confirmation
+- **Confidence**: 50-80%
+
+#### Low Risk (Green) - Minor Spike
+- **Criteria**: Signup spike + retention rate 0-49% but above spam threshold
+- **Meaning**: Possible organic growth or minor concern
+- **Action**: Continue monitoring
+- **Confidence**: 30-50%
+
+### 4. Summary Metrics Dashboard
+
+Four key metric cards show at a glance:
+- **Critical Alerts**: Possible spam attacks requiring immediate investigation
+- **High Risk Alerts**: Likely shutdown preparation events
+- **Medium Risk Alerts**: Situations requiring close monitoring
+- **Total Countries Monitored**: Countries with active signup data
+
+### 5. Two View Modes
+
+#### Alert Dashboard View
+- Shows all active alerts with detailed information
+- Color-coded severity levels
+- Key metrics for each country:
+ - Current signup count
+ - Baseline (7-day average)
+ - Percentage change
+ - Retention rate
+ - Confidence score
+
+#### Timeline View
+- Visual line charts for top 10 countries
+- Shows signup trends over the selected period
+- Helps identify patterns and trends
+- Useful for comparing historical data
+
+## How It Works
+
+### Detection Algorithm
+
+```javascript
+1. Fetch baseline data (7 days before current period)
+2. Fetch current period data
+3. Fetch retention data
+4. For each country:
+ a. Calculate current total signups
+ b. Calculate baseline average
+ c. Calculate percentage change
+ d. If spike detected (>200% or >100% with 50+ new signups):
+ - Calculate retention rate
+ - Classify alert level based on retention
+ - Assign confidence score
+ - Generate alert message
+5. Sort alerts by severity and percentage change
+6. Display in dashboard
+```
+
+### Retention Rate Calculation
+
+```javascript
+Retention Rate = (Retained Users / Total Signups) × 100
+
+Example:
+- Country X had 300 signups in current period
+- 210 of those users were retained (active)
+- Retention Rate = (210 / 300) × 100 = 70%
+- Classification: High Risk (likely shutdown prep)
+```
+
+### Confidence Score
+
+The confidence score is calculated based on:
+- Retention rate percentage
+- Magnitude of signup spike
+- Historical patterns
+
+```javascript
+For High Retention (≥50%):
+ confidence = 50 + (retention_rate / 2)
+ Range: 75-95%
+
+For Low Retention (<50%):
+ confidence = 30 + retention_rate
+ Range: 30-80%
+
+For Spam Detection:
+ confidence = fixed 75%
+```
+
+## Use Cases
+
+### 1. Shutdown Preparation
+**Scenario**: Cameroon shows 340% signup spike with 72% retention
+
+**Interpretation**:
+- High confidence this is legitimate shutdown preparation
+- Users are actually using the service
+- Likely an impending or ongoing internet shutdown
+
+**Actions**:
+- Alert operations team to prepare infrastructure
+- Monitor publication volume shifts (internet → SMS)
+- Document the event for advocacy groups
+- Consider reaching out to affected users
+
+### 2. Spam Detection
+**Scenario**: Ethiopia shows 250% signup spike with 15% retention
+
+**Interpretation**:
+- Users signing up but not using the service
+- Likely bot attack or coordinated fake signups
+- Low confidence in legitimate use
+
+**Actions**:
+- Flag accounts for review
+- Check for patterns (same IP, device, timing)
+- Temporarily increase signup verification requirements
+- Clean up inactive accounts
+
+### 3. Regional Monitoring
+**Scenario**: Multiple countries in West Africa showing moderate spikes
+
+**Interpretation**:
+- Could indicate regional political instability
+- Users preparing for possible shutdowns
+- Pattern suggests real concerns
+
+**Actions**:
+- Monitor news and social media
+- Prepare for increased load
+- Coordinate with advocacy organizations
+- Document patterns for reports
+
+## Data Sources
+
+### API Endpoints Used
+
+1. **Signup Data**
+ - Endpoint: `/signup`
+ - Parameters: `category=signup`, `group_by=country`, `granularity=day`
+ - Used for: Baseline and current period comparison
+
+2. **Retention Data**
+ - Endpoint: `/retained`
+ - Parameters: `category=retained`, `group_by=country`, `granularity=day`
+ - Used for: Validation of user legitimacy
+
+### Date Ranges
+
+- **Baseline Period**: 7 days before the selected start date
+- **Current Period**: User-selected date range (default: last 30 days)
+- **Comparison**: Current period compared to baseline to detect anomalies
+
+## Integration
+
+### Dashboard Integration
+
+The component is added to the main dashboard page:
+
+```jsx
+import ShutdownEarlyWarning from 'sections/dashboard/default/ShutdownEarlyWarning';
+
+// In dashboard render
+
+
+
+```
+
+### Filter Support
+
+The component respects dashboard filters:
+- **Date Range**: Changes baseline and current periods
+- **Country Filter**: Focuses on specific country
+- Other filters don't apply to signup/retention data
+
+## Interpreting Results
+
+### Example Alert Breakdown
+
+```
+🇨🇲 Cameroon (CM) - HIGH RISK ALERT
+─────────────────────────────────────
+Message: Possible shutdown preparation - Users actively using service
+Confidence: 86%
+
+Metrics:
+├─ Current Signups: 1,230 ↑
+├─ Baseline (7-day): 285
+├─ Change: +331.6%
+└─ Retention: 72% ✓
+
+Interpretation:
+This is likely a legitimate response to anticipated internet restrictions.
+The high retention rate (72%) indicates users are genuinely concerned
+and actively using the service.
+```
+
+### Reading Timeline Charts
+
+- **Flat line**: Normal, stable activity
+- **Gradual increase**: Organic growth
+- **Sharp spike**: Anomaly requiring investigation
+- **Drop after spike**: May indicate shutdown ended or spam cleaned up
+
+## Best Practices
+
+### 1. Regular Monitoring
+- Check dashboard daily during high-risk periods
+- Set up alerts for High Risk and Critical levels
+- Review trends weekly to identify patterns
+
+### 2. Context Awareness
+- Cross-reference with news and social media
+- Consider regional political situations
+- Look for patterns across neighboring countries
+
+### 3. Response Planning
+- Document all detected events
+- Maintain contact list for advocacy groups
+- Have infrastructure scaling plan ready
+- Prepare user communication templates
+
+### 4. False Positive Management
+- Investigate all Critical alerts promptly
+- Verify High Risk alerts with external sources
+- Adjust thresholds based on historical data
+- Document false positives for algorithm improvement
+
+## Limitations
+
+1. **Requires Historical Data**: Needs at least 7 days of baseline data
+2. **Retention Lag**: Retention data may lag by days
+3. **Cannot Predict**: Only detects preparation, not actual shutdowns
+4. **Regional Variations**: Thresholds may need adjustment per region
+5. **External Factors**: Legitimate growth can trigger false positives
+
+## Future Enhancements
+
+### Planned Improvements
+1. Machine learning model trained on historical shutdown data
+2. Integration with external shutdown tracking APIs
+3. Automated notifications via email/webhook
+4. SMS vs. Internet publication routing analysis
+5. Historical shutdown archive
+6. Multi-factor confidence scoring
+7. Real-time monitoring mode
+8. Export reports for advocacy use
+
+### Suggested Thresholds Adjustment
+- Consider country-specific baselines
+- Adjust spike threshold based on country size
+- Weight recent history more than older data
+- Add time-of-day patterns
+
+## Technical Details
+
+### Component Location
+`src/sections/dashboard/default/ShutdownEarlyWarning.jsx`
+
+### Key Functions
+
+1. **fetchSignupData**: Retrieves signup data from API
+2. **fetchRetainedData**: Retrieves retention data from API
+3. **calculateRetentionRate**: Computes retention percentage per country
+4. **detectAnomalies**: Main detection algorithm
+5. **processTimelineData**: Prepares data for visualization
+
+### Dependencies
+- Material-UI components
+- @mui/x-charts for LineChart
+- axios for API calls
+- dayjs for date manipulation
+
+### State Management
+- `alerts`: Array of detected anomalies
+- `countryTimelines`: Timeline data for visualization
+- `loading`: Loading state
+- `view`: Current view mode (alerts/timeline)
+
+## Support
+
+For questions or issues:
+1. Check documentation in the app (Help section)
+2. Review this technical guide
+3. Contact development team
+4. Submit issue on GitHub
+
+## Changelog
+
+### Version 1.0.0 (December 2025)
+- Initial release
+- Signup spike detection
+- Retention validation
+- Alert classification system
+- Dual view mode (alerts/timelines)
+- Summary metrics dashboard
+- Integration with dashboard filters
+
+---
+
+**Note**: This feature is critical for RelaySMS operations. Regular monitoring and prompt investigation of alerts can help identify both security threats (spam) and human rights concerns (shutdowns).
diff --git a/src/pages/dashboard/default.jsx b/src/pages/dashboard/default.jsx
index 68f6b8b..27ba77c 100644
--- a/src/pages/dashboard/default.jsx
+++ b/src/pages/dashboard/default.jsx
@@ -5,7 +5,7 @@ import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
// ant design
-import { DatePicker, Select, Button, Dropdown, Switch } from 'antd';
+import { DatePicker, Select, Button, Dropdown, Switch, Radio } from 'antd';
import 'antd/dist/reset.css';
// project imports
@@ -16,6 +16,7 @@ import ErrorDisplay from 'components/ErrorDisplay';
import CombinedChartCard from 'sections/dashboard/default/CombinedChartCard';
import UserTable from 'sections/dashboard/default/UserTable';
import UserRetentionMetrics from 'sections/dashboard/default/UserRetentionMetrics';
+import ShutdownEarlyWarning from 'sections/dashboard/default/ShutdownEarlyWarning';
import axios from 'axios';
import { useEffect, useState } from 'react';
@@ -66,12 +67,15 @@ export default function DashboardDefault() {
const [granularity, setGranularity] = useState('day');
const [groupBy, setGroupBy] = useState('date');
const [countryCode, setCountryCode] = useState(null);
+ const [type, setType] = useState(null);
+ const [origin, setOrigin] = useState(null);
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const [dateRangeFilter, setDateRangeFilter] = useState('custom');
const [showCustomDatePickers, setShowCustomDatePickers] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [loading, setLoading] = useState(true);
+ const [downloading, setDownloading] = useState(false);
const [error, setError] = useState(null);
const [filtersApplied, setFiltersApplied] = useState({});
const [availableCountries, setAvailableCountries] = useState([]);
@@ -242,7 +246,7 @@ export default function DashboardDefault() {
if (startDate && endDate) {
return `${startDate.format('YYYY-MM-DD')} - ${endDate.format('YYYY-MM-DD')}`;
}
- return '2021-01-10 - Today';
+ return '2020-01-10 - Today';
default:
return 'Date Range Filter';
}
@@ -252,24 +256,28 @@ export default function DashboardDefault() {
const today = new Date().toISOString().split('T')[0];
const appliedFilters = {
category,
- startDate: startDate ? startDate.format('YYYY-MM-DD') : '2021-01-10',
+ startDate: startDate ? startDate.format('YYYY-MM-DD') : '2020-01-10',
endDate: endDate ? endDate.format('YYYY-MM-DD') : today,
granularity,
// groupBy,
- countryCode: countryCode || undefined
+ countryCode: countryCode || undefined,
+ type: type || undefined,
+ origin: origin || undefined
};
setFiltersApplied(appliedFilters);
};
const handleResetFilters = () => {
- const resetStartDate = dayjs('2021-01-01');
+ const resetStartDate = dayjs('2020-01-01');
const resetEndDate = dayjs();
setCategory('all');
// setGranularity('month');
// setGroupBy('country');
setCountryCode(null);
+ setType(null);
+ setOrigin(null);
setStartDate(resetStartDate);
setEndDate(resetEndDate);
setDateRangeFilter('custom');
@@ -285,48 +293,152 @@ export default function DashboardDefault() {
};
const handleDownloadData = async () => {
+ setDownloading(true);
try {
const today = new Date().toISOString().split('T')[0];
- const appliedStart = filtersApplied.startDate || '2021-01-10';
+ const appliedStart = filtersApplied.startDate || '2020-01-10';
const appliedEnd = filtersApplied.endDate || today;
+ const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
+
+ const countryParam = filtersApplied.countryCode ? `&country_code=${filtersApplied.countryCode}` : '';
+ const typeParam = filtersApplied.type ? `&type=${filtersApplied.type}` : '';
+ const originParam = filtersApplied.origin ? `&origin=${filtersApplied.origin}` : '';
+ const currentCategory = filtersApplied.category || category;
+
+ const summaryUrl = `${baseUrl}summary?start_date=${appliedStart}&end_date=${appliedEnd}${countryParam}${typeParam}${originParam}`;
+ const summaryResponse = await axios.get(summaryUrl);
+ const summaryData = summaryResponse.data.summary;
+
+ const fetchAllPages = async (endpoint, category, groupBy = 'date') => {
+ const params = {
+ category,
+ start_date: appliedStart,
+ end_date: appliedEnd,
+ granularity: 'day',
+ group_by: groupBy,
+ page: 1,
+ page_size: 100
+ };
- let apiUrl = `${import.meta.env.VITE_APP_TELEMETRY_API}summary?start_date=${appliedStart}&end_date=${appliedEnd}`;
+ if (filtersApplied.countryCode) {
+ params.country_code = filtersApplied.countryCode;
+ }
+
+ if (filtersApplied.type) {
+ params.type = filtersApplied.type;
+ }
+
+ if (filtersApplied.origin) {
+ params.origin = filtersApplied.origin;
+ }
+
+ const firstResponse = await axios.get(`${baseUrl}${endpoint}`, { params });
+ const totalPages = firstResponse?.data?.[category]?.pagination?.total_pages || 1;
+ let allData = firstResponse?.data?.[category]?.data ?? [];
+
+ if (totalPages > 1) {
+ const pagePromises = [];
+ for (let page = 2; page <= totalPages; page++) {
+ pagePromises.push(axios.get(`${baseUrl}${endpoint}`, { params: { ...params, page } }));
+ }
+
+ const responses = await Promise.all(pagePromises);
+ responses.forEach((response) => {
+ const pageData = response?.data?.[category]?.data ?? [];
+ allData = [...allData, ...pageData];
+ });
+ }
+ return allData;
+ };
+
+ const signupByDate = await fetchAllPages('signup', 'signup', 'date');
+ const signupByCountry = await fetchAllPages('signup', 'signup', 'country');
+
+ const retainedByDate = await fetchAllPages('retained', 'retained', 'date');
+ const retainedByCountry = await fetchAllPages('retained', 'retained', 'country');
+
+ const pubParams = {
+ start_date: appliedStart,
+ end_date: appliedEnd,
+ page: 1,
+ page_size: 100
+ };
if (filtersApplied.countryCode) {
- apiUrl += `&country_code=${filtersApplied.countryCode}`;
+ pubParams.country_code = filtersApplied.countryCode;
+ }
+ if (filtersApplied.type) {
+ pubParams.type = filtersApplied.type;
}
+ if (filtersApplied.origin) {
+ pubParams.origin = filtersApplied.origin;
+ }
+
+ const firstPubResponse = await axios.get(`${baseUrl}publications`, { params: pubParams });
+ const totalPubPages = firstPubResponse?.data?.publications?.pagination?.total_pages || 1;
+ let allPublications = firstPubResponse?.data?.publications?.data ?? [];
- const response = await axios.get(apiUrl);
- const data = response.data.summary;
+ if (totalPubPages > 1) {
+ const pubPagePromises = [];
+ for (let page = 2; page <= totalPubPages; page++) {
+ pubPagePromises.push(axios.get(`${baseUrl}publications`, { params: { ...pubParams, page } }));
+ }
+
+ const pubResponses = await Promise.all(pubPagePromises);
+ pubResponses.forEach((response) => {
+ const pageData = response?.data?.publications?.data ?? [];
+ allPublications = [...allPublications, ...pageData];
+ });
+ }
const downloadData = {
metadata: {
exportDate: new Date().toISOString(),
filters: {
- category: filtersApplied.category || category,
+ category: currentCategory,
startDate: appliedStart,
endDate: appliedEnd,
- countryCode: filtersApplied.countryCode || countryCode || 'All Countries'
+ countryCode: filtersApplied.countryCode || 'All Countries',
+ type: filtersApplied.type || 'All Types',
+ origin: filtersApplied.origin || 'All Origins'
+ },
+ recordCounts: {
+ signupByDate: signupByDate.length,
+ signupByCountry: signupByCountry.length,
+ retainedByDate: retainedByDate.length,
+ retainedByCountry: retainedByCountry.length,
+ publications: allPublications.length
}
},
summary: {
signup: {
- total_signup_users: data.total_signup_users || 0,
- total_signups_from_bridges: data.total_signups_from_bridges || 0,
- signup_countries: data.signup_countries || [],
- total_signup_countries: data.total_signup_countries || 0
+ total_signup_users: summaryData.total_signup_users || 0,
+ total_signups_from_bridges: summaryData.total_signups_from_bridges || 0,
+ signup_countries: summaryData.signup_countries || [],
+ total_signup_countries: summaryData.total_signup_countries || 0
},
retained: {
- total_retained_users: data.total_retained_users || 0,
- total_retained_users_with_tokens: data.total_retained_users_with_tokens || 0,
- retained_countries: data.retained_countries || [],
- total_retained_countries: data.total_retained_countries || 0
+ total_retained_users: summaryData.total_retained_users || 0,
+ total_retained_users_with_tokens: summaryData.total_retained_users_with_tokens || 0,
+ retained_countries: summaryData.retained_countries || [],
+ total_retained_countries: summaryData.total_retained_countries || 0
},
publications: {
- total_publications: data.total_publications || 0
+ total_publications: summaryData.total_publications || 0
}
},
- rawData: data
+ detailedData: {
+ signup: {
+ byDate: signupByDate,
+ byCountry: signupByCountry
+ },
+ retained: {
+ byDate: retainedByDate,
+ byCountry: retainedByCountry
+ },
+ publications: allPublications
+ },
+ rawSummary: summaryData
};
const jsonString = JSON.stringify(downloadData, null, 2);
@@ -337,7 +449,7 @@ export default function DashboardDefault() {
link.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
- link.download = `telemetry-dashboard-data-${timestamp}.json`;
+ link.download = `telemetry-dashboard-complete-${timestamp}.json`;
document.body.appendChild(link);
link.click();
@@ -347,6 +459,8 @@ export default function DashboardDefault() {
} catch (error) {
console.error('Error downloading data:', error);
alert('Failed to download data. Please try again.');
+ } finally {
+ setDownloading(false);
}
};
@@ -355,7 +469,7 @@ export default function DashboardDefault() {
setLoading(true);
try {
const today = new Date().toISOString().split('T')[0];
- const appliedStart = filtersApplied.startDate || '2021-01-10';
+ const appliedStart = filtersApplied.startDate || '2020-01-10';
const appliedEnd = filtersApplied.endDate || today;
const startDateObj = dayjs(appliedStart);
@@ -373,6 +487,16 @@ export default function DashboardDefault() {
previousApiUrl += `&country_code=${filtersApplied.countryCode}`;
}
+ if (filtersApplied.type) {
+ currentApiUrl += `&type=${filtersApplied.type}`;
+ previousApiUrl += `&type=${filtersApplied.type}`;
+ }
+
+ if (filtersApplied.origin) {
+ currentApiUrl += `&origin=${filtersApplied.origin}`;
+ previousApiUrl += `&origin=${filtersApplied.origin}`;
+ }
+
const [currentResponse, previousResponse] = await Promise.all([axios.get(currentApiUrl), axios.get(previousApiUrl)]);
const data = currentResponse.data.summary;
@@ -666,6 +790,30 @@ export default function DashboardDefault() {
/>
+ {/* Type Filter */}
+
+
+ Type
+
+ setType(e.target.value)} style={{ width: '100%' }}>
+ All
+ Phone
+ Email
+
+
+
+ {/* Origin Filter */}
+
+
+ Origin
+
+ setOrigin(e.target.value)} style={{ width: '100%' }}>
+ All
+ Web
+ Bridge
+
+
+
{/* Buttons */}
@@ -679,8 +827,15 @@ export default function DashboardDefault() {
- } style={{ width: '100%' }} onClick={handleDownloadData}>
- Download Data
+ }
+ style={{ width: '100%' }}
+ onClick={handleDownloadData}
+ loading={downloading}
+ disabled={downloading}
+ >
+ {downloading ? 'Downloading...' : 'Download Data'}
@@ -719,6 +874,11 @@ export default function DashboardDefault() {
{/*
*/}
+
+ {/* row 5: Internet Shutdown Early Warning */}
+ {/*
+
+ */}
);
}
diff --git a/src/pages/extra-pages/documentation.jsx b/src/pages/extra-pages/documentation.jsx
index b16e1c5..1ae90f1 100644
--- a/src/pages/extra-pages/documentation.jsx
+++ b/src/pages/extra-pages/documentation.jsx
@@ -422,6 +422,56 @@ export default function Documentation() {
secondary="Interactive visualization showing user distribution across countries. Click on countries to see detailed statistics."
/>
+
+ Internet Shutdown Early Warning System:}
+ secondary={
+
+ Advanced anomaly detection that monitors signup patterns to identify potential internet shutdowns or spam attacks.
+ This is a critical feature because RelaySMS is a preparedness tool - people sign up when they anticipate connectivity
+ issues:
+
+ -
+ Signup Spike Detection - Automatically detects when signups from a country increase by 200%+
+ above the 7-day baseline
+
+ -
+ Retention Validation - Distinguishes real shutdown preparation (high retention) from spam attacks
+ (low retention)
+
+ -
+ Alert Levels:
+
+ -
+ Critical (Red) - Low retention (<50%) indicates possible spam
+ or bot attack. Requires investigation.
+
+ -
+ High Risk (Orange) - High retention (≥70%) with major spike.
+ Likely legitimate shutdown preparation.
+
+ -
+ Medium Risk (Blue) - Moderate retention (50-70%) with spike.
+ Monitor situation closely.
+
+
+
+ -
+ Confidence Score - Percentage indicating how certain the system is about the classification
+
+ -
+ Timeline View - Visual charts showing signup trends by country over time to spot patterns
+
+
+ Understanding the Logic: When people expect internet shutdowns, they prepare by signing up for
+ RelaySMS to maintain communication via SMS. A sudden spike in signups from a specific country, combined with high user
+ retention (users actually using the service), strongly suggests an impending or ongoing shutdown. Conversely, a spike
+ with low retention suggests fake accounts or spam. Use this tool to monitor high-risk regions, prepare infrastructure
+ for increased load, and potentially alert advocacy groups about shutdowns.
+
+ }
+ />
+
{
const appliedFilters = {
platform,
- startDate: startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2021-01-10',
+ startDate: startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2020-01-10',
endDate: endDate ? dayjs(endDate).format('YYYY-MM-DD') : today,
status,
source,
@@ -319,7 +320,7 @@ export default function Publications() {
if (startDate && endDate) {
return `${startDate.format('YYYY-MM-DD')} - ${endDate.format('YYYY-MM-DD')}`;
}
- return '2021-01-10 - Today';
+ return '2020-01-10 - Today';
default:
return 'Date Range Filter';
}
@@ -338,9 +339,10 @@ export default function Publications() {
};
const handleDownloadData = async () => {
+ setDownloading(true);
try {
const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
- const appliedStart = filtersApplied.startDate || '2021-01-10';
+ const appliedStart = filtersApplied.startDate || '2020-01-10';
const appliedEnd = filtersApplied.endDate || today;
const params = {
@@ -406,6 +408,8 @@ export default function Publications() {
} catch (error) {
console.error('Error downloading publications data:', error);
alert('Failed to download data. Please try again.');
+ } finally {
+ setDownloading(false);
}
};
@@ -463,7 +467,7 @@ export default function Publications() {
const fetchMetrics = async () => {
setLoading(true);
try {
- const appliedStart = startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2021-01-10';
+ const appliedStart = startDate ? dayjs(startDate).format('YYYY-MM-DD') : '2020-01-10';
const appliedEnd = endDate ? dayjs(endDate).format('YYYY-MM-DD') : today;
const startDateObj = dayjs(appliedStart);
@@ -600,7 +604,7 @@ export default function Publications() {
const fetchAllCountries = async () => {
try {
const params = {
- start_date: '2021-01-10',
+ start_date: '2020-01-10',
end_date: new Date().toISOString().split('T')[0],
page: 1,
page_size: 100
@@ -641,8 +645,8 @@ export default function Publications() {
Publications
- } onClick={handleDownloadData}>
- Download Data
+ } onClick={handleDownloadData} loading={downloading} disabled={downloading}>
+ {downloading ? 'Downloading...' : 'Download Data'}
diff --git a/src/pages/reliability/reliabilityTable.jsx b/src/pages/reliability/reliabilityTable.jsx
index d9dd2df..0532b53 100644
--- a/src/pages/reliability/reliabilityTable.jsx
+++ b/src/pages/reliability/reliabilityTable.jsx
@@ -134,7 +134,7 @@ export default function ReliabilityTable() {
operator,
operatorCode,
reliability,
- dateFilter: dateFilter ? dayjs(dateFilter).format('YYYY-MM-DD') : '2021-01-10'
+ dateFilter: dateFilter ? dayjs(dateFilter).format('YYYY-MM-DD') : '2020-01-10'
};
setFiltersApplied(appliedFilters);
setPage(0);
@@ -153,7 +153,7 @@ export default function ReliabilityTable() {
const handleDownloadData = async () => {
try {
- const appliedDate = filtersApplied.dateFilter || '2021-01-10';
+ const appliedDate = filtersApplied.dateFilter || '2020-01-10';
const params = {
date_filter: appliedDate,
@@ -261,7 +261,7 @@ export default function ReliabilityTable() {
setLoading(true);
setError(null);
try {
- const appliedDate = filtersApplied.dateFilter || '2021-01-10';
+ const appliedDate = filtersApplied.dateFilter || '2020-01-10';
const params = {
date_filter: appliedDate,
diff --git a/src/sections/dashboard/UserBarChart.jsx b/src/sections/dashboard/UserBarChart.jsx
index 2d1b602..c5d3dc0 100644
--- a/src/sections/dashboard/UserBarChart.jsx
+++ b/src/sections/dashboard/UserBarChart.jsx
@@ -46,13 +46,15 @@ export default function UserBarChart({ view, filters }) {
try {
const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : '';
+ const typeParam = filters?.type ? `&type=${filters.type}` : '';
+ const originParam = filters?.origin ? `&origin=${filters.origin}` : '';
const signupResponse = await axios.get(
- `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}`
+ `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}`
);
const retainedResponse = await axios.get(
- `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}`
+ `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}`
);
const signup = signupResponse?.data?.signup?.data ?? [];
@@ -78,7 +80,7 @@ export default function UserBarChart({ view, filters }) {
};
fetchData();
- }, [startDate, endDate, granularity, currentPage, filters?.countryCode]);
+ }, [startDate, endDate, granularity, currentPage, filters?.countryCode, filters?.type, filters?.origin]);
const axisFontStyle = { fontSize: 10, fill: theme.palette.text.secondary };
@@ -100,7 +102,7 @@ export default function UserBarChart({ view, filters }) {
User Data
- Signups & Retained
+ Signups & Current
@@ -123,7 +125,7 @@ export default function UserBarChart({ view, filters }) {
sx={{ '&.Mui-checked': { color: secondaryColor } }}
/>
}
- label="Retained"
+ label="Current"
/>
@@ -136,7 +138,7 @@ export default function UserBarChart({ view, filters }) {
yAxis={[{ disableLine: true, disableTicks: true, tickLabelStyle: axisFontStyle }]}
series={[
...(showSignups ? [{ data: signupData, label: 'Signups', color: primaryColor, type: 'bar' }] : []),
- ...(showRetained ? [{ data: retainedData, label: 'Retained', color: secondaryColor, type: 'bar' }] : [])
+ ...(showRetained ? [{ data: retainedData, label: 'Current', color: secondaryColor, type: 'bar' }] : [])
]}
slotProps={{ legend: { hidden: true }, bar: { rx: 5, ry: 5 } }}
axisHighlight={{ x: 'none' }}
diff --git a/src/sections/dashboard/default/CountryTable.jsx b/src/sections/dashboard/default/CountryTable.jsx
index 0846d6f..3aca74b 100644
--- a/src/sections/dashboard/default/CountryTable.jsx
+++ b/src/sections/dashboard/default/CountryTable.jsx
@@ -65,13 +65,15 @@ export default function CountryTable({ filters, onCountryClick, selectedCountry
useEffect(() => {
setPage(0);
- }, [effectiveStartDate, effectiveEndDate, effectiveCategory, effectiveGranularity, filters?.countryCode]);
+ }, [effectiveStartDate, effectiveEndDate, effectiveCategory, effectiveGranularity, filters?.countryCode, filters?.type, filters?.origin]);
useEffect(() => {
const fetchCountryData = async () => {
setLoading(true);
try {
const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : '';
+ const typeParam = filters?.type ? `&type=${filters.type}` : '';
+ const originParam = filters?.origin ? `&origin=${filters.origin}` : '';
if (effectiveCategory === 'all') {
const fetchAllPages = async (endpoint) => {
@@ -81,7 +83,7 @@ export default function CountryTable({ filters, onCountryClick, selectedCountry
do {
const response = await axios.get(
- `${import.meta.env.VITE_APP_TELEMETRY_API}${endpoint}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${currentPage}&page_size=25${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}${endpoint}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${currentPage}&page_size=25${countryParam}${typeParam}${originParam}`
);
const categoryKey = endpoint.includes('retained') ? 'retained' : 'signup';
@@ -152,7 +154,7 @@ export default function CountryTable({ filters, onCountryClick, selectedCountry
setCountryData(paginatedData);
} else {
const response = await axios.get(
- `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${page + 1}&page_size=${rowsPerPage}${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}`
);
const categoryKey = effectiveCategory.includes('retained') ? 'retained' : 'signup';
diff --git a/src/sections/dashboard/default/Map.jsx b/src/sections/dashboard/default/Map.jsx
index 7562133..246429d 100644
--- a/src/sections/dashboard/default/Map.jsx
+++ b/src/sections/dashboard/default/Map.jsx
@@ -42,14 +42,16 @@ export default function CountryMap({ filters, selectedCountry, onCountrySelect }
setLoading(true);
try {
const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : '';
+ const typeParam = filters?.type ? `&type=${filters.type}` : '';
+ const originParam = filters?.origin ? `&origin=${filters.origin}` : '';
if (effectiveCategory === 'all') {
const [signupResponse, retainedResponse] = await Promise.all([
axios.get(
- `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}`
),
axios.get(
- `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}`
)
]);
@@ -111,7 +113,7 @@ export default function CountryMap({ filters, selectedCountry, onCountrySelect }
setCountryData(formatted);
} else {
const response = await axios.get(
- `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${effectiveGranularity}&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}`
);
const categoryKey = effectiveCategory.includes('retained') ? 'retained' : 'signup';
@@ -153,7 +155,7 @@ export default function CountryMap({ filters, selectedCountry, onCountrySelect }
} finally {
setLoading(false);
}
- }, [effectiveCategory, effectiveStartDate, effectiveEndDate, effectiveGranularity, filters?.countryCode]);
+ }, [effectiveCategory, effectiveStartDate, effectiveEndDate, effectiveGranularity, filters?.countryCode, filters?.type, filters?.origin]);
useEffect(() => {
fetchCountryData();
diff --git a/src/sections/dashboard/default/ShutdownEarlyWarning.jsx b/src/sections/dashboard/default/ShutdownEarlyWarning.jsx
new file mode 100644
index 0000000..d4a3f7d
--- /dev/null
+++ b/src/sections/dashboard/default/ShutdownEarlyWarning.jsx
@@ -0,0 +1,517 @@
+import { useEffect, useState, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import {
+ Box,
+ Typography,
+ Grid,
+ Card,
+ CardContent,
+ Chip,
+ Alert,
+ AlertTitle,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ ToggleButtonGroup,
+ ToggleButton
+} from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { LineChart } from '@mui/x-charts/LineChart';
+import { WarningOutlined } from '@ant-design/icons';
+import { CheckCircleOutlined } from '@ant-design/icons';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc';
+import { RiseOutlined } from '@ant-design/icons';
+import { FallOutlined } from '@ant-design/icons';
+
+// components
+import MainCard from 'components/MainCard';
+import Loader from 'components/Loader';
+
+dayjs.extend(utc);
+
+export default function ShutdownEarlyWarning({ filters }) {
+ const theme = useTheme();
+ const [loading, setLoading] = useState(true);
+ const [alerts, setAlerts] = useState([]);
+ const [countryTimelines, setCountryTimelines] = useState([]);
+ const [view, setView] = useState('alerts');
+
+ const today = new Date().toISOString().split('T')[0];
+ const startDate = filters?.startDate || dayjs().subtract(30, 'day').format('YYYY-MM-DD');
+ const endDate = filters?.endDate || today;
+ const countryFilter = filters?.countryCode || '';
+ const typeFilter = filters?.type || '';
+ const originFilter = filters?.origin || '';
+
+ const baselineStartDate = dayjs(startDate).subtract(7, 'day').format('YYYY-MM-DD');
+ const baselineEndDate = dayjs(startDate).subtract(1, 'day').format('YYYY-MM-DD');
+
+ const fetchSignupData = useCallback(async (start, end, country = '', type = '', origin = '') => {
+ const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
+ const countryParam = country ? `&country_code=${country}` : '';
+ const typeParam = type ? `&type=${type}` : '';
+ const originParam = origin ? `&origin=${origin}` : '';
+
+ const response = await axios.get(
+ `${baseUrl}signup?category=signup&start_date=${start}&end_date=${end}&granularity=day&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}`
+ );
+
+ return response?.data?.signup?.data ?? [];
+ }, []);
+
+ const fetchRetainedData = useCallback(async (start, end, country = '', type = '', origin = '') => {
+ const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
+ const countryParam = country ? `&country_code=${country}` : '';
+ const typeParam = type ? `&type=${type}` : '';
+ const originParam = origin ? `&origin=${origin}` : '';
+
+ const response = await axios.get(
+ `${baseUrl}retained?category=retained&start_date=${start}&end_date=${end}&granularity=day&group_by=country&page=1&page_size=100${countryParam}${typeParam}${originParam}`
+ );
+
+ return response?.data?.retained?.data ?? [];
+ }, []);
+
+ const calculateRetentionRate = useCallback((signups, retained, country) => {
+ const countrySignups = signups.filter((s) => s.country_code === country);
+ const countryRetained = retained.filter((r) => r.country_code === country);
+
+ const totalSignups = countrySignups.reduce((sum, s) => sum + (s.signup_users || 0), 0);
+ const totalRetained = countryRetained.reduce((sum, r) => sum + (r.retained_users || 0), 0);
+
+ if (totalSignups === 0) return 0;
+ return ((totalRetained / totalSignups) * 100).toFixed(1);
+ }, []);
+
+ const detectAnomalies = useCallback(
+ (baselineData, currentData, retainedData) => {
+ const anomalies = [];
+
+ const currentByCountry = currentData.reduce((acc, item) => {
+ const country = item.country_code;
+ if (!acc[country]) {
+ acc[country] = { signups: 0, countryName: item.country_name };
+ }
+ acc[country].signups += item.signup_users || 0;
+ return acc;
+ }, {});
+
+ const baselineByCountry = baselineData.reduce((acc, item) => {
+ const country = item.country_code;
+ if (!acc[country]) {
+ acc[country] = { signups: 0 };
+ }
+ acc[country].signups += item.signup_users || 0;
+ return acc;
+ }, {});
+
+ Object.entries(currentByCountry).forEach(([country, data]) => {
+ const currentSignups = data.signups;
+ const baselineSignups = baselineByCountry[country]?.signups || 0;
+
+ let percentageChange = 0;
+ if (baselineSignups > 0) {
+ percentageChange = ((currentSignups - baselineSignups) / baselineSignups) * 100;
+ } else if (currentSignups > 0) {
+ percentageChange = 100;
+ }
+
+ if (percentageChange > 200 || (currentSignups - baselineSignups > 50 && percentageChange > 100)) {
+ const retentionRate = parseFloat(calculateRetentionRate(currentData, retainedData, country));
+
+ let alertType = 'info';
+ let alertLevel = 'Low';
+ let message = '';
+ let confidence = 0;
+
+ if (retentionRate >= 50) {
+ alertType = 'warning';
+ alertLevel = retentionRate >= 70 ? 'High' : 'Medium';
+ message = 'Possible shutdown preparation - Users actively using service';
+ confidence = Math.min(95, 50 + retentionRate / 2);
+ } else if (retentionRate < 50 && retentionRate > 0) {
+ alertType = 'info';
+ alertLevel = 'Low';
+ message = 'Moderate retention - Monitor for confirmation';
+ confidence = 30 + retentionRate;
+ } else {
+ alertType = 'error';
+ alertLevel = 'Critical';
+ message = 'Possible spam/bot attack - Low retention rate';
+ confidence = 75;
+ }
+
+ anomalies.push({
+ country,
+ countryName: data.countryName || country,
+ currentSignups,
+ baselineSignups,
+ percentageChange: percentageChange.toFixed(1),
+ retentionRate,
+ alertType,
+ alertLevel,
+ message,
+ confidence: confidence.toFixed(0),
+ detectedAt: new Date().toISOString()
+ });
+ }
+ });
+
+ return anomalies.sort((a, b) => {
+ const severityOrder = { High: 3, Medium: 2, Low: 1, Critical: 4 };
+ if (severityOrder[a.alertLevel] !== severityOrder[b.alertLevel]) {
+ return severityOrder[b.alertLevel] - severityOrder[a.alertLevel];
+ }
+ return b.percentageChange - a.percentageChange;
+ });
+ },
+ [calculateRetentionRate]
+ );
+
+ const processTimelineData = useCallback((data) => {
+ const grouped = data.reduce((acc, item) => {
+ const country = item.country_code;
+ const date = item.timeframe;
+
+ if (!acc[country]) {
+ acc[country] = {
+ countryName: item.country_name || country,
+ data: []
+ };
+ }
+
+ acc[country].data.push({
+ date,
+ signups: item.signup_users || 0
+ });
+
+ return acc;
+ }, {});
+
+ return Object.entries(grouped)
+ .map(([country, info]) => ({
+ country,
+ countryName: info.countryName,
+ totalSignups: info.data.reduce((sum, d) => sum + d.signups, 0),
+ timeline: info.data.sort((a, b) => new Date(a.date) - new Date(b.date))
+ }))
+ .sort((a, b) => b.totalSignups - a.totalSignups)
+ .slice(0, 10);
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ try {
+ const baselineSignups = await fetchSignupData(baselineStartDate, baselineEndDate, countryFilter, typeFilter, originFilter);
+
+ const currentSignups = await fetchSignupData(startDate, endDate, countryFilter, typeFilter, originFilter);
+
+ const retainedUsers = await fetchRetainedData(startDate, endDate, countryFilter, typeFilter, originFilter);
+
+ const detectedAnomalies = detectAnomalies(baselineSignups, currentSignups, retainedUsers);
+ setAlerts(detectedAnomalies);
+
+ const allData = [...baselineSignups, ...currentSignups];
+ const timelines = processTimelineData(allData);
+ setCountryTimelines(timelines);
+ } catch (err) {
+ console.error('Error fetching shutdown warning data:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [
+ startDate,
+ endDate,
+ countryFilter,
+ typeFilter,
+ originFilter,
+ baselineStartDate,
+ baselineEndDate,
+ fetchSignupData,
+ fetchRetainedData,
+ detectAnomalies,
+ processTimelineData
+ ]);
+
+ const getAlertIcon = (alertType) => {
+ switch (alertType) {
+ case 'warning':
+ return ;
+ case 'error':
+ return ;
+ case 'info':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getAlertColor = (alertType) => {
+ switch (alertType) {
+ case 'warning':
+ return theme.palette.warning.main;
+ case 'error':
+ return theme.palette.error.main;
+ case 'info':
+ return theme.palette.info.main;
+ default:
+ return theme.palette.success.main;
+ }
+ };
+
+ const handleViewChange = (event, newView) => {
+ if (newView !== null) {
+ setView(newView);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Internet Shutdown Early Warning System
+
+
+ Monitors signup spikes and retention patterns to detect potential shutdowns or spam attacks
+
+
+
+
+ Alert Dashboard
+ Country Timelines
+
+
+
+ {/* Summary Metrics */}
+
+
+
+
+ Critical Alerts
+
+
+ {alerts.filter((a) => a.alertLevel === 'Critical').length}
+
+ Possible spam attacks
+
+
+
+
+
+ High Risk Alerts
+
+
+ {alerts.filter((a) => a.alertLevel === 'High').length}
+
+ Likely shutdown prep
+
+
+
+
+
+ Medium Risk Alerts
+
+
+ {alerts.filter((a) => a.alertLevel === 'Medium').length}
+
+ Monitor closely
+
+
+
+
+
+ Total Countries Monitored
+
+
+ {countryTimelines.length}
+
+ Active countries
+
+
+
+
+
+ {alerts.length === 0 && view === 'alerts' ? (
+
+
+
+ No Anomalies Detected
+
+
+ All signup patterns appear normal. The system is monitoring for unusual activity.
+
+
+ ) : view === 'alerts' ? (
+
+
+ Active Alerts ({alerts.length})
+
+
+ {alerts.map((alert, index) => (
+
+
+
+
+ {alert.countryName} ({alert.country})
+
+
+
+
+
+ {alert.message}
+
+
+
+
+ Current Signups
+
+
+ {alert.currentSignups}
+
+
+
+
+
+ Baseline (7-day avg)
+
+ {alert.baselineSignups}
+
+
+
+ Change
+
+
+ +{alert.percentageChange}%
+
+
+
+
+ Retention Rate
+
+ = 50 ? theme.palette.success.main : theme.palette.error.main
+ }}
+ >
+ {alert.retentionRate}%
+
+
+
+
+
+ ))}
+
+
+ ) : (
+ // Timeline View
+
+
+ Signup Trends by Country (Top 10)
+
+ {countryTimelines.length === 0 ? (
+
+
+ No timeline data available for the selected period
+
+
+ ) : (
+
+ {countryTimelines.slice(0, 6).map((countryData, index) => (
+
+
+
+ {countryData.countryName} ({countryData.country})
+
+
+ Total Signups: {countryData.totalSignups.toLocaleString()}
+
+
+ idx),
+ scaleType: 'point',
+ tickLabelStyle: {
+ fontSize: 10,
+ fill: theme.palette.text.primary
+ }
+ }
+ ]}
+ yAxis={[
+ {
+ tickLabelStyle: {
+ fontSize: 10,
+ fill: theme.palette.text.primary
+ }
+ }
+ ]}
+ series={[
+ {
+ data: countryData.timeline.map((t) => t.signups),
+ curve: 'monotoneX',
+ showMark: true,
+ color: theme.palette.primary.main
+ }
+ ]}
+ margin={{ top: 10, bottom: 30, left: 40, right: 10 }}
+ grid={{ horizontal: true }}
+ />
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ );
+}
+
+ShutdownEarlyWarning.propTypes = {
+ filters: PropTypes.object
+};
diff --git a/src/sections/dashboard/default/UserAreaChart.jsx b/src/sections/dashboard/default/UserAreaChart.jsx
index 16eddea..182f79c 100644
--- a/src/sections/dashboard/default/UserAreaChart.jsx
+++ b/src/sections/dashboard/default/UserAreaChart.jsx
@@ -65,13 +65,15 @@ export default function UserAreaChart({ view, filters }) {
try {
const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : '';
+ const typeParam = filters?.type ? `&type=${filters.type}` : '';
+ const originParam = filters?.origin ? `&origin=${filters.origin}` : '';
const signupResponse = await axios.get(
- `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}`
+ `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}`
);
const retainedResponse = await axios.get(
- `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}`
+ `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=${granularity}&group_by=date&page=${currentPage + 1}&page_size=${pageSize}${countryParam}${typeParam}${originParam}`
);
const signup = signupResponse?.data?.signup?.data ?? [];
@@ -97,7 +99,7 @@ export default function UserAreaChart({ view, filters }) {
};
fetchData();
- }, [startDate, endDate, granularity, currentPage, filters?.countryCode]);
+ }, [startDate, endDate, granularity, currentPage, filters?.countryCode, filters?.type, filters?.origin]);
const toggleVisibility = (label) => {
setVisibility((prev) => ({ ...prev, [label]: !prev[label] }));
@@ -115,12 +117,12 @@ export default function UserAreaChart({ view, filters }) {
},
{
data: retainedData,
- label: 'Retained',
+ label: 'Current',
showMark: false,
area: true,
- id: 'Retained',
+ id: 'Current',
color: theme.palette.primary[700],
- visible: visibility['Retained']
+ visible: visibility['Current']
}
];
@@ -161,7 +163,7 @@ export default function UserAreaChart({ view, filters }) {
slotProps={{ legend: { hidden: true } }}
sx={{
'& .MuiAreaElement-series-Signups': { fill: "url('#myGradient1')", strokeWidth: 2, opacity: 0.8 },
- '& .MuiAreaElement-series-Retained': { fill: "url('#myGradient2')", strokeWidth: 2, opacity: 0.8 },
+ '& .MuiAreaElement-series-Current': { fill: "url('#myGradient2')", strokeWidth: 2, opacity: 0.8 },
'& .MuiChartsAxis-directionX .MuiChartsAxis-tick': { stroke: theme.palette.divider }
}}
>
@@ -189,8 +191,6 @@ export default function UserAreaChart({ view, filters }) {
Next
-
-
>
)}
>
diff --git a/src/sections/dashboard/default/UserRetentionMetrics.jsx b/src/sections/dashboard/default/UserRetentionMetrics.jsx
index fb3f878..5e7a2f1 100644
--- a/src/sections/dashboard/default/UserRetentionMetrics.jsx
+++ b/src/sections/dashboard/default/UserRetentionMetrics.jsx
@@ -35,9 +35,11 @@ export default function UserRetentionMetrics({ filters }) {
const [periodType, setPeriodType] = useState('month');
const today = new Date().toISOString().split('T')[0];
- const startDate = filters?.startDate || '2021-01-10';
+ const startDate = filters?.startDate || '2020-01-10';
const endDate = filters?.endDate || today;
const country = filters?.countryCode || '';
+ const type = filters?.type || '';
+ const origin = filters?.origin || '';
const calculateFuturePeriod = useCallback((basePeriod, periodsAhead) => {
const date = dayjs(basePeriod);
@@ -107,13 +109,15 @@ export default function UserRetentionMetrics({ filters }) {
try {
const baseUrl = import.meta.env.VITE_APP_TELEMETRY_API;
const countryParam = country ? `&country_code=${country}` : '';
+ const typeParam = type ? `&type=${type}` : '';
+ const originParam = origin ? `&origin=${origin}` : '';
const signupResponse = await axios.get(
- `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}`
+ `${baseUrl}signup?category=signup&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}${typeParam}${originParam}`
);
const retainedResponse = await axios.get(
- `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}`
+ `${baseUrl}retained?category=retained&start_date=${startDate}&end_date=${endDate}&granularity=month&group_by=date&page=1&page_size=100${countryParam}${typeParam}${originParam}`
);
const signupData = signupResponse?.data?.signup?.data ?? [];
@@ -132,7 +136,7 @@ export default function UserRetentionMetrics({ filters }) {
};
fetchRetentionData();
- }, [startDate, endDate, country, processCohortData, processRetentionCurves]);
+ }, [startDate, endDate, country, type, origin, processCohortData, processRetentionCurves]);
const getCellColor = (percentage) => {
if (percentage >= 80) return theme.palette.success.dark;
diff --git a/src/sections/dashboard/default/UserTable.jsx b/src/sections/dashboard/default/UserTable.jsx
index 8320e2f..ca63498 100644
--- a/src/sections/dashboard/default/UserTable.jsx
+++ b/src/sections/dashboard/default/UserTable.jsx
@@ -65,7 +65,7 @@ export default function UserTable({ filters }) {
const today = new Date();
const defaultEndDate = today.toISOString().split('T')[0];
- const defaultStartDate = '2021-01-10';
+ const defaultStartDate = '2020-01-10';
const effectiveStartDate = filters?.startDate || defaultStartDate;
const effectiveEndDate = filters?.endDate || defaultEndDate;
const groupBy = filters?.groupBy || 'date';
@@ -73,10 +73,12 @@ export default function UserTable({ filters }) {
useEffect(() => {
setPage(0);
- }, [effectiveStartDate, effectiveEndDate, effectiveCategory, filters?.countryCode]);
+ }, [effectiveStartDate, effectiveEndDate, effectiveCategory, filters?.countryCode, filters?.type, filters?.origin]);
useEffect(() => {
const countryParam = filters?.countryCode ? `&country_code=${filters.countryCode}` : '';
+ const typeParam = filters?.type ? `&type=${filters.type}` : '';
+ const originParam = filters?.origin ? `&origin=${filters.origin}` : '';
setLoading(true);
setError('');
@@ -84,10 +86,10 @@ export default function UserTable({ filters }) {
if (effectiveCategory === 'all') {
Promise.all([
fetch(
- `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}signup?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}`
),
fetch(
- `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}`
+ `${import.meta.env.VITE_APP_TELEMETRY_API}retained?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}`
)
])
.then(([signupRes, retainedRes]) => Promise.all([signupRes.json(), retainedRes.json()]))
@@ -128,7 +130,7 @@ export default function UserTable({ filters }) {
})
.finally(() => setLoading(false));
} else {
- const apiUrl = `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}`;
+ const apiUrl = `${import.meta.env.VITE_APP_TELEMETRY_API}${effectiveCategory}?start_date=${effectiveStartDate}&end_date=${effectiveEndDate}&granularity=${granularity}&group_by=date&page=${page + 1}&page_size=${rowsPerPage}${countryParam}${typeParam}${originParam}`;
fetch(apiUrl)
.then((res) => res.json())
@@ -152,6 +154,8 @@ export default function UserTable({ filters }) {
filters?.startDate,
filters?.endDate,
filters?.countryCode,
+ filters?.type,
+ filters?.origin,
groupBy,
effectiveCategory,
page,
diff --git a/src/sections/publications/PlatformDistributionChart.jsx b/src/sections/publications/PlatformDistributionChart.jsx
index f3d06ec..70ab7e1 100644
--- a/src/sections/publications/PlatformDistributionChart.jsx
+++ b/src/sections/publications/PlatformDistributionChart.jsx
@@ -17,7 +17,7 @@ export default function PlatformDistributionChart({ filters }) {
const today = new Date();
const defaultEndDate = today.toISOString().split('T')[0];
- const startDate = filters?.startDate || '2021-01-10';
+ const startDate = filters?.startDate || '2020-01-10';
const endDate = filters?.endDate || defaultEndDate;
const status = filters?.status || '';
const source = filters?.source || '';
diff --git a/src/sections/publications/PlatformSuccessRateChart.jsx b/src/sections/publications/PlatformSuccessRateChart.jsx
index 5c2e9fa..888956b 100644
--- a/src/sections/publications/PlatformSuccessRateChart.jsx
+++ b/src/sections/publications/PlatformSuccessRateChart.jsx
@@ -88,7 +88,7 @@ export function PlatformSuccessRateSummary({ filters }) {
const today = new Date();
const defaultEndDate = today.toISOString().split('T')[0];
- const startDate = filters?.startDate || '2021-01-10';
+ const startDate = filters?.startDate || '2020-01-10';
const endDate = filters?.endDate || defaultEndDate;
const source = filters?.source || '';
const country = filters?.country || '';
@@ -188,7 +188,7 @@ export default function PlatformSuccessRateChart({ filters }) {
const today = new Date();
const defaultEndDate = today.toISOString().split('T')[0];
- const startDate = filters?.startDate || '2021-01-10';
+ const startDate = filters?.startDate || '2020-01-10';
const endDate = filters?.endDate || defaultEndDate;
const source = filters?.source || '';
const country = filters?.country || '';
diff --git a/src/sections/publications/PublicationChart.jsx b/src/sections/publications/PublicationChart.jsx
index 506157a..49bff3b 100644
--- a/src/sections/publications/PublicationChart.jsx
+++ b/src/sections/publications/PublicationChart.jsx
@@ -23,7 +23,7 @@ export default function PublicationChart({ filters }) {
const today = new Date();
const defaultEndDate = today.toISOString().split('T')[0];
- const startDate = filters?.startDate || '2021-01-10';
+ const startDate = filters?.startDate || '2020-01-10';
const endDate = filters?.endDate || defaultEndDate;
const platform = filters?.platform || '';
const status = filters?.status || '';
diff --git a/src/sections/publications/PublicationMap.jsx b/src/sections/publications/PublicationMap.jsx
index d695df1..dda3722 100644
--- a/src/sections/publications/PublicationMap.jsx
+++ b/src/sections/publications/PublicationMap.jsx
@@ -31,7 +31,7 @@ export default function PublicationMap({ filters, selectedCountry, onCountrySele
const geoJsonLayerRef = useRef(null);
const today = new Date().toISOString().split('T')[0];
- const effectiveStartDate = filters?.startDate || '2021-01-10';
+ const effectiveStartDate = filters?.startDate || '2020-01-10';
const effectiveEndDate = filters?.endDate || today;
const effectivePlatform = filters?.platform || '';
const effectiveStatus = filters?.status || '';
diff --git a/src/sections/publications/UsageHeatmap.jsx b/src/sections/publications/UsageHeatmap.jsx
index f0916d3..fe0fbd1 100644
--- a/src/sections/publications/UsageHeatmap.jsx
+++ b/src/sections/publications/UsageHeatmap.jsx
@@ -22,7 +22,7 @@ export default function UsageHeatmap({ filters }) {
const [error, setError] = useState('');
const today = new Date().toISOString().split('T')[0];
- const startDate = filters?.startDate || '2021-01-10';
+ const startDate = filters?.startDate || '2020-01-10';
const endDate = filters?.endDate || today;
const platform = filters?.platform || '';
const status = filters?.status || '';