chore(branding): rebrand FixMate to CityPulse across codebase

- Update product name in README, backend docs, and design tokens
- Rename Flutter root widget to CityPulseApp and update tests
- Update dashboard brand strings and HTML title
- Refresh i18n strings (en/ms) and welcome taglines
- Adjust backend API title/description and root message
- Minor formatting in ApiService comments; no logic changes
- Update Android/iOS manifest comments and pubspec description

No API endpoints or response schema changes.
This commit is contained in:
2025-09-27 11:15:15 +08:00
parent 6924455d35
commit ed63a0cbc8
20 changed files with 59 additions and 44 deletions

View File

@@ -1,6 +1,6 @@
# 🏗️ FixMate - Smart Citizen-Driven Urban Maintenance Platform # 🏗️ CityPulse - Smart Citizen-Driven Urban Maintenance Platform
FixMate is a comprehensive citizen reporting application that combines **Flutter frontend** with **Python FastAPI backend** and **AI-powered image classification**. Users can capture urban issues (potholes, broken streetlights, trash, etc.), get automatic AI classification, and track their reports through a complete management system. CityPulse is a comprehensive citizen reporting application that combines **Flutter frontend** with **Python FastAPI backend** and **AI-powered image classification**. Users can capture urban issues (potholes, broken streetlights, trash, etc.), get automatic AI classification, and track their reports through a complete management system.
## 🎯 System Architecture ## 🎯 System Architecture

View File

@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions required for FixMate --> <!-- Permissions required for CityPulse -->
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

View File

@@ -1,5 +1,5 @@
{ {
"app.name": "FixMate", "app.name": "CityPulse",
"nav.report": "Report", "nav.report": "Report",
"nav.map": "Map", "nav.map": "Map",
"nav.myReports": "My Reports", "nav.myReports": "My Reports",
@@ -81,14 +81,14 @@
"settings.theme.dark": "Dark", "settings.theme.dark": "Dark",
"lang.en": "English", "lang.en": "English",
"lang.ms": "Bahasa Malaysia", "lang.ms": "Bahasa Malaysia",
"welcome.title": "Spot it. Snap it. Fix it.", "welcome.title": "Snap. Detect. Fix.",
"welcome.subtitle": "Report city issues in seconds with AI-powered detection. Help create safer, better communities together.", "welcome.subtitle": "Report city issues in seconds with AI-powered detection. Help create safer, better communities together.",
"cta.continueGuest": "Continue as guest", "cta.continueGuest": "Continue as guest",
"cta.signIn": "Sign in", "cta.signIn": "Sign in",
"cta.skip": "Skip", "cta.skip": "Skip",
"cta.next": "Next", "cta.next": "Next",
"cta.getStarted": "Get started", "cta.getStarted": "Get started",
"onboarding.header": "Welcome to FixMate", "onboarding.header": "Welcome to CityPulse",
"onboarding.title1": "Fast Issue Reporting", "onboarding.title1": "Fast Issue Reporting",
"onboarding.body1": "Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.", "onboarding.body1": "Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.",
"onboarding.title2": "Smart City Mapping", "onboarding.title2": "Smart City Mapping",
@@ -99,12 +99,12 @@
"auth.signInWithApple": "Sign in with Apple", "auth.signInWithApple": "Sign in with Apple",
"auth.signInWithGoogle": "Sign in with Google", "auth.signInWithGoogle": "Sign in with Google",
"auth.comingSoon": "Coming soon", "auth.comingSoon": "Coming soon",
"welcome.title": "Spot it. Snap it. Fix it.", "welcome.title": "Snap. Detect. Fix.",
"welcome.subtitle": "Report city issues in seconds with AI-powered detection. Help create safer, better communities together.", "welcome.subtitle": "Report city issues in seconds with AI-powered detection. Help create safer, better communities together.",
"cta.continueGuest": "Continue as Guest", "cta.continueGuest": "Continue as Guest",
"cta.signIn": "Sign In", "cta.signIn": "Sign In",
"cta.skip": "Skip for now", "cta.skip": "Skip for now",
"onboarding.header": "Welcome to FixMate", "onboarding.header": "Welcome to CityPulse",
"onboarding.title1": "Fast Issue Reporting", "onboarding.title1": "Fast Issue Reporting",
"onboarding.subtitle1": "AI-Powered Detection", "onboarding.subtitle1": "AI-Powered Detection",
"onboarding.body1": "Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.", "onboarding.body1": "Simply take a photo of any urban issue - our AI automatically identifies and categorizes the problem in seconds.",

View File

@@ -1,5 +1,5 @@
{ {
"app.name": "FixMate", "app.name": "CityPulse",
"nav.report": "Lapor", "nav.report": "Lapor",
"nav.map": "Peta", "nav.map": "Peta",
"nav.myReports": "Laporan Saya", "nav.myReports": "Laporan Saya",
@@ -88,7 +88,7 @@
"cta.skip": "Langkau", "cta.skip": "Langkau",
"cta.next": "Seterusnya", "cta.next": "Seterusnya",
"cta.getStarted": "Mula", "cta.getStarted": "Mula",
"onboarding.header": "Selamat datang ke FixMate", "onboarding.header": "Selamat datang ke CityPulse",
"onboarding.title1": "Tangkap pantas", "onboarding.title1": "Tangkap pantas",
"onboarding.body1": "Ambil gambar dan hantar dalam kurang satu minit.", "onboarding.body1": "Ambil gambar dan hantar dalam kurang satu minit.",
"onboarding.title2": "Peta yang jelas", "onboarding.title2": "Peta yang jelas",
@@ -104,7 +104,7 @@
"cta.continueGuest": "Teruskan sebagai Tetamu", "cta.continueGuest": "Teruskan sebagai Tetamu",
"cta.signIn": "Log Masuk", "cta.signIn": "Log Masuk",
"cta.skip": "Langkau buat masa ini", "cta.skip": "Langkau buat masa ini",
"onboarding.header": "Selamat Datang ke FixMate", "onboarding.header": "Selamat Datang ke CityPulse",
"onboarding.title1": "Laporan Isu Pantas", "onboarding.title1": "Laporan Isu Pantas",
"onboarding.subtitle1": "Pengesanan Berkuasa AI", "onboarding.subtitle1": "Pengesanan Berkuasa AI",
"onboarding.body1": "Hanya ambil gambar mana-mana isu bandar - AI kami secara automatik mengenal pasti dan mengkategorikan masalah dalam beberapa saat.", "onboarding.body1": "Hanya ambil gambar mana-mana isu bandar - AI kami secara automatik mengenal pasti dan mengkategorikan masalah dalam beberapa saat.",

View File

@@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "FixMate Design Tokens", "name": "CityPulse Design Tokens",
"version": "1.0.0", "version": "1.0.0",
"brand": "Civic Premium Citizen First" "brand": "Civic Premium Citizen First"
}, },

View File

@@ -2,7 +2,7 @@ Perfect 👍 thanks for clarifying — lets keep it **venv only**. Ill adj
--- ---
# 🛠️ FixMate Backend Hackathon Prototype # 🛠️ CityPulse Backend Hackathon Prototype
Smart citizen-driven urban maintenance platform powered by **Computer Vision + Generative AI**. Smart citizen-driven urban maintenance platform powered by **Computer Vision + Generative AI**.
This backend runs fully **locally** (no cloud required). This backend runs fully **locally** (no cloud required).

Binary file not shown.

View File

@@ -16,18 +16,18 @@ logger = logging.getLogger(__name__)
# ---------------------- # ----------------------
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
logger.info("Starting FixMate Backend...") logger.info("Starting CityPulse Backend...")
init_ai_service() # ✅ Models load once here init_ai_service() # ✅ Models load once here
logger.info("AI models loaded successfully.") logger.info("AI models loaded successfully.")
yield yield
logger.info("FixMate Backend shutting down...") logger.info("CityPulse Backend shutting down...")
# ---------------------- # ----------------------
# Initialize FastAPI # Initialize FastAPI
# ---------------------- # ----------------------
app = FastAPI( app = FastAPI(
title="FixMate Backend API", title="CityPulse Backend API",
description="Backend for FixMate Hackathon Prototype", description="Backend for CityPulse Hackathon Prototype",
version="1.0.0", version="1.0.0",
lifespan=lifespan lifespan=lifespan
) )
@@ -80,7 +80,7 @@ except Exception as e:
@app.get("/") @app.get("/")
def root(): def root():
return {"message": "Welcome to FixMate Backend API! Visit /docs for API documentation."} return {"message": "Welcome to CityPulse Backend API! Visit /docs for API documentation."}
@app.get("/test") @app.get("/test")
def test(): def test():

View File

@@ -2,7 +2,7 @@ Perfect 👍 Before I drop a full codebase, lets agree on the **flow + plan**
--- ---
# ⚡ Backend Flow (FixMate Local Prototype) # ⚡ Backend Flow (CityPulse Local Prototype)
### 1. Citizen Upload Flow ### 1. Citizen Upload Flow
@@ -32,7 +32,7 @@ Perfect 👍 Before I drop a full codebase, lets agree on the **flow + plan**
* Every report: * Every report:
* Pass image to model → detect objects. * Pass image to model → detect objects.
* Map objects to FixMate categories (`pothole`, `streetlight`, `trash`, `signage`). * Map objects to CityPulse categories (`pothole`, `streetlight`, `trash`, `signage`).
* Apply **severity scoring** (e.g. bounding box area = High if > certain %). * Apply **severity scoring** (e.g. bounding box area = High if > certain %).
* If model fails (no internet, missing weights): * If model fails (no internet, missing weights):

View File

@@ -430,7 +430,7 @@ const cycleStatus = async (reportId) => {
return ( return (
<div className="app-root"> <div className="app-root">
<header className="header"> <header className="header">
<div className="brand">{t('dashboard.brand') || 'FixMate'}</div> <div className="brand">{t('dashboard.brand') || 'CityPulse'}</div>
<div className="lang-toggle"> <div className="lang-toggle">
<label style={{fontSize:12, color:'#374151'}}>{t('label.language') || 'Language'}</label> <label style={{fontSize:12, color:'#374151'}}>{t('label.language') || 'Language'}</label>
<select value={lang} onChange={e=>setLang(e.target.value)}> <select value={lang} onChange={e=>setLang(e.target.value)}>

View File

@@ -1,5 +1,5 @@
{ {
"dashboard.brand": "FixMate", "dashboard.brand": "CityPulse",
"dashboard.filters": "Filters", "dashboard.filters": "Filters",
"queue.title": "Tickets", "queue.title": "Tickets",
"drawer.details": "Details", "drawer.details": "Details",

View File

@@ -1,5 +1,5 @@
{ {
"dashboard.brand": "FixMate", "dashboard.brand": "CityPulse",
"dashboard.filters": "Penapis", "dashboard.filters": "Penapis",
"queue.title": "Tiket", "queue.title": "Tiket",
"drawer.details": "Maklumat", "drawer.details": "Maklumat",

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FixMate Dashboard</title> <title>CityPulse Dashboard</title>
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />

View File

@@ -46,7 +46,7 @@
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<!-- Permissions required by FixMate --> <!-- Permissions required by CityPulse -->
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Camera access is required to capture issue photos.</string> <string>Camera access is required to capture issue photos.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>

View File

@@ -10,8 +10,8 @@ import 'screens/settings/settings_screen.dart';
import 'theme/themes.dart'; import 'theme/themes.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class FixMateApp extends StatelessWidget { class CityPulseApp extends StatelessWidget {
const FixMateApp({super.key}); const CityPulseApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@@ -18,7 +18,7 @@ void main() async {
runApp( runApp(
ChangeNotifierProvider.value( ChangeNotifierProvider.value(
value: localeProvider, value: localeProvider,
child: const FixMateApp(), child: const CityPulseApp(),
), ),
); );
} }

View File

@@ -5,7 +5,7 @@ import 'package:uuid/uuid.dart';
import '../models/report.dart'; import '../models/report.dart';
import '../models/enums.dart'; import '../models/enums.dart';
/// Service for communicating with the FixMate Backend API /// Service for communicating with the CityPulse Backend API
class ApiService { class ApiService {
// Configure this to match your backend URL // Configure this to match your backend URL
// Use localhost for web/desktop, network IP for mobile/emulator // Use localhost for web/desktop, network IP for mobile/emulator
@@ -77,8 +77,10 @@ class ApiService {
request.fields['latitude'] = latitude.toString(); request.fields['latitude'] = latitude.toString();
request.fields['longitude'] = longitude.toString(); request.fields['longitude'] = longitude.toString();
request.fields['description'] = description; request.fields['description'] = description;
if (userName != null && userName.isNotEmpty) request.fields['user_name'] = userName; if (userName != null && userName.isNotEmpty)
if (address != null && address.isNotEmpty) request.fields['address'] = address; request.fields['user_name'] = userName;
if (address != null && address.isNotEmpty)
request.fields['address'] = address;
// Add the image file // Add the image file
request.files.add( request.files.add(
@@ -162,7 +164,9 @@ class ApiService {
if (response.statusCode == 200 || response.statusCode == 204) { if (response.statusCode == 200 || response.statusCode == 204) {
return true; return true;
} else { } else {
print('Failed to delete ticket: ${response.statusCode} ${response.body}'); print(
'Failed to delete ticket: ${response.statusCode} ${response.body}',
);
return false; return false;
} }
} catch (e) { } catch (e) {
@@ -190,7 +194,8 @@ class ApiService {
/// Convert API ticket response to Report model /// Convert API ticket response to Report model
static Report _convertApiTicketToReport(Map<String, dynamic> data) { static Report _convertApiTicketToReport(Map<String, dynamic> data) {
final id = (data['id'] ?? data['ticket_id'] ?? '').toString(); final id = (data['id'] ?? data['ticket_id'] ?? '').toString();
final imageUrl = (data['image_url'] as String?) ?? final imageUrl =
(data['image_url'] as String?) ??
(data['image_path'] != null (data['image_path'] != null
? '$_uploadsUrl/${(data['image_path'] as String).split('/').last}' ? '$_uploadsUrl/${(data['image_path'] as String).split('/').last}'
: null); : null);
@@ -207,9 +212,19 @@ class ApiService {
lat: (data['latitude'] as num?)?.toDouble() ?? 0.0, lat: (data['latitude'] as num?)?.toDouble() ?? 0.0,
lng: (data['longitude'] as num?)?.toDouble() ?? 0.0, lng: (data['longitude'] as num?)?.toDouble() ?? 0.0,
), ),
createdAt: (data['created_at'] ?? data['createdAt'] ?? DateTime.now().toIso8601String()) as String, createdAt:
updatedAt: (data['updated_at'] ?? data['updatedAt'] ?? DateTime.now().toIso8601String()) as String, (data['created_at'] ??
deviceId: data['user_id'] != null ? data['user_id'].toString() : 'api-$id', data['createdAt'] ??
DateTime.now().toIso8601String())
as String,
updatedAt:
(data['updated_at'] ??
data['updatedAt'] ??
DateTime.now().toIso8601String())
as String,
deviceId: data['user_id'] != null
? data['user_id'].toString()
: 'api-$id',
notes: data['description'] as String?, notes: data['description'] as String?,
address: data['address'] as String?, address: data['address'] as String?,
submittedBy: data['user_name'] as String?, submittedBy: data['user_name'] as String?,

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Design tokens and themes for FixMate (Civic Pro Minimal) /// Design tokens and themes for CityPulse (Civic Pro Minimal)
class AppColors { class AppColors {
// Primary civic colors // Primary civic colors
static const Color civicBlue = Color(0xFF2563EB); static const Color civicBlue = Color(0xFF2563EB);

View File

@@ -1,5 +1,5 @@
name: citypulse name: citypulse
description: "FixMate - A citizen reporting app for community issues" description: "CityPulse - A citizen reporting app for community issues"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev

View File

@@ -6,7 +6,7 @@ import 'package:citypulse/app.dart';
void main() { void main() {
testWidgets('App builds MaterialApp', (WidgetTester tester) async { testWidgets('App builds MaterialApp', (WidgetTester tester) async {
await tester.pumpWidget(const FixMateApp()); await tester.pumpWidget(const CityPulseApp());
expect(find.byType(MaterialApp), findsOneWidget); expect(find.byType(MaterialApp), findsOneWidget);
}); });
} }