mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
new file: .gemini/settings.json
This commit is contained in:
parent
308b047be0
commit
7e275b020c
59 changed files with 5139 additions and 467 deletions
8
.gemini/settings.json
Normal file
8
.gemini/settings.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"myLocalServer": {
|
||||||
|
"command": "python my_mcp_server.py",
|
||||||
|
"cwd": "./mcp_server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -3,5 +3,6 @@
|
||||||
"builder.command": "npm run dev",
|
"builder.command": "npm run dev",
|
||||||
"builder.runDevServer": true,
|
"builder.runDevServer": true,
|
||||||
"builder.autoDetectDevServer": true,
|
"builder.autoDetectDevServer": true,
|
||||||
"builder.launchType": "desktop"
|
"builder.launchType": "desktop",
|
||||||
|
"chatgpt.openOnStartup": true
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +61,14 @@ sudo bash script/build-linux-iso.sh
|
||||||
- Size: ~2-4GB
|
- Size: ~2-4GB
|
||||||
- Checksum: `~/aethex-linux-build/AeThex-Linux-1.0.0-alpha-amd64.iso.sha256`
|
- Checksum: `~/aethex-linux-build/AeThex-Linux-1.0.0-alpha-amd64.iso.sha256`
|
||||||
|
|
||||||
|
### Step 1.5: Verify the ISO
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./script/verify-iso.sh -i ~/aethex-linux-build/AeThex-Linux-1.0.0-alpha-amd64.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
For strict verification and mount checks, see `docs/ISO_VERIFICATION.md`.
|
||||||
|
|
||||||
### Step 2: Test in Virtual Machine
|
### Step 2: Test in Virtual Machine
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
206
SUPABASE_INTEGRATION_COMPLETE.md
Normal file
206
SUPABASE_INTEGRATION_COMPLETE.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# Supabase Integration Complete ✅
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
Your Android mobile app now connects to **real Supabase data** instead of hardcoded mock arrays. All three main mobile pages have been updated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updated Pages
|
||||||
|
|
||||||
|
### 1. **Notifications Page** (`mobile-notifications.tsx`)
|
||||||
|
**Before:** Hardcoded array of 4 fake notifications
|
||||||
|
**After:** Live data from `notifications` table in Supabase
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Fetches user's notifications from Supabase on page load
|
||||||
|
- ✅ Mark notifications as read → updates database
|
||||||
|
- ✅ Delete notifications → removes from database
|
||||||
|
- ✅ Mark all as read → batch updates database
|
||||||
|
- ✅ Pull-to-refresh → re-fetches latest data
|
||||||
|
- ✅ Shows "Sign in to sync" message for logged-out users
|
||||||
|
- ✅ Real-time timestamps (just now, 2m ago, 1h ago, etc.)
|
||||||
|
|
||||||
|
**Schema:** Uses `notifications` table with fields: `id`, `user_id`, `type`, `title`, `message`, `read`, `created_at`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Projects Page** (`mobile-projects.tsx`)
|
||||||
|
**Before:** Hardcoded array of 4 fake projects
|
||||||
|
**After:** Live data from `projects` table in Supabase
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Fetches user's projects from Supabase
|
||||||
|
- ✅ Displays status (active, completed, archived)
|
||||||
|
- ✅ Shows progress bars based on real data
|
||||||
|
- ✅ Sorted by creation date (newest first)
|
||||||
|
- ✅ Empty state handling (no projects yet)
|
||||||
|
- ✅ Shows "Sign in to view projects" for logged-out users
|
||||||
|
|
||||||
|
**Schema:** Uses `projects` table with fields: `id`, `user_id`, `name`, `description`, `status`, `progress`, `created_at`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Messaging Page** (`mobile-messaging.tsx`)
|
||||||
|
**Before:** Hardcoded array of 4 fake messages
|
||||||
|
**After:** Live data from `messages` table in Supabase
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Fetches conversations from Supabase
|
||||||
|
- ✅ Shows messages sent TO or FROM the user
|
||||||
|
- ✅ Unread indicators for new messages
|
||||||
|
- ✅ Real-time timestamps
|
||||||
|
- ✅ Sorted by creation date (newest first)
|
||||||
|
- ✅ Shows "Sign in to view messages" for logged-out users
|
||||||
|
|
||||||
|
**Schema:** Uses `messages` table with fields: `id`, `sender_id`, `recipient_id`, `sender_name`, `content`, `read`, `created_at`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
1. User opens app → checks if logged in via `useAuth()` hook
|
||||||
|
2. **If logged out:** Shows demo/welcome message ("Sign in to sync data")
|
||||||
|
3. **If logged in:** Fetches real data from Supabase using `user.id`
|
||||||
|
|
||||||
|
### Data Fetching Pattern
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(50);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Mutations (Update/Delete)
|
||||||
|
```typescript
|
||||||
|
// Mark as read
|
||||||
|
await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.update({ read: true })
|
||||||
|
.eq('id', notificationId)
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.delete()
|
||||||
|
.eq('id', notificationId)
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing the Integration
|
||||||
|
|
||||||
|
### On Your Device
|
||||||
|
1. **Open the app** on your Samsung R5CW217D49H
|
||||||
|
2. **Sign in** with your Supabase account (if not already)
|
||||||
|
3. **Navigate to each page:**
|
||||||
|
- **Alerts/Notifications** → Should show real notifications from DB
|
||||||
|
- **Projects** → Should show real projects from DB
|
||||||
|
- **Messages** → Should show real conversations from DB
|
||||||
|
|
||||||
|
### Create Test Data (via Supabase Dashboard)
|
||||||
|
1. Go to: `https://kmdeisowhtsalsekkzqd.supabase.co`
|
||||||
|
2. Navigate to **Table Editor**
|
||||||
|
3. Insert test data:
|
||||||
|
|
||||||
|
**Example Notification:**
|
||||||
|
```sql
|
||||||
|
INSERT INTO notifications (user_id, type, title, message, read)
|
||||||
|
VALUES ('YOUR_USER_ID', 'success', 'Test Notification', 'This is from Supabase!', false);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Project:**
|
||||||
|
```sql
|
||||||
|
INSERT INTO projects (user_id, name, description, status, progress)
|
||||||
|
VALUES ('YOUR_USER_ID', 'My First Project', 'Testing Supabase sync', 'active', 50);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Pull to refresh** on mobile → New data should appear instantly!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's Still Mock Data
|
||||||
|
|
||||||
|
These pages still use hardcoded arrays (not yet connected to Supabase):
|
||||||
|
|
||||||
|
- **Modules/Code Gallery** (`mobile-modules.tsx`) - Would need a `modules` or `packages` table
|
||||||
|
- **Camera Page** (`mobile-camera.tsx`) - Uses native device APIs, doesn't need backend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Optional)
|
||||||
|
|
||||||
|
### Add Real-Time Subscriptions
|
||||||
|
Currently, data refreshes when you:
|
||||||
|
- Open the page
|
||||||
|
- Pull to refresh
|
||||||
|
|
||||||
|
To get **live updates** (instant sync when data changes):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Real-time notifications
|
||||||
|
useEffect(() => {
|
||||||
|
const channel = supabase
|
||||||
|
.channel('notifications')
|
||||||
|
.on('postgres_changes',
|
||||||
|
{
|
||||||
|
event: '*',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'notifications',
|
||||||
|
filter: `user_id=eq.${user.id}`
|
||||||
|
},
|
||||||
|
(payload) => {
|
||||||
|
console.log('Change detected:', payload);
|
||||||
|
fetchNotifications(); // Refresh data
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel);
|
||||||
|
};
|
||||||
|
}, [user]);
|
||||||
|
```
|
||||||
|
|
||||||
|
This would make notifications appear **instantly** when created from the web app or desktop app!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "No data showing"
|
||||||
|
- **Check:** Are you signed in? App shows demo data when logged out.
|
||||||
|
- **Check:** Does your Supabase user have any data in the tables?
|
||||||
|
- **Fix:** Insert test data via Supabase dashboard.
|
||||||
|
|
||||||
|
### "Sign in to sync" always shows
|
||||||
|
- **Check:** Is `useAuth()` returning a valid user object?
|
||||||
|
- **Check:** Open browser console → look for auth errors.
|
||||||
|
- **Fix:** Make sure Supabase auth is configured correctly.
|
||||||
|
|
||||||
|
### Data not updating after changes
|
||||||
|
- **Check:** Did you mark as read/delete but changes didn't persist?
|
||||||
|
- **Check:** Look for console errors during Supabase mutations.
|
||||||
|
- **Fix:** Verify Supabase Row Level Security (RLS) policies allow updates/deletes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Mobile notifications** → Live Supabase data
|
||||||
|
✅ **Mobile projects** → Live Supabase data
|
||||||
|
✅ **Mobile messages** → Live Supabase data
|
||||||
|
✅ **Pull-to-refresh** → Refetches from database
|
||||||
|
✅ **Mark as read/Delete** → Persists to database
|
||||||
|
✅ **Auth-aware** → Shows demo data when logged out
|
||||||
|
|
||||||
|
Your Android app is now a **production-ready** mobile client with full backend integration! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: ${new Date().toISOString()}*
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2025-12-27T06:38:05.234197200Z">
|
<DropdownSelection timestamp="2026-01-01T15:39:10.647645200Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R5CW217D49H" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=R5CW217D49H" />
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<targets>
|
<targets>
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\PCOEM\.android\avd\Medium_Phone.avd" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=R5CW217D49H" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</targets>
|
</targets>
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
flatDir{
|
// flatDir{
|
||||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
// dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -36,10 +36,13 @@ dependencies {
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:33.6.0')
|
||||||
|
implementation 'com.google.firebase:firebase-analytics'
|
||||||
|
implementation 'com.google.firebase:firebase-messaging'
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
// implementation project(':capacitor-cordova-android-plugins')
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'capacitor.build.gradle'
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:windowLayoutInDisplayCutoutMode="shortEdges">
|
android:windowLayoutInDisplayCutoutMode="shortEdges"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,79 @@
|
||||||
package com.aethex.os;
|
package com.aethex.os;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import androidx.core.view.WindowCompat;
|
import androidx.core.view.WindowCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.core.view.WindowInsetsControllerCompat;
|
import androidx.core.view.WindowInsetsControllerCompat;
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
import com.google.firebase.FirebaseApp;
|
||||||
|
import com.google.firebase.FirebaseOptions;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Enable edge-to-edge display
|
// Enable fullscreen immersive mode
|
||||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
enableImmersiveMode();
|
||||||
|
|
||||||
// Get window insets controller
|
// Ensure Firebase is ready before any Capacitor plugin requests it; stay resilient if config is missing
|
||||||
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
try {
|
||||||
|
if (FirebaseApp.getApps(this).isEmpty()) {
|
||||||
if (controller != null) {
|
FirebaseOptions options = null;
|
||||||
// Hide system bars (status bar and navigation bar)
|
try {
|
||||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
options = FirebaseOptions.fromResource(this);
|
||||||
|
} catch (Exception ignored) {
|
||||||
// Set behavior for when user swipes to show system bars
|
// No google-services.json resources, we'll fall back below
|
||||||
controller.setSystemBarsBehavior(
|
}
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
||||||
);
|
if (options != null) {
|
||||||
}
|
FirebaseApp.initializeApp(getApplicationContext(), options);
|
||||||
|
} else {
|
||||||
// Keep screen on for gaming/OS experience
|
// Minimal placeholder so Firebase-dependent plugins don't crash when config is absent
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
FirebaseOptions fallback = new FirebaseOptions.Builder()
|
||||||
}
|
.setApplicationId("1:000000000000:android:placeholder")
|
||||||
|
.setApiKey("FAKE_API_KEY")
|
||||||
|
.setProjectId("aethex-placeholder")
|
||||||
|
.build();
|
||||||
|
FirebaseApp.initializeApp(getApplicationContext(), fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w("MainActivity", "Firebase init skipped: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) {
|
||||||
|
enableImmersiveMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableImmersiveMode() {
|
||||||
|
View decorView = getWindow().getDecorView();
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|
||||||
|
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(getWindow(), decorView);
|
||||||
|
if (controller != null) {
|
||||||
|
// Hide both status and navigation bars
|
||||||
|
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||||
|
// Make them sticky so they stay hidden
|
||||||
|
controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional flags for fullscreen
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
getWindow().setFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||||
|
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
npx cap sync
|
npx cap syncnpx cap syncnpx cap sync npx cap sync
|
||||||
|
|
||||||
476
build-fixed.sh
Normal file
476
build-fixed.sh
Normal file
|
|
@ -0,0 +1,476 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# AeThex OS - Full Layered Architecture Builder
|
||||||
|
# Includes: Base OS + Wine Runtime + Linux Dev Tools + Mode Switching
|
||||||
|
|
||||||
|
WORK_DIR="${1:-.}"
|
||||||
|
BUILD_DIR="$WORK_DIR/aethex-linux-build"
|
||||||
|
ROOTFS_DIR="$BUILD_DIR/rootfs"
|
||||||
|
ISO_DIR="$BUILD_DIR/iso"
|
||||||
|
ISO_NAME="AeThex-OS-Full-amd64.iso"
|
||||||
|
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
echo " AeThex OS - Full Build"
|
||||||
|
echo " Layered Architecture: Base + Runtimes + Shell"
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo "[*] Build directory: $BUILD_DIR"
|
||||||
|
echo "[*] Target ISO: $ISO_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean and prepare
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
mkdir -p "$ROOTFS_DIR" "$ISO_DIR"/{casper,isolinux,boot/grub}
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
echo "[*] Checking dependencies..."
|
||||||
|
for cmd in debootstrap xorriso genisoimage mksquashfs grub-mkrescue; do
|
||||||
|
if ! command -v "$cmd" &> /dev/null; then
|
||||||
|
echo "[!] Missing: $cmd - installing..."
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq "$cmd" 2>&1 | tail -5
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ LAYER 1: Base OS (Ubuntu 22.04 LTS) - HP Compatible │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Bootstrapping Ubuntu 22.04 base system (older kernel 5.15)..."
|
||||||
|
echo " (debootstrap takes ~10-15 minutes...)"
|
||||||
|
debootstrap --arch=amd64 --variant=minbase jammy "$ROOTFS_DIR" http://archive.ubuntu.com/ubuntu/ 2>&1 | tail -20
|
||||||
|
|
||||||
|
echo "[+] Configuring base system..."
|
||||||
|
echo "aethex-os" > "$ROOTFS_DIR/etc/hostname"
|
||||||
|
cat > "$ROOTFS_DIR/etc/hosts" << 'EOF'
|
||||||
|
127.0.0.1 localhost
|
||||||
|
127.0.1.1 aethex-os
|
||||||
|
::1 localhost ip6-localhost ip6-loopback
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Mount filesystems for chroot
|
||||||
|
mount -t proc /proc "$ROOTFS_DIR/proc"
|
||||||
|
mount -t sysfs /sys "$ROOTFS_DIR/sys"
|
||||||
|
mount --bind /dev "$ROOTFS_DIR/dev"
|
||||||
|
mount -t devpts devpts "$ROOTFS_DIR/dev/pts"
|
||||||
|
|
||||||
|
echo "[+] Installing base packages..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Add universe repository
|
||||||
|
echo "deb http://archive.ubuntu.com/ubuntu jammy main restricted universe multiverse" > /etc/apt/sources.list
|
||||||
|
echo "deb http://archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse" >> /etc/apt/sources.list
|
||||||
|
echo "deb http://archive.ubuntu.com/ubuntu jammy-security main restricted universe multiverse" >> /etc/apt/sources.list
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
linux-image-generic linux-headers-generic \
|
||||||
|
casper \
|
||||||
|
grub-pc-bin grub-efi-amd64-bin grub-common xorriso \
|
||||||
|
systemd-sysv dbus \
|
||||||
|
network-manager wpasupplicant \
|
||||||
|
sudo curl wget git ca-certificates gnupg \
|
||||||
|
pipewire wireplumber \
|
||||||
|
xorg xserver-xorg-video-all \
|
||||||
|
xfce4 xfce4-goodies lightdm \
|
||||||
|
firefox thunar xfce4-terminal \
|
||||||
|
file-roller mousepad ristretto \
|
||||||
|
zenity notify-osd \
|
||||||
|
vim nano
|
||||||
|
|
||||||
|
apt-get clean
|
||||||
|
' 2>&1 | tail -50
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ LAYER 2a: Windows Runtime (Wine 9.0) │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Adding WineHQ repository..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Enable 32-bit architecture for Wine
|
||||||
|
dpkg --add-architecture i386
|
||||||
|
|
||||||
|
# Add WineHQ repository
|
||||||
|
mkdir -pm755 /etc/apt/keyrings
|
||||||
|
wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
|
||||||
|
wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/noble/winehq-noble.sources
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --install-recommends winehq-stable winetricks
|
||||||
|
|
||||||
|
# Install Windows fonts
|
||||||
|
apt-get install -y ttf-mscorefonts-installer
|
||||||
|
|
||||||
|
# Install DXVK for DirectX support
|
||||||
|
apt-get install -y dxvk
|
||||||
|
|
||||||
|
apt-get clean
|
||||||
|
' 2>&1 | tail -30
|
||||||
|
|
||||||
|
echo "[+] Setting up Wine runtime environment..."
|
||||||
|
mkdir -p "$ROOTFS_DIR/opt/aethex/runtimes/windows"
|
||||||
|
cp os/runtimes/windows/wine-launcher.sh "$ROOTFS_DIR/opt/aethex/runtimes/windows/"
|
||||||
|
chmod +x "$ROOTFS_DIR/opt/aethex/runtimes/windows/wine-launcher.sh"
|
||||||
|
|
||||||
|
# Create Wine file associations
|
||||||
|
cat > "$ROOTFS_DIR/usr/share/applications/wine-aethex.desktop" << 'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Windows Application (Wine)
|
||||||
|
Comment=Run Windows .exe files
|
||||||
|
Exec=/opt/aethex/runtimes/windows/wine-launcher.sh %f
|
||||||
|
Type=Application
|
||||||
|
MimeType=application/x-ms-dos-executable;application/x-msi;application/x-msdownload;
|
||||||
|
Icon=wine
|
||||||
|
Categories=Wine;
|
||||||
|
NoDisplay=false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chroot "$ROOTFS_DIR" update-desktop-database /usr/share/applications/ 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ LAYER 2b: Linux Dev Runtime (Docker + Tools) │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Installing Docker CE..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Add Docker repository
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" > /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
systemctl enable docker
|
||||||
|
|
||||||
|
apt-get clean
|
||||||
|
' 2>&1 | tail -20
|
||||||
|
|
||||||
|
echo "[+] Installing development tools..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Build essentials
|
||||||
|
apt-get install -y build-essential gcc g++ make cmake autoconf automake
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
apt-get install -y git git-lfs
|
||||||
|
|
||||||
|
# Node.js 20.x
|
||||||
|
mkdir -p /etc/apt/keyrings
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" > /etc/apt/sources.list.d/nodesource.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y nodejs
|
||||||
|
|
||||||
|
# Python
|
||||||
|
apt-get install -y python3 python3-pip python3-venv
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /etc/apt/keyrings/packages.microsoft.gpg
|
||||||
|
echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y code
|
||||||
|
|
||||||
|
apt-get clean
|
||||||
|
' 2>&1 | tail -30
|
||||||
|
|
||||||
|
echo "[+] Setting up dev runtime launchers..."
|
||||||
|
mkdir -p "$ROOTFS_DIR/opt/aethex/runtimes/linux-dev"
|
||||||
|
cp os/runtimes/linux-dev/dev-launcher.sh "$ROOTFS_DIR/opt/aethex/runtimes/linux-dev/"
|
||||||
|
chmod +x "$ROOTFS_DIR/opt/aethex/runtimes/linux-dev/dev-launcher.sh"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ LAYER 3: Shell & Mode Switching │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Installing runtime selector..."
|
||||||
|
mkdir -p "$ROOTFS_DIR/opt/aethex/shell/bin"
|
||||||
|
cp os/shell/bin/runtime-selector.sh "$ROOTFS_DIR/opt/aethex/shell/bin/"
|
||||||
|
chmod +x "$ROOTFS_DIR/opt/aethex/shell/bin/runtime-selector.sh"
|
||||||
|
|
||||||
|
# Install systemd service
|
||||||
|
cp os/shell/systemd/aethex-runtime-selector.service "$ROOTFS_DIR/etc/systemd/system/"
|
||||||
|
chroot "$ROOTFS_DIR" systemctl enable aethex-runtime-selector.service 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "[+] Installing Node.js for AeThex Mobile UI..."
|
||||||
|
# Already installed in dev tools section
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ AeThex Mobile App Integration │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Setting up AeThex Desktop application..."
|
||||||
|
|
||||||
|
# Build mobile app if possible
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
echo " Building AeThex mobile app..."
|
||||||
|
npm run build 2>&1 | tail -5 || echo " Build skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy app files
|
||||||
|
if [ -d "client" ] && [ -d "server" ]; then
|
||||||
|
echo " Copying AeThex Desktop files..."
|
||||||
|
mkdir -p "$ROOTFS_DIR/opt/aethex-desktop"
|
||||||
|
|
||||||
|
cp -r client "$ROOTFS_DIR/opt/aethex-desktop/"
|
||||||
|
cp -r server "$ROOTFS_DIR/opt/aethex-desktop/"
|
||||||
|
cp -r shared "$ROOTFS_DIR/opt/aethex-desktop/" 2>/dev/null || true
|
||||||
|
cp package*.json "$ROOTFS_DIR/opt/aethex-desktop/" 2>/dev/null || true
|
||||||
|
cp tsconfig.json "$ROOTFS_DIR/opt/aethex-desktop/" 2>/dev/null || true
|
||||||
|
cp vite.config.ts "$ROOTFS_DIR/opt/aethex-desktop/" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Copy built assets
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
cp -r dist "$ROOTFS_DIR/opt/aethex-desktop/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Installing dependencies..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c 'cd /opt/aethex-desktop && npm install --production --legacy-peer-deps' 2>&1 | tail -10 || true
|
||||||
|
else
|
||||||
|
echo " (client/server not found; skipping)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create systemd service
|
||||||
|
cat > "$ROOTFS_DIR/etc/systemd/system/aethex-mobile-server.service" << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=AeThex Mobile Server
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=aethex
|
||||||
|
WorkingDirectory=/opt/aethex-desktop
|
||||||
|
Environment="NODE_ENV=production"
|
||||||
|
Environment="PORT=5000"
|
||||||
|
ExecStart=/usr/bin/npm start
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chroot "$ROOTFS_DIR" systemctl enable aethex-mobile-server.service 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ User Configuration │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Creating aethex user..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
useradd -m -s /bin/bash -G sudo,docker aethex
|
||||||
|
echo "aethex:aethex" | chpasswd
|
||||||
|
echo "aethex ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||||
|
'
|
||||||
|
|
||||||
|
# Configure LightDM auto-login
|
||||||
|
mkdir -p "$ROOTFS_DIR/etc/lightdm"
|
||||||
|
cat > "$ROOTFS_DIR/etc/lightdm/lightdm.conf" << 'EOF'
|
||||||
|
[Seat:*]
|
||||||
|
autologin-user=aethex
|
||||||
|
autologin-user-timeout=0
|
||||||
|
user-session=xfce
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Auto-start Firefox kiosk
|
||||||
|
mkdir -p "$ROOTFS_DIR/home/aethex/.config/autostart"
|
||||||
|
cat > "$ROOTFS_DIR/home/aethex/.config/autostart/aethex-kiosk.desktop" << 'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=AeThex Mobile UI
|
||||||
|
Exec=sh -c "sleep 5 && firefox --kiosk http://localhost:5000"
|
||||||
|
Hidden=false
|
||||||
|
NoDisplay=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
Comment=Launch AeThex mobile interface in fullscreen
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chroot "$ROOTFS_DIR" chown -R aethex:aethex /home/aethex /opt/aethex-desktop 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
|
echo "│ ISO Packaging │"
|
||||||
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Regenerating initramfs with casper..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
KERNEL_VERSION=$(ls /boot/vmlinuz-* | sed "s|/boot/vmlinuz-||" | head -n 1)
|
||||||
|
echo " Rebuilding initramfs for kernel $KERNEL_VERSION with casper..."
|
||||||
|
update-initramfs -u -k "$KERNEL_VERSION"
|
||||||
|
' 2>&1 | tail -10
|
||||||
|
|
||||||
|
echo "[+] Extracting kernel and initrd..."
|
||||||
|
KERNEL="$(ls -1 $ROOTFS_DIR/boot/vmlinuz-* 2>/dev/null | head -n 1)"
|
||||||
|
INITRD="$(ls -1 $ROOTFS_DIR/boot/initrd.img-* 2>/dev/null | head -n 1)"
|
||||||
|
|
||||||
|
if [ -z "$KERNEL" ] || [ -z "$INITRD" ]; then
|
||||||
|
echo "[!] Kernel or initrd not found."
|
||||||
|
ls -la "$ROOTFS_DIR/boot/" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$KERNEL" "$ISO_DIR/casper/vmlinuz"
|
||||||
|
cp "$INITRD" "$ISO_DIR/casper/initrd.img"
|
||||||
|
echo "[✓] Kernel: $(basename "$KERNEL")"
|
||||||
|
echo "[✓] Initrd: $(basename "$INITRD")"
|
||||||
|
|
||||||
|
echo "[+] Verifying casper in initrd..."
|
||||||
|
if lsinitramfs "$ISO_DIR/casper/initrd.img" | grep -q "scripts/casper"; then
|
||||||
|
echo "[✓] Casper scripts found in initrd"
|
||||||
|
else
|
||||||
|
echo "[!] WARNING: Casper scripts NOT found in initrd!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unmount chroot filesystems
|
||||||
|
echo "[+] Unmounting chroot..."
|
||||||
|
umount -lf "$ROOTFS_DIR/dev/pts" 2>/dev/null || true
|
||||||
|
umount -lf "$ROOTFS_DIR/proc" 2>/dev/null || true
|
||||||
|
umount -lf "$ROOTFS_DIR/sys" 2>/dev/null || true
|
||||||
|
umount -lf "$ROOTFS_DIR/dev" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "[+] Creating SquashFS filesystem..."
|
||||||
|
echo " (compressing ~4-5GB system, takes 15-20 minutes...)"
|
||||||
|
mksquashfs "$ROOTFS_DIR" "$ISO_DIR/casper/filesystem.squashfs" -b 1048576 -comp xz -Xdict-size 100% 2>&1 | tail -5
|
||||||
|
|
||||||
|
echo "[+] Setting up BIOS boot (isolinux)..."
|
||||||
|
cat > "$ISO_DIR/isolinux/isolinux.cfg" << 'EOF'
|
||||||
|
PROMPT 0
|
||||||
|
TIMEOUT 50
|
||||||
|
DEFAULT linux
|
||||||
|
|
||||||
|
LABEL linux
|
||||||
|
MENU LABEL AeThex OS - Full Stack
|
||||||
|
KERNEL /casper/vmlinuz
|
||||||
|
APPEND initrd=/casper/initrd.img boot=casper quiet splash ---
|
||||||
|
|
||||||
|
LABEL safe
|
||||||
|
MENU LABEL AeThex OS - Safe Mode (No ACPI)
|
||||||
|
KERNEL /casper/vmlinuz
|
||||||
|
APPEND initrd=/casper/initrd.img boot=casper acpi=off noapic nomodeset ---
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cp /usr/lib/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || \
|
||||||
|
cp /usr/share/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || true
|
||||||
|
cp /usr/lib/syslinux/ldlinux.c32 "$ISO_DIR/isolinux/" 2>/dev/null || \
|
||||||
|
cp /usr/share/syslinux/ldlinux.c32 "$ISO_DIR/isolinux/" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "[+] Setting up UEFI boot (GRUB)..."
|
||||||
|
cat > "$ISO_DIR/boot/grub/grub.cfg" << 'EOF'
|
||||||
|
set timeout=10
|
||||||
|
set default=0
|
||||||
|
|
||||||
|
menuentry "AeThex OS - Full Stack" {
|
||||||
|
linux /casper/vmlinuz boot=casper quiet splash ---
|
||||||
|
initrd /casper/initrd.img
|
||||||
|
}
|
||||||
|
|
||||||
|
menuentry "AeThex OS - Safe Mode (No ACPI)" {
|
||||||
|
linux /casper/vmlinuz boot=casper acpi=off noapic nomodeset ---
|
||||||
|
initrd /casper/initrd.img
|
||||||
|
}
|
||||||
|
|
||||||
|
menuentry "AeThex OS - Debug Mode" {
|
||||||
|
linux /casper/vmlinuz boot=casper debug ignore_loglevel earlyprintk=vga ---
|
||||||
|
initrd /casper/initrd.img
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[+] Creating hybrid ISO..."
|
||||||
|
grub-mkrescue -o "$BUILD_DIR/$ISO_NAME" "$ISO_DIR" --verbose 2>&1 | tail -20
|
||||||
|
|
||||||
|
echo "[+] Computing SHA256 checksum..."
|
||||||
|
if [ -f "$BUILD_DIR/$ISO_NAME" ]; then
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
echo " ✓ ISO Build Complete!"
|
||||||
|
echo "═══════════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
ls -lh "$ISO_NAME" | awk '{print " Size: " $5}'
|
||||||
|
cat "$ISO_NAME.sha256" | awk '{print " SHA256: " $1}'
|
||||||
|
echo " Location: $BUILD_DIR/$ISO_NAME"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "[!] ISO creation failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Cleaning up rootfs..."
|
||||||
|
rm -rf "$ROOTFS_DIR"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"
|
||||||
|
echo "┃ AeThex OS - Full Stack Edition ┃"
|
||||||
|
echo "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
||||||
|
echo ""
|
||||||
|
echo "ARCHITECTURE:"
|
||||||
|
echo " ├── Base OS: Ubuntu 22.04 LTS (kernel 5.15 - better hardware compat)"
|
||||||
|
echo " ├── Runtime: Windows (Wine 9.0 + DXVK)"
|
||||||
|
echo " ├── Runtime: Linux Dev (Docker + VSCode + Node + Python + Rust)"
|
||||||
|
echo " ├── Live Boot: Casper (full live USB support)"
|
||||||
|
echo " └── Shell: Mode switching + file associations"
|
||||||
|
echo ""
|
||||||
|
echo "INSTALLED RUNTIMES:"
|
||||||
|
echo " • Wine 9.0 (run .exe files)"
|
||||||
|
echo " • Docker CE (containerized development)"
|
||||||
|
echo " • Node.js 20.x + npm"
|
||||||
|
echo " • Python 3 + pip"
|
||||||
|
echo " • Rust + Cargo"
|
||||||
|
echo " • VSCode"
|
||||||
|
echo " • Git + build tools"
|
||||||
|
echo ""
|
||||||
|
echo "DESKTOP ENVIRONMENT:"
|
||||||
|
echo " • Xfce 4.18 (lightweight, customizable)"
|
||||||
|
echo " • LightDM (auto-login as 'aethex')"
|
||||||
|
echo " • Firefox (kiosk mode for mobile UI)"
|
||||||
|
echo " • NetworkManager (WiFi/Ethernet)"
|
||||||
|
echo " • PipeWire (modern audio)"
|
||||||
|
echo ""
|
||||||
|
echo "AETHEX MOBILE APP:"
|
||||||
|
echo " • Server: http://localhost:5000"
|
||||||
|
echo " • Ingress-style hexagonal UI"
|
||||||
|
echo " • 18 Capacitor plugins"
|
||||||
|
echo " • Auto-launches on boot"
|
||||||
|
echo ""
|
||||||
|
echo "CREDENTIALS:"
|
||||||
|
echo " Username: aethex"
|
||||||
|
echo " Password: aethex"
|
||||||
|
echo " Sudo: passwordless"
|
||||||
|
echo ""
|
||||||
|
echo "FLASH TO USB:"
|
||||||
|
echo " sudo dd if=$BUILD_DIR/$ISO_NAME of=/dev/sdX bs=4M status=progress"
|
||||||
|
echo " (or use Rufus on Windows)"
|
||||||
|
echo ""
|
||||||
|
echo "[✓] Build complete! Flash to USB and boot."
|
||||||
|
echo ""
|
||||||
|
|
||||||
80
build-iso.ps1
Normal file
80
build-iso.ps1
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# AeThex OS - ISO Build Wrapper for Windows/WSL
|
||||||
|
# Automatically handles line ending conversion
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$BuildDir = "/home/mrpiglr/aethex-build",
|
||||||
|
[switch]$Clean,
|
||||||
|
[switch]$Background
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
|
Write-Host " AeThex OS - ISO Builder (Windows to WSL)" -ForegroundColor Cyan
|
||||||
|
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Convert line endings and copy to temp location
|
||||||
|
Write-Host "[*] Converting line endings (CRLF to LF)..." -ForegroundColor Yellow
|
||||||
|
$scriptPath = "script/build-linux-iso-full.sh"
|
||||||
|
$timestamp = Get-Date -Format 'yyyyMMddHHmmss'
|
||||||
|
$tempScript = "/tmp/aethex-build-$timestamp.sh"
|
||||||
|
|
||||||
|
if (!(Test-Path $scriptPath)) {
|
||||||
|
Write-Host "Error: $scriptPath not found" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read, convert, and pipe to WSL
|
||||||
|
$content = Get-Content $scriptPath -Raw
|
||||||
|
$unixContent = $content -replace "`r`n", "`n"
|
||||||
|
$unixContent | wsl bash -c "cat > $tempScript && chmod +x $tempScript"
|
||||||
|
|
||||||
|
Write-Host "[OK] Script prepared: $tempScript" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Clean previous build if requested
|
||||||
|
if ($Clean) {
|
||||||
|
Write-Host "[*] Cleaning previous build..." -ForegroundColor Yellow
|
||||||
|
wsl bash -c "sudo rm -rf $BuildDir/aethex-linux-build; mkdir -p $BuildDir"
|
||||||
|
Write-Host "[OK] Cleaned" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the build
|
||||||
|
$logFile = "$BuildDir/build-$timestamp.log"
|
||||||
|
|
||||||
|
if ($Background) {
|
||||||
|
Write-Host "[*] Starting build in background..." -ForegroundColor Yellow
|
||||||
|
Write-Host " Log: $logFile" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
wsl bash -c "nohup sudo bash $tempScript $BuildDir > $logFile 2>&1 &"
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
|
||||||
|
Write-Host "[*] Monitoring initial output:" -ForegroundColor Yellow
|
||||||
|
wsl bash -c "tail -30 $logFile 2>/dev/null || echo 'Waiting for log...'"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[i] Build running in background. Monitor with:" -ForegroundColor Cyan
|
||||||
|
Write-Host " wsl bash -c `"tail -f $logFile`"" -ForegroundColor Gray
|
||||||
|
Write-Host " or" -ForegroundColor Gray
|
||||||
|
Write-Host " wsl bash -c `"ps aux | grep build-linux-iso`"" -ForegroundColor Gray
|
||||||
|
} else {
|
||||||
|
Write-Host "[*] Starting build (30-60 min)..." -ForegroundColor Yellow
|
||||||
|
Write-Host " Log: $logFile" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
wsl bash -c "sudo bash $tempScript $BuildDir 2>&1 | tee $logFile"
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[OK] Build completed!" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[*] Checking for ISO..." -ForegroundColor Yellow
|
||||||
|
wsl bash -c "find $BuildDir -name '*.iso' -exec ls -lh {} \;"
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Build failed. Check log:" -ForegroundColor Red
|
||||||
|
Write-Host " wsl bash -c `"tail -100 $logFile`"" -ForegroundColor Gray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
build-output.txt
Normal file
BIN
build-output.txt
Normal file
Binary file not shown.
32
capacitor.config.json
Normal file
32
capacitor.config.json
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"appId": "com.aethex.os",
|
||||||
|
"appName": "AeThex OS",
|
||||||
|
"webDir": "dist/public",
|
||||||
|
"server": {
|
||||||
|
"androidScheme": "https",
|
||||||
|
"iosScheme": "https"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"SplashScreen": {
|
||||||
|
"launchShowDuration": 0,
|
||||||
|
"launchAutoHide": true,
|
||||||
|
"backgroundColor": "#000000",
|
||||||
|
"androidSplashResourceName": "splash",
|
||||||
|
"androidScaleType": "CENTER_CROP",
|
||||||
|
"showSpinner": false,
|
||||||
|
"androidSpinnerStyle": "large",
|
||||||
|
"iosSpinnerStyle": "small",
|
||||||
|
"spinnerColor": "#999999",
|
||||||
|
"splashFullScreen": true,
|
||||||
|
"splashImmersive": true
|
||||||
|
},
|
||||||
|
"PushNotifications": {
|
||||||
|
"presentationOptions": ["badge", "sound", "alert"]
|
||||||
|
},
|
||||||
|
"LocalNotifications": {
|
||||||
|
"smallIcon": "ic_stat_icon_config_sample",
|
||||||
|
"iconColor": "#488AFF",
|
||||||
|
"sound": "beep.wav"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,14 @@ const config: CapacitorConfig = {
|
||||||
splashFullScreen: true,
|
splashFullScreen: true,
|
||||||
splashImmersive: true
|
splashImmersive: true
|
||||||
},
|
},
|
||||||
|
StatusBar: {
|
||||||
|
style: 'DARK',
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
overlaysWebView: true
|
||||||
|
},
|
||||||
|
App: {
|
||||||
|
backButtonEnabled: true
|
||||||
|
},
|
||||||
PushNotifications: {
|
PushNotifications: {
|
||||||
presentationOptions: ['badge', 'sound', 'alert']
|
presentationOptions: ['badge', 'sound', 'alert']
|
||||||
},
|
},
|
||||||
|
|
@ -30,7 +38,13 @@ const config: CapacitorConfig = {
|
||||||
iconColor: '#488AFF',
|
iconColor: '#488AFF',
|
||||||
sound: 'beep.wav'
|
sound: 'beep.wav'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
allowMixedContent: true,
|
||||||
|
captureInput: true,
|
||||||
|
webContentsDebuggingEnabled: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
||||||
|
|
||||||
<title>AeThex OS - Operating System for the Metaverse</title>
|
<title>AeThex OS - Operating System for the Metaverse</title>
|
||||||
<meta name="description" content="AeThex is the Operating System for the Metaverse. Join the network, earn credentials, and build the future with Axiom, Codex, and Aegis." />
|
<meta name="description" content="AeThex is the Operating System for the Metaverse. Join the network, earn credentials, and build the future with Axiom, Codex, and Aegis." />
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="apple-touch-icon" href="/favicon.png" />
|
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="theme-color" content="#06B6D4" />
|
<meta name="theme-color" content="#10b981" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
@ -41,18 +41,36 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Orbitron:wght@400..900&family=Oxanium:wght@200..800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Orbitron:wght@400..900&family=Oxanium:wght@200..800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Mobile safe area support */
|
||||||
|
:root {
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||||
|
}
|
||||||
|
.safe-area-inset-top { padding-top: var(--safe-area-inset-top) !important; }
|
||||||
|
.safe-area-inset-bottom { padding-bottom: var(--safe-area-inset-bottom) !important; }
|
||||||
|
.safe-area-inset-left { padding-left: var(--safe-area-inset-left) !important; }
|
||||||
|
.safe-area-inset-right { padding-right: var(--safe-area-inset-right) !important; }
|
||||||
|
/* Disable pull-to-refresh on body */
|
||||||
|
body { overscroll-behavior-y: contain; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<!-- Unregister any existing service workers -->
|
<!-- Register service worker for PWA -->
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
window.addEventListener('load', () => {
|
||||||
for(let registration of registrations) {
|
navigator.serviceWorker.register('/sw.js')
|
||||||
registration.unregister();
|
.then(registration => {
|
||||||
console.log('Unregistered service worker:', registration.scope);
|
console.log('[SW] Registered:', registration.scope);
|
||||||
}
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log('[SW] Registration failed:', err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
48
client/public/aethex-logo.svg
Normal file
48
client/public/aethex-logo.svg
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#06b6d4;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#00d9ff;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Outer ring with spikes -->
|
||||||
|
<g filter="url(#glow)">
|
||||||
|
<!-- Top spike -->
|
||||||
|
<polygon points="100,20 110,60 90,60" fill="url(#grad1)" opacity="0.9"/>
|
||||||
|
<!-- Top-right spike -->
|
||||||
|
<polygon points="141.4,41.4 120,75 135,65" fill="url(#grad1)" opacity="0.85"/>
|
||||||
|
<!-- Right spike -->
|
||||||
|
<polygon points="180,100 140,110 140,90" fill="url(#grad1)" opacity="0.9"/>
|
||||||
|
<!-- Bottom-right spike -->
|
||||||
|
<polygon points="158.6,158.6 135,135 145,150" fill="url(#grad1)" opacity="0.85"/>
|
||||||
|
<!-- Bottom spike -->
|
||||||
|
<polygon points="100,180 90,140 110,140" fill="url(#grad1)" opacity="0.9"/>
|
||||||
|
<!-- Bottom-left spike -->
|
||||||
|
<polygon points="41.4,158.6 65,135 55,150" fill="url(#grad1)" opacity="0.85"/>
|
||||||
|
<!-- Left spike -->
|
||||||
|
<polygon points="20,100 60,90 60,110" fill="url(#grad1)" opacity="0.9"/>
|
||||||
|
<!-- Top-left spike -->
|
||||||
|
<polygon points="58.6,41.4 75,60 65,75" fill="url(#grad1)" opacity="0.85"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Inner dark ring -->
|
||||||
|
<circle cx="100" cy="100" r="75" fill="#0f172a" stroke="url(#grad1)" stroke-width="2" opacity="0.95"/>
|
||||||
|
|
||||||
|
<!-- Middle ring with glow -->
|
||||||
|
<circle cx="100" cy="100" r="55" fill="none" stroke="url(#grad1)" stroke-width="1.5" opacity="0.6" filter="url(#glow)"/>
|
||||||
|
|
||||||
|
<!-- Center diamond -->
|
||||||
|
<g filter="url(#glow)">
|
||||||
|
<polygon points="100,70 130,100 100,130 70,100" fill="url(#grad1)" opacity="0.9"/>
|
||||||
|
<polygon points="100,75 125,100 100,125 75,100" fill="#00ffff" opacity="0.7"/>
|
||||||
|
<circle cx="100" cy="100" r="8" fill="#ffffff" opacity="0.8"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -4,8 +4,8 @@
|
||||||
"description": "Join the AeThex Network. Earn credentials as a certified Metaverse Architect. Build the future with Axiom, Codex, and Aegis.",
|
"description": "Join the AeThex Network. Earn credentials as a certified Metaverse Architect. Build the future with Axiom, Codex, and Aegis.",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0F172A",
|
"background_color": "#000000",
|
||||||
"theme_color": "#06B6D4",
|
"theme_color": "#10b981",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
|
@ -13,10 +13,42 @@
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/favicon.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"categories": ["productivity", "utilities", "developer", "entertainment"],
|
"categories": ["productivity", "utilities", "developer", "entertainment"],
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"lang": "en-US"
|
"lang": "en-US",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "Mobile Dashboard",
|
||||||
|
"short_name": "Dashboard",
|
||||||
|
"description": "View your mobile dashboard",
|
||||||
|
"url": "/mobile",
|
||||||
|
"icons": [{ "src": "/favicon.png", "sizes": "192x192" }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Projects",
|
||||||
|
"short_name": "Projects",
|
||||||
|
"description": "Manage projects",
|
||||||
|
"url": "/hub/projects",
|
||||||
|
"icons": [{ "src": "/favicon.png", "sizes": "192x192" }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"share_target": {
|
||||||
|
"action": "/share",
|
||||||
|
"method": "POST",
|
||||||
|
"enctype": "multipart/form-data",
|
||||||
|
"params": {
|
||||||
|
"title": "title",
|
||||||
|
"text": "text",
|
||||||
|
"url": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,91 @@
|
||||||
// Service Worker disabled for development
|
// Service Worker for PWA functionality
|
||||||
// This file unregisters any existing service workers
|
const CACHE_NAME = 'aethex-v1';
|
||||||
|
const urlsToCache = [
|
||||||
|
'/',
|
||||||
|
'/mobile',
|
||||||
|
'/home',
|
||||||
|
'/manifest.json',
|
||||||
|
'/favicon.png'
|
||||||
|
];
|
||||||
|
|
||||||
self.addEventListener('install', () => {
|
// Install event - cache assets
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
console.log('[SW] Caching app shell');
|
||||||
|
return cache.addAll(urlsToCache);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[SW] Cache failed:', err);
|
||||||
|
})
|
||||||
|
);
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Activate event - clean up old caches
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
self.registration.unregister().then(() => {
|
caches.keys().then((cacheNames) => {
|
||||||
return self.clients.matchAll();
|
return Promise.all(
|
||||||
}).then((clients) => {
|
cacheNames.map((cacheName) => {
|
||||||
clients.forEach(client => client.navigate(client.url));
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
console.log('[SW] Deleting old cache:', cacheName);
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch event - network first, fallback to cache
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
if (event.request.method !== 'GET' || !event.request.url.startsWith('http')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match(event.request).then((cachedResponse) => {
|
||||||
|
return cachedResponse || (event.request.mode === 'navigate' ? caches.match('/') : null);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push notification
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
const options = {
|
||||||
|
body: event.data ? event.data.text() : 'New notification',
|
||||||
|
icon: '/favicon.png',
|
||||||
|
vibrate: [200, 100, 200],
|
||||||
|
actions: [
|
||||||
|
{ action: 'explore', title: 'View' },
|
||||||
|
{ action: 'close', title: 'Close' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification('AeThex OS', options)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notification click
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
if (event.action === 'explore') {
|
||||||
|
event.waitUntil(clients.openWindow('/mobile'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import Curriculum from "@/pages/curriculum";
|
||||||
import Login from "@/pages/login";
|
import Login from "@/pages/login";
|
||||||
import Admin from "@/pages/admin";
|
import Admin from "@/pages/admin";
|
||||||
import Pitch from "@/pages/pitch";
|
import Pitch from "@/pages/pitch";
|
||||||
|
import Builds from "@/pages/builds";
|
||||||
import AdminArchitects from "@/pages/admin-architects";
|
import AdminArchitects from "@/pages/admin-architects";
|
||||||
import AdminProjects from "@/pages/admin-projects";
|
import AdminProjects from "@/pages/admin-projects";
|
||||||
import AdminCredentials from "@/pages/admin-credentials";
|
import AdminCredentials from "@/pages/admin-credentials";
|
||||||
|
|
@ -40,12 +41,31 @@ import HubCodeGallery from "@/pages/hub/code-gallery";
|
||||||
import HubNotifications from "@/pages/hub/notifications";
|
import HubNotifications from "@/pages/hub/notifications";
|
||||||
import HubAnalytics from "@/pages/hub/analytics";
|
import HubAnalytics from "@/pages/hub/analytics";
|
||||||
import OsLink from "@/pages/os/link";
|
import OsLink from "@/pages/os/link";
|
||||||
|
import MobileDashboard from "@/pages/mobile-dashboard";
|
||||||
|
import SimpleMobileDashboard from "@/pages/mobile-simple";
|
||||||
|
import MobileCamera from "@/pages/mobile-camera";
|
||||||
|
import MobileNotifications from "@/pages/mobile-notifications";
|
||||||
|
import MobileProjects from "@/pages/mobile-projects";
|
||||||
|
import MobileMessaging from "@/pages/mobile-messaging";
|
||||||
|
import MobileModules from "@/pages/mobile-modules";
|
||||||
import { LabTerminalProvider } from "@/hooks/use-lab-terminal";
|
import { LabTerminalProvider } from "@/hooks/use-lab-terminal";
|
||||||
|
|
||||||
|
|
||||||
|
function HomeRoute() {
|
||||||
|
// On mobile devices, show the native mobile app
|
||||||
|
// On desktop/web, show the web OS
|
||||||
|
return <AeThexOS />;
|
||||||
|
}
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={AeThexOS} />
|
<Route path="/" component={HomeRoute} />
|
||||||
|
<Route path="/camera" component={MobileCamera} />
|
||||||
|
<Route path="/notifications" component={MobileNotifications} />
|
||||||
|
<Route path="/hub/projects" component={MobileProjects} />
|
||||||
|
<Route path="/hub/messaging" component={MobileMessaging} />
|
||||||
|
<Route path="/hub/code-gallery" component={MobileModules} />
|
||||||
<Route path="/home" component={Home} />
|
<Route path="/home" component={Home} />
|
||||||
<Route path="/passport" component={Passport} />
|
<Route path="/passport" component={Passport} />
|
||||||
<Route path="/achievements" component={Achievements} />
|
<Route path="/achievements" component={Achievements} />
|
||||||
|
|
@ -67,6 +87,7 @@ function Router() {
|
||||||
<Route path="/admin/activity">{() => <ProtectedRoute><AdminActivity /></ProtectedRoute>}</Route>
|
<Route path="/admin/activity">{() => <ProtectedRoute><AdminActivity /></ProtectedRoute>}</Route>
|
||||||
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
|
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
|
||||||
<Route path="/pitch" component={Pitch} />
|
<Route path="/pitch" component={Pitch} />
|
||||||
|
<Route path="/builds" component={Builds} />
|
||||||
<Route path="/os" component={AeThexOS} />
|
<Route path="/os" component={AeThexOS} />
|
||||||
<Route path="/os/link">{() => <ProtectedRoute><OsLink /></ProtectedRoute>}</Route>
|
<Route path="/os/link">{() => <ProtectedRoute><OsLink /></ProtectedRoute>}</Route>
|
||||||
<Route path="/network" component={Network} />
|
<Route path="/network" component={Network} />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { MessageCircle, X, Send, Bot, User, Loader2 } from "lucide-react";
|
import { MessageCircle, X, Send, Bot, User, Loader2 } from "lucide-react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
import { isMobile } from "@/lib/platform";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -63,7 +64,7 @@ export function Chatbot() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't render chatbot on the OS page - it has its own environment
|
// Don't render chatbot on the OS page - it has its own environment
|
||||||
if (location === "/os") {
|
if (location === "/os" || isMobile()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
59
client/src/components/Mobile3DScene.tsx
Normal file
59
client/src/components/Mobile3DScene.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Sparkles, Orbit } from "lucide-react";
|
||||||
|
import { isMobile } from "@/lib/platform";
|
||||||
|
|
||||||
|
export function Mobile3DScene() {
|
||||||
|
if (!isMobile()) return null;
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ title: "Spatial", accent: "from-cyan-500 to-emerald-500", delay: 0 },
|
||||||
|
{ title: "Realtime", accent: "from-purple-500 to-pink-500", delay: 0.08 },
|
||||||
|
{ title: "Secure", accent: "from-amber-500 to-orange-500", delay: 0.16 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative my-4 px-4">
|
||||||
|
<div className="overflow-hidden rounded-3xl border border-white/10 bg-gradient-to-br from-slate-950 via-slate-900 to-black p-4 shadow-2xl" style={{ perspective: "900px" }}>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(56,189,248,0.25),transparent_35%),radial-gradient(circle_at_80%_10%,rgba(168,85,247,0.2),transparent_30%),radial-gradient(circle_at_50%_80%,rgba(16,185,129,0.18),transparent_30%)] blur-3xl" />
|
||||||
|
|
||||||
|
<div className="relative flex items-center justify-between text-xs uppercase tracking-[0.2em] text-cyan-200 font-mono mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
<span>3D Surface</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-emerald-200">
|
||||||
|
<Orbit className="w-4 h-4" />
|
||||||
|
<span>Live</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3 transform-style-3d">
|
||||||
|
{cards.map((card, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={card.title}
|
||||||
|
initial={{ opacity: 0, rotateX: -15, rotateY: 8, z: -30 }}
|
||||||
|
animate={{ opacity: 1, rotateX: -6, rotateY: 6, z: -12 }}
|
||||||
|
transition={{ duration: 0.9, delay: card.delay, ease: "easeOut" }}
|
||||||
|
whileHover={{ rotateX: 0, rotateY: 0, z: 0, scale: 1.04 }}
|
||||||
|
className={`relative h-28 rounded-2xl bg-gradient-to-br ${card.accent} p-3 text-white shadow-xl shadow-black/40 border border-white/10`}
|
||||||
|
style={{ transformStyle: "preserve-3d" }}
|
||||||
|
>
|
||||||
|
<div className="text-[11px] font-semibold uppercase tracking-wide opacity-80">{card.title}</div>
|
||||||
|
<div className="text-[10px] text-white/80 mt-1">AeThex OS</div>
|
||||||
|
<div className="absolute bottom-2 right-2 text-[9px] font-mono text-white/70">3D</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="relative mt-4 rounded-2xl border border-cyan-400/40 bg-white/5 px-3 py-2 text-[11px] text-cyan-50 font-mono"
|
||||||
|
>
|
||||||
|
<span className="text-emerald-300 font-semibold">Immersive Mode:</span> Haptics + live network + native toasts are active.
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
client/src/components/MobileBottomNav.tsx
Normal file
76
client/src/components/MobileBottomNav.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Home, Package, MessageSquare, Settings, Camera, Zap } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export interface BottomTabItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
badge?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MobileBottomNavProps {
|
||||||
|
tabs: BottomTabItem[];
|
||||||
|
activeTab: string;
|
||||||
|
onTabChange: (tabId: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileBottomNav({
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
className = '',
|
||||||
|
}: MobileBottomNavProps) {
|
||||||
|
return (
|
||||||
|
<div className={`fixed bottom-0 left-0 right-0 h-16 bg-black/90 border-t border-emerald-500/30 z-40 safe-area-inset-bottom ${className}`}>
|
||||||
|
<div className="flex items-center justify-around h-full px-2">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => onTabChange(tab.id)}
|
||||||
|
className="flex flex-col items-center justify-center gap-1 flex-1 h-full relative group"
|
||||||
|
>
|
||||||
|
{activeTab === tab.id && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="tab-indicator"
|
||||||
|
className="absolute bottom-0 left-1/2 -translate-x-1/2 w-8 h-1 bg-emerald-400 rounded-t-full"
|
||||||
|
transition={{ type: 'spring', stiffness: 380, damping: 30 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'text-emerald-300'
|
||||||
|
: 'text-cyan-200 group-hover:text-emerald-200'
|
||||||
|
}`}>
|
||||||
|
{tab.icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={`text-[10px] font-mono uppercase tracking-wide transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'text-emerald-300'
|
||||||
|
: 'text-cyan-200 group-hover:text-emerald-200'
|
||||||
|
}`}>
|
||||||
|
{tab.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{tab.badge !== undefined && tab.badge > 0 && (
|
||||||
|
<div className="absolute top-1 right-2 w-4 h-4 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center">
|
||||||
|
{tab.badge > 9 ? '9+' : tab.badge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MOBILE_TABS: BottomTabItem[] = [
|
||||||
|
{ id: 'home', label: 'Home', icon: <Home className="w-5 h-5" /> },
|
||||||
|
{ id: 'projects', label: 'Projects', icon: <Package className="w-5 h-5" /> },
|
||||||
|
{ id: 'chat', label: 'Chat', icon: <MessageSquare className="w-5 h-5" /> },
|
||||||
|
{ id: 'camera', label: 'Camera', icon: <Camera className="w-5 h-5" /> },
|
||||||
|
{ id: 'settings', label: 'Settings', icon: <Settings className="w-5 h-5" /> },
|
||||||
|
];
|
||||||
104
client/src/components/MobileNativeBridge.tsx
Normal file
104
client/src/components/MobileNativeBridge.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Battery, BellRing, Smartphone, Wifi, WifiOff } from "lucide-react";
|
||||||
|
import { Device } from "@capacitor/device";
|
||||||
|
import { isMobile } from "@/lib/platform";
|
||||||
|
import { useNativeFeatures } from "@/hooks/use-native-features";
|
||||||
|
import { useHaptics } from "@/hooks/use-haptics";
|
||||||
|
|
||||||
|
export function MobileNativeBridge() {
|
||||||
|
const native = useNativeFeatures();
|
||||||
|
const haptics = useHaptics();
|
||||||
|
const prevNetwork = useRef(native.networkStatus.connected);
|
||||||
|
const [batteryLevel, setBatteryLevel] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Request notifications + prime native layer
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile()) return;
|
||||||
|
native.requestNotificationPermission();
|
||||||
|
const loadBattery = async () => {
|
||||||
|
try {
|
||||||
|
const info = await Device.getBatteryInfo();
|
||||||
|
if (typeof info.batteryLevel === "number") {
|
||||||
|
setBatteryLevel(Math.round(info.batteryLevel * 100));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("[MobileNativeBridge] battery info unavailable", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadBattery();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Network change feedback
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile()) return;
|
||||||
|
const current = native.networkStatus.connected;
|
||||||
|
if (prevNetwork.current !== current) {
|
||||||
|
const label = current ? "Online" : "Offline";
|
||||||
|
native.showToast(`Network: ${label}`);
|
||||||
|
haptics.notification(current ? "success" : "warning");
|
||||||
|
prevNetwork.current = current;
|
||||||
|
}
|
||||||
|
}, [native.networkStatus.connected, native, haptics]);
|
||||||
|
|
||||||
|
if (!isMobile()) return null;
|
||||||
|
|
||||||
|
const batteryText = batteryLevel !== null ? `${batteryLevel}%` : "--";
|
||||||
|
|
||||||
|
const handleNotify = async () => {
|
||||||
|
await native.sendLocalNotification("AeThex OS", "Synced with your device");
|
||||||
|
await haptics.notification("success");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToast = async () => {
|
||||||
|
await native.showToast("AeThex is live on-device");
|
||||||
|
await haptics.impact("light");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed top-4 right-4 z-40 flex flex-col gap-3 w-56 text-white drop-shadow-lg">
|
||||||
|
<div className="rounded-2xl border border-emerald-400/30 bg-black/70 backdrop-blur-xl p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-emerald-300 font-mono">
|
||||||
|
<Smartphone className="w-4 h-4" />
|
||||||
|
<span>Device Link</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-cyan-200">
|
||||||
|
{native.networkStatus.connected ? (
|
||||||
|
<Wifi className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="w-4 h-4 text-red-300" />
|
||||||
|
)}
|
||||||
|
<span className="font-semibold uppercase text-[11px]">
|
||||||
|
{native.networkStatus.connected ? "Online" : "Offline"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs text-cyan-100 mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Battery className="w-4 h-4" />
|
||||||
|
<span>Battery</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-semibold text-emerald-200">{batteryText}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleNotify}
|
||||||
|
className="flex items-center justify-center gap-2 rounded-lg bg-emerald-500/20 border border-emerald-400/50 px-3 py-2 text-xs font-semibold uppercase tracking-wide active:scale-95 transition"
|
||||||
|
>
|
||||||
|
<BellRing className="w-4 h-4" />
|
||||||
|
Notify
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleToast}
|
||||||
|
className="flex items-center justify-center gap-2 rounded-lg bg-cyan-500/20 border border-cyan-400/50 px-3 py-2 text-xs font-semibold uppercase tracking-wide active:scale-95 transition"
|
||||||
|
>
|
||||||
|
Toast
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
client/src/components/mobile/MobileHeader.tsx
Normal file
55
client/src/components/mobile/MobileHeader.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Home, ArrowLeft, Menu } from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
|
||||||
|
interface MobileHeaderProps {
|
||||||
|
title?: string;
|
||||||
|
onMenuClick?: () => void;
|
||||||
|
showBack?: boolean;
|
||||||
|
backPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileHeader({
|
||||||
|
title = 'AeThex OS',
|
||||||
|
onMenuClick,
|
||||||
|
showBack = true,
|
||||||
|
backPath = '/mobile'
|
||||||
|
}: MobileHeaderProps) {
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed top-0 left-0 right-0 z-50 bg-black/95 backdrop-blur-xl border-b border-emerald-500/30">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 safe-area-inset-top">
|
||||||
|
{showBack ? (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(backPath)}
|
||||||
|
className="p-3 rounded-full bg-emerald-600 active:bg-emerald-700 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="w-11" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h1 className="text-base font-bold text-white truncate max-w-[200px]">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{onMenuClick ? (
|
||||||
|
<button
|
||||||
|
onClick={onMenuClick}
|
||||||
|
className="p-3 rounded-full bg-gray-800 active:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/mobile')}
|
||||||
|
className="p-3 rounded-full bg-gray-800 active:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Home className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
client/src/components/mobile/PullToRefresh.tsx
Normal file
77
client/src/components/mobile/PullToRefresh.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PullToRefreshProps {
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PullToRefresh({
|
||||||
|
onRefresh,
|
||||||
|
children,
|
||||||
|
disabled = false
|
||||||
|
}: PullToRefreshProps) {
|
||||||
|
const [pullDistance, setPullDistance] = useState(0);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const startY = useRef(0);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback((e: TouchEvent) => {
|
||||||
|
if (disabled || isRefreshing || window.scrollY > 0) return;
|
||||||
|
startY.current = e.touches[0].clientY;
|
||||||
|
}, [disabled, isRefreshing]);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||||
|
if (disabled || isRefreshing || window.scrollY > 0) return;
|
||||||
|
const distance = e.touches[0].clientY - startY.current;
|
||||||
|
if (distance > 0) {
|
||||||
|
setPullDistance(Math.min(distance * 0.5, 100));
|
||||||
|
}
|
||||||
|
}, [disabled, isRefreshing]);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(async () => {
|
||||||
|
if (pullDistance > 60 && !isRefreshing) {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await onRefresh();
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPullDistance(0);
|
||||||
|
}, [pullDistance, isRefreshing, onRefresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.addEventListener('touchstart', handleTouchStart as any, { passive: true });
|
||||||
|
container.addEventListener('touchmove', handleTouchMove as any, { passive: true });
|
||||||
|
container.addEventListener('touchend', handleTouchEnd as any, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('touchstart', handleTouchStart as any);
|
||||||
|
container.removeEventListener('touchmove', handleTouchMove as any);
|
||||||
|
container.removeEventListener('touchend', handleTouchEnd as any);
|
||||||
|
};
|
||||||
|
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative">
|
||||||
|
{pullDistance > 0 && (
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center bg-gray-900 overflow-hidden"
|
||||||
|
style={{ height: `${pullDistance}px` }}
|
||||||
|
>
|
||||||
|
{isRefreshing ? (
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-400">Pull to refresh</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
client/src/components/mobile/SwipeableCard.tsx
Normal file
101
client/src/components/mobile/SwipeableCard.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Trash2, Archive } from 'lucide-react';
|
||||||
|
import { useHaptics } from '@/hooks/use-haptics';
|
||||||
|
|
||||||
|
interface SwipeableCardProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onSwipeLeft?: () => void;
|
||||||
|
onSwipeRight?: () => void;
|
||||||
|
leftAction?: { icon?: React.ReactNode; label?: string; color?: string };
|
||||||
|
rightAction?: { icon?: React.ReactNode; label?: string; color?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwipeableCard({
|
||||||
|
children,
|
||||||
|
onSwipeLeft,
|
||||||
|
onSwipeRight,
|
||||||
|
leftAction = { icon: <Trash2 className="w-5 h-5" />, label: 'Delete', color: 'bg-red-500' },
|
||||||
|
rightAction = { icon: <Archive className="w-5 h-5" />, label: 'Archive', color: 'bg-blue-500' }
|
||||||
|
}: SwipeableCardProps) {
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
const haptics = useHaptics();
|
||||||
|
let startX = 0;
|
||||||
|
let currentX = 0;
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
startX = e.touches[0].clientX;
|
||||||
|
currentX = startX;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: React.TouchEvent) => {
|
||||||
|
currentX = e.touches[0].clientX;
|
||||||
|
const diff = currentX - startX;
|
||||||
|
if (Math.abs(diff) > 10) {
|
||||||
|
setOffset(Math.max(-100, Math.min(100, diff)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
if (offset < -50 && onSwipeLeft) {
|
||||||
|
haptics.impact('medium');
|
||||||
|
onSwipeLeft();
|
||||||
|
} else if (offset > 50 && onSwipeRight) {
|
||||||
|
haptics.impact('medium');
|
||||||
|
onSwipeRight();
|
||||||
|
}
|
||||||
|
setOffset(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-gray-900 border border-gray-700 rounded-lg p-4"
|
||||||
|
style={{
|
||||||
|
transform: `translateX(${offset}px)`,
|
||||||
|
transition: offset === 0 ? 'transform 0.2s ease-out' : 'none'
|
||||||
|
}}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CardListProps<T> {
|
||||||
|
items: T[];
|
||||||
|
renderItem: (item: T, index: number) => React.ReactNode;
|
||||||
|
onItemSwipeLeft?: (item: T, index: number) => void;
|
||||||
|
onItemSwipeRight?: (item: T, index: number) => void;
|
||||||
|
keyExtractor: (item: T, index: number) => string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwipeableCardList<T>({
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
onItemSwipeLeft,
|
||||||
|
onItemSwipeRight,
|
||||||
|
keyExtractor,
|
||||||
|
emptyMessage = 'No items'
|
||||||
|
}: CardListProps<T>) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return <div className="text-center py-12 text-gray-500">{emptyMessage}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<SwipeableCard
|
||||||
|
key={keyExtractor(item, index)}
|
||||||
|
onSwipeLeft={onItemSwipeLeft ? () => onItemSwipeLeft(item, index) : undefined}
|
||||||
|
onSwipeRight={onItemSwipeRight ? () => onItemSwipeRight(item, index) : undefined}
|
||||||
|
>
|
||||||
|
{renderItem(item, index)}
|
||||||
|
</SwipeableCard>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
client/src/hooks/use-biometric-check.ts
Normal file
63
client/src/hooks/use-biometric-check.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
|
||||||
|
export interface BiometricCheckResult {
|
||||||
|
isAvailable: boolean;
|
||||||
|
biometryType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBiometricCheck() {
|
||||||
|
const [isCheckingBio, setIsCheckingBio] = useState(false);
|
||||||
|
const [bioAvailable, setBioAvailable] = useState(false);
|
||||||
|
const [bioType, setBioType] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const checkBiometric = useCallback(async (): Promise<BiometricCheckResult> => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
return { isAvailable: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCheckingBio(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Mock response for now
|
||||||
|
console.log('[Biometric] Plugin not available - using mock');
|
||||||
|
setBioAvailable(false);
|
||||||
|
return { isAvailable: false };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Biometric check error';
|
||||||
|
setError(message);
|
||||||
|
console.log('[Biometric Check] Error:', message);
|
||||||
|
return { isAvailable: false };
|
||||||
|
} finally {
|
||||||
|
setIsCheckingBio(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const authenticate = useCallback(async (reason: string = 'Authenticate') => {
|
||||||
|
if (!bioAvailable) {
|
||||||
|
setError('Biometric not available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mock auth for now
|
||||||
|
console.log('[Biometric] Mock authentication');
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Authentication failed';
|
||||||
|
setError(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [bioAvailable]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bioAvailable,
|
||||||
|
bioType,
|
||||||
|
isCheckingBio,
|
||||||
|
error,
|
||||||
|
checkBiometric,
|
||||||
|
authenticate,
|
||||||
|
};
|
||||||
|
}
|
||||||
104
client/src/hooks/use-device-camera.ts
Normal file
104
client/src/hooks/use-device-camera.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
|
||||||
|
export interface PhotoResult {
|
||||||
|
webPath?: string;
|
||||||
|
path?: string;
|
||||||
|
format?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeviceCamera() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [photo, setPhoto] = useState<PhotoResult | null>(null);
|
||||||
|
|
||||||
|
const takePhoto = useCallback(async () => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
setError('Camera only available on mobile');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const { Camera } = await import('@capacitor/camera');
|
||||||
|
const { CameraResultType, CameraSource } = await import('@capacitor/camera');
|
||||||
|
|
||||||
|
const image = await Camera.getPhoto({
|
||||||
|
quality: 90,
|
||||||
|
allowEditing: false,
|
||||||
|
resultType: CameraResultType.Uri,
|
||||||
|
source: CameraSource.Camera,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: PhotoResult = {
|
||||||
|
path: image.path || '',
|
||||||
|
webPath: image.webPath,
|
||||||
|
format: image.format,
|
||||||
|
};
|
||||||
|
|
||||||
|
setPhoto(result);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Camera error';
|
||||||
|
setError(message);
|
||||||
|
console.error('[Camera Error]', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const pickPhoto = useCallback(async () => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
setError('Photo picker only available on mobile');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const { Camera } = await import('@capacitor/camera');
|
||||||
|
const { CameraResultType, CameraSource } = await import('@capacitor/camera');
|
||||||
|
|
||||||
|
const image = await Camera.getPhoto({
|
||||||
|
quality: 90,
|
||||||
|
allowEditing: false,
|
||||||
|
resultType: CameraResultType.Uri,
|
||||||
|
source: CameraSource.Photos,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: PhotoResult = {
|
||||||
|
path: image.path || '',
|
||||||
|
webPath: image.webPath,
|
||||||
|
format: image.format,
|
||||||
|
};
|
||||||
|
|
||||||
|
setPhoto(result);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Photo picker error';
|
||||||
|
setError(message);
|
||||||
|
console.error('[Photo Picker Error]', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearPhoto = useCallback(() => {
|
||||||
|
setPhoto(null);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
takePhoto,
|
||||||
|
pickPhoto,
|
||||||
|
clearPhoto,
|
||||||
|
photo,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
58
client/src/hooks/use-device-contacts.ts
Normal file
58
client/src/hooks/use-device-contacts.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
|
||||||
|
export interface ContactResult {
|
||||||
|
contactId: string;
|
||||||
|
displayName?: string;
|
||||||
|
phoneNumbers?: Array<{ number?: string }>;
|
||||||
|
emails?: Array<{ address?: string }>;
|
||||||
|
photoThumbnail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeviceContacts() {
|
||||||
|
const [contacts, setContacts] = useState<ContactResult[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchContacts = useCallback(async () => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
setError('Contacts only available on mobile');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Mock contacts for now since plugin may not be installed
|
||||||
|
const mockContacts = [
|
||||||
|
{ contactId: '1', displayName: 'John Doe', phoneNumbers: [{ number: '555-0100' }], emails: [{ address: 'john@example.com' }] },
|
||||||
|
{ contactId: '2', displayName: 'Jane Smith', phoneNumbers: [{ number: '555-0101' }], emails: [{ address: 'jane@example.com' }] },
|
||||||
|
{ contactId: '3', displayName: 'Bob Wilson', phoneNumbers: [{ number: '555-0102' }], emails: [{ address: 'bob@example.com' }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
setContacts(mockContacts);
|
||||||
|
return mockContacts;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Contacts fetch error';
|
||||||
|
setError(message);
|
||||||
|
console.error('[Contacts Error]', err);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearContacts = useCallback(() => {
|
||||||
|
setContacts([]);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contacts,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
fetchContacts,
|
||||||
|
clearContacts,
|
||||||
|
};
|
||||||
|
}
|
||||||
81
client/src/hooks/use-device-file-picker.ts
Normal file
81
client/src/hooks/use-device-file-picker.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface FileResult {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
dataUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeviceFilePicker() {
|
||||||
|
const [file, setFile] = useState<FileResult | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const pickFile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
// Use web file input as fallback
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '*/*';
|
||||||
|
|
||||||
|
const filePromise = new Promise<File | null>((resolve) => {
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
resolve(target.files?.[0] || null);
|
||||||
|
};
|
||||||
|
input.oncancel = () => resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
|
||||||
|
const selectedFile = await filePromise;
|
||||||
|
if (!selectedFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file as data URL
|
||||||
|
const reader = new FileReader();
|
||||||
|
const dataUrlPromise = new Promise<string>((resolve, reject) => {
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
reader.readAsDataURL(selectedFile);
|
||||||
|
const dataUrl = await dataUrlPromise;
|
||||||
|
|
||||||
|
const result: FileResult = {
|
||||||
|
name: selectedFile.name,
|
||||||
|
size: selectedFile.size,
|
||||||
|
type: selectedFile.type,
|
||||||
|
dataUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
setFile(result);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'File picker error';
|
||||||
|
setError(message);
|
||||||
|
console.error('[File Picker Error]', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearFile = useCallback(() => {
|
||||||
|
setFile(null);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
pickFile,
|
||||||
|
clearFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
101
client/src/hooks/use-offline-sync.ts
Normal file
101
client/src/hooks/use-offline-sync.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface SyncQueueItem {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
action: string;
|
||||||
|
payload: any;
|
||||||
|
timestamp: number;
|
||||||
|
synced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOfflineSync() {
|
||||||
|
const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
|
||||||
|
const [syncQueue, setSyncQueue] = useState<SyncQueueItem[]>(() => {
|
||||||
|
if (typeof window === 'undefined') return [];
|
||||||
|
const saved = localStorage.getItem('aethex-sync-queue');
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
});
|
||||||
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
|
|
||||||
|
// Track online/offline
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOnline = () => setIsOnline(true);
|
||||||
|
const handleOffline = () => setIsOnline(false);
|
||||||
|
|
||||||
|
window.addEventListener('online', handleOnline);
|
||||||
|
window.addEventListener('offline', handleOffline);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', handleOnline);
|
||||||
|
window.removeEventListener('offline', handleOffline);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Persist queue to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('aethex-sync-queue', JSON.stringify(syncQueue));
|
||||||
|
}, [syncQueue]);
|
||||||
|
|
||||||
|
// Auto-sync when online
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOnline && syncQueue.length > 0) {
|
||||||
|
processSyncQueue();
|
||||||
|
}
|
||||||
|
}, [isOnline]);
|
||||||
|
|
||||||
|
const addToQueue = useCallback((type: string, action: string, payload: any) => {
|
||||||
|
const item: SyncQueueItem = {
|
||||||
|
id: `${type}-${Date.now()}`,
|
||||||
|
type,
|
||||||
|
action,
|
||||||
|
payload,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
synced: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
setSyncQueue(prev => [...prev, item]);
|
||||||
|
return item;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const processSyncQueue = useCallback(async () => {
|
||||||
|
if (isSyncing || !isOnline) return;
|
||||||
|
|
||||||
|
setIsSyncing(true);
|
||||||
|
const unsynced = syncQueue.filter(item => !item.synced);
|
||||||
|
|
||||||
|
for (const item of unsynced) {
|
||||||
|
try {
|
||||||
|
// Send to server
|
||||||
|
const res = await fetch('/api/sync', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(item),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setSyncQueue(prev =>
|
||||||
|
prev.map(i => i.id === item.id ? { ...i, synced: true } : i)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Sync Error]', item.id, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSyncing(false);
|
||||||
|
}, [syncQueue, isOnline, isSyncing]);
|
||||||
|
|
||||||
|
const clearSynced = useCallback(() => {
|
||||||
|
setSyncQueue(prev => prev.filter(item => !item.synced));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOnline,
|
||||||
|
syncQueue,
|
||||||
|
isSyncing,
|
||||||
|
addToQueue,
|
||||||
|
processSyncQueue,
|
||||||
|
clearSynced,
|
||||||
|
};
|
||||||
|
}
|
||||||
100
client/src/hooks/use-samsung-dex.ts
Normal file
100
client/src/hooks/use-samsung-dex.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { Device } from '@capacitor/device';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
|
||||||
|
export function useSamsungDex() {
|
||||||
|
const [isDexMode, setIsDexMode] = useState(false);
|
||||||
|
const [isLinkAvailable, setIsLinkAvailable] = useState(false);
|
||||||
|
const [deviceInfo, setDeviceInfo] = useState<any>(null);
|
||||||
|
const [isChecking, setIsChecking] = useState(false);
|
||||||
|
|
||||||
|
const checkDexMode = useCallback(async () => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
setIsLinkAvailable(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsChecking(true);
|
||||||
|
|
||||||
|
const info = await Device.getInfo();
|
||||||
|
setDeviceInfo(info);
|
||||||
|
|
||||||
|
const screenWidth = window.innerWidth;
|
||||||
|
const screenHeight = window.innerHeight;
|
||||||
|
const aspectRatio = screenWidth / screenHeight;
|
||||||
|
|
||||||
|
// Check for Samsung-specific APIs and user agent
|
||||||
|
const hasSamsungAPIs = !!(
|
||||||
|
(window as any).__SAMSUNG__ ||
|
||||||
|
(window as any).samsung ||
|
||||||
|
(window as any).SamsungLink
|
||||||
|
);
|
||||||
|
const isSamsung = navigator.userAgent.includes('SAMSUNG') ||
|
||||||
|
navigator.userAgent.includes('Samsung') ||
|
||||||
|
info.manufacturer?.toLowerCase().includes('samsung');
|
||||||
|
|
||||||
|
// SAMSUNG LINK FOR WINDOWS DETECTION
|
||||||
|
// Link mirrors phone screen to Windows PC - key indicators:
|
||||||
|
// 1. Samsung device
|
||||||
|
// 2. Desktop-sized viewport (>1024px width)
|
||||||
|
// 3. Desktop aspect ratio (landscape)
|
||||||
|
// 4. Navigator platform hints at Windows connection
|
||||||
|
const isWindowsLink = isSamsung &&
|
||||||
|
screenWidth >= 1024 &&
|
||||||
|
aspectRatio > 1.3 &&
|
||||||
|
(navigator.userAgent.includes('Windows') ||
|
||||||
|
(window as any).SamsungLink ||
|
||||||
|
hasSamsungAPIs);
|
||||||
|
|
||||||
|
// DeX (dock to monitor) vs Link (mirror to PC)
|
||||||
|
// DeX: 1920x1080+ desktop mode
|
||||||
|
// Link: 1024-1920 mirrored mode
|
||||||
|
const isDex = screenWidth >= 1920 && aspectRatio > 1.5 && aspectRatio < 1.8;
|
||||||
|
const isLink = screenWidth >= 1024 && screenWidth < 1920 && aspectRatio > 1.3;
|
||||||
|
|
||||||
|
setIsDexMode(isDex || isWindowsLink);
|
||||||
|
setIsLinkAvailable(isWindowsLink || isLink || hasSamsungAPIs);
|
||||||
|
|
||||||
|
console.log('🔗 [SAMSUNG WINDOWS LINK DETECTION]', {
|
||||||
|
screenWidth,
|
||||||
|
screenHeight,
|
||||||
|
aspectRatio,
|
||||||
|
isSamsung,
|
||||||
|
hasSamsungAPIs,
|
||||||
|
isDex,
|
||||||
|
isWindowsLink,
|
||||||
|
manufacturer: info.manufacturer,
|
||||||
|
platform: info.platform,
|
||||||
|
userAgent: navigator.userAgent.substring(0, 100),
|
||||||
|
});
|
||||||
|
|
||||||
|
return isDex || isWindowsLink;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('[DeX/Link Check] Error:', err);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsChecking(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkDexMode();
|
||||||
|
|
||||||
|
// Re-check on orientation change / window resize
|
||||||
|
const handleResize = () => {
|
||||||
|
checkDexMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [checkDexMode]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDexMode,
|
||||||
|
isLinkAvailable,
|
||||||
|
deviceInfo,
|
||||||
|
isChecking,
|
||||||
|
checkDexMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
118
client/src/hooks/use-touch-gestures.ts
Normal file
118
client/src/hooks/use-touch-gestures.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export interface SwipeHandlers {
|
||||||
|
onSwipeLeft?: () => void;
|
||||||
|
onSwipeRight?: () => void;
|
||||||
|
onSwipeUp?: () => void;
|
||||||
|
onSwipeDown?: () => void;
|
||||||
|
onPinch?: (scale: number) => void;
|
||||||
|
onDoubleTap?: () => void;
|
||||||
|
onLongPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTouchGestures(handlers: SwipeHandlers, elementRef?: React.RefObject<HTMLElement>) {
|
||||||
|
const touchStart = useRef<{ x: number; y: number; time: number } | null>(null);
|
||||||
|
const lastTap = useRef<number>(0);
|
||||||
|
const pinchStart = useRef<number>(0);
|
||||||
|
const longPressTimer = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = elementRef?.current || document;
|
||||||
|
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
touchStart.current = {
|
||||||
|
x: e.touches[0].clientX,
|
||||||
|
y: e.touches[0].clientY,
|
||||||
|
time: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start long press timer
|
||||||
|
if (handlers.onLongPress) {
|
||||||
|
longPressTimer.current = setTimeout(() => {
|
||||||
|
handlers.onLongPress?.();
|
||||||
|
touchStart.current = null;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
} else if (e.touches.length === 2 && handlers.onPinch) {
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
pinchStart.current = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: TouchEvent) => {
|
||||||
|
// Clear long press timer
|
||||||
|
if (longPressTimer.current) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
longPressTimer.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!touchStart.current || e.touches.length > 0) return;
|
||||||
|
|
||||||
|
const deltaX = e.changedTouches[0].clientX - touchStart.current.x;
|
||||||
|
const deltaY = e.changedTouches[0].clientY - touchStart.current.y;
|
||||||
|
const deltaTime = Date.now() - touchStart.current.time;
|
||||||
|
|
||||||
|
// Double tap detection
|
||||||
|
if (deltaTime < 300 && Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10) {
|
||||||
|
if (Date.now() - lastTap.current < 300 && handlers.onDoubleTap) {
|
||||||
|
handlers.onDoubleTap();
|
||||||
|
}
|
||||||
|
lastTap.current = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swipe detection (minimum 50px, max 300ms)
|
||||||
|
if (deltaTime < 300) {
|
||||||
|
if (Math.abs(deltaX) > 50 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||||
|
if (deltaX > 0 && handlers.onSwipeRight) {
|
||||||
|
handlers.onSwipeRight();
|
||||||
|
} else if (deltaX < 0 && handlers.onSwipeLeft) {
|
||||||
|
handlers.onSwipeLeft();
|
||||||
|
}
|
||||||
|
} else if (Math.abs(deltaY) > 50) {
|
||||||
|
if (deltaY > 0 && handlers.onSwipeDown) {
|
||||||
|
handlers.onSwipeDown();
|
||||||
|
} else if (deltaY < 0 && handlers.onSwipeUp) {
|
||||||
|
handlers.onSwipeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touchStart.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
// Cancel long press on move
|
||||||
|
if (longPressTimer.current && touchStart.current) {
|
||||||
|
const dx = e.touches[0].clientX - touchStart.current.x;
|
||||||
|
const dy = e.touches[0].clientY - touchStart.current.y;
|
||||||
|
if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
longPressTimer.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.touches.length === 2 && handlers.onPinch && pinchStart.current) {
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const scale = distance / pinchStart.current;
|
||||||
|
handlers.onPinch(scale);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
target.addEventListener('touchstart', handleTouchStart as any);
|
||||||
|
target.addEventListener('touchend', handleTouchEnd as any);
|
||||||
|
target.addEventListener('touchmove', handleTouchMove as any);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (longPressTimer.current) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
}
|
||||||
|
target.removeEventListener('touchstart', handleTouchStart as any);
|
||||||
|
target.removeEventListener('touchend', handleTouchEnd as any);
|
||||||
|
target.removeEventListener('touchmove', handleTouchMove as any);
|
||||||
|
};
|
||||||
|
}, [handlers, elementRef]);
|
||||||
|
}
|
||||||
80
client/src/lib/haptics.ts
Normal file
80
client/src/lib/haptics.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
export const haptics = {
|
||||||
|
/**
|
||||||
|
* Light impact for subtle feedback
|
||||||
|
*/
|
||||||
|
light: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate(10);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Medium impact for standard interactions
|
||||||
|
*/
|
||||||
|
medium: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate(20);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heavy impact for significant actions
|
||||||
|
*/
|
||||||
|
heavy: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate([30, 10, 30]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success notification pattern
|
||||||
|
*/
|
||||||
|
success: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate([10, 30, 10]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warning notification pattern
|
||||||
|
*/
|
||||||
|
warning: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate([20, 50, 20]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error notification pattern
|
||||||
|
*/
|
||||||
|
error: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate([50, 50, 50]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selection changed pattern
|
||||||
|
*/
|
||||||
|
selection: () => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate(5);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom vibration pattern
|
||||||
|
*/
|
||||||
|
pattern: (pattern: number | number[]) => {
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
navigator.vibrate(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device supports vibration
|
||||||
|
*/
|
||||||
|
export function isHapticSupported(): boolean {
|
||||||
|
return 'vibrate' in navigator;
|
||||||
|
}
|
||||||
246
client/src/pages/builds.tsx
Normal file
246
client/src/pages/builds.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
import { Link } from "wouter";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import gridBg from "@assets/generated_images/dark_digital_circuit_board_background.png";
|
||||||
|
|
||||||
|
export default function Builds() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background text-foreground font-mono relative overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-15 pointer-events-none z-0"
|
||||||
|
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: "cover" }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(234,179,8,0.18),transparent_60%)] opacity-80" />
|
||||||
|
<div className="absolute -top-40 right-0 h-72 w-72 rounded-full bg-secondary/20 blur-3xl" />
|
||||||
|
<div className="absolute -bottom-32 left-0 h-72 w-72 rounded-full bg-primary/20 blur-3xl" />
|
||||||
|
|
||||||
|
<div className="relative z-10 container mx-auto px-6 py-12 max-w-6xl">
|
||||||
|
<Link href="/">
|
||||||
|
<button className="text-muted-foreground hover:text-primary transition-colors flex items-center gap-2 uppercase text-xs tracking-widest mb-10">
|
||||||
|
Return Home
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<section className="mb-14">
|
||||||
|
<div className="inline-flex items-center gap-3 border border-primary/40 bg-primary/10 px-3 py-1 text-xs uppercase tracking-[0.3em] text-primary">
|
||||||
|
AeThex Builds
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-6xl font-display font-bold uppercase tracking-tight mt-6 mb-4">
|
||||||
|
Everything We Ship
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
||||||
|
AeThex OS is a multi-form build system: a live web OS, a bootable Linux ISO,
|
||||||
|
and an Android app that mirrors the OS runtime. This page is the single
|
||||||
|
source of truth for what exists, how to verify it, and how to build it.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<Button asChild>
|
||||||
|
<a href="#build-matrix">Build Matrix</a>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<a href="#verification">Verification</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="build-matrix" className="mb-16">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="h-px flex-1 bg-primary/30" />
|
||||||
|
<h2 className="text-sm uppercase tracking-[0.4em] text-primary">Build Matrix</h2>
|
||||||
|
<div className="h-px flex-1 bg-primary/30" />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="border border-primary/30 bg-card/60 p-6 relative">
|
||||||
|
<div className="absolute top-0 left-0 h-1 w-full bg-primary/60" />
|
||||||
|
<h3 className="font-display text-xl uppercase text-white mb-2">AeThex OS Linux ISO</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Bootable Linux build of the full AeThex OS desktop runtime. Designed for
|
||||||
|
verification, demos, and on-device deployments.
|
||||||
|
</p>
|
||||||
|
<div className="text-xs uppercase tracking-widest text-secondary mb-2">Outputs</div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
|
`aethex-linux-build/AeThex-Linux-amd64.iso` plus checksum.
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button variant="secondary" asChild>
|
||||||
|
<a href="#verification">Verify ISO</a>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<a href="#iso-build">Build Guide</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-secondary/30 bg-card/60 p-6 relative">
|
||||||
|
<div className="absolute top-0 left-0 h-1 w-full bg-secondary/60" />
|
||||||
|
<h3 className="font-display text-xl uppercase text-white mb-2">Android App</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Capacitor + Android Studio build for mobile deployment. Mirrors the OS UI
|
||||||
|
with native bridge hooks and mobile quick actions.
|
||||||
|
</p>
|
||||||
|
<div className="text-xs uppercase tracking-widest text-secondary mb-2">Status</div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
|
Build from source now. Distribution APK coming soon.
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button variant="secondary" asChild>
|
||||||
|
<a href="#android-build">Build Android</a>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
APK Coming Soon
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-white/10 bg-card/60 p-6 relative">
|
||||||
|
<div className="absolute top-0 left-0 h-1 w-full bg-white/30" />
|
||||||
|
<h3 className="font-display text-xl uppercase text-white mb-2">Web Client</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Primary OS surface for browsers. Ships continuously and powers live
|
||||||
|
demos, admin panels, and the runtime workspace.
|
||||||
|
</p>
|
||||||
|
<div className="text-xs uppercase tracking-widest text-secondary mb-2">Status</div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
|
Live, iterating daily. Can be built locally or deployed on demand.
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button variant="secondary" asChild>
|
||||||
|
<a href="#web-build">Build Web</a>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<a href="/">Launch OS</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-destructive/30 bg-card/60 p-6 relative">
|
||||||
|
<div className="absolute top-0 left-0 h-1 w-full bg-destructive/60" />
|
||||||
|
<h3 className="font-display text-xl uppercase text-white mb-2">iOS App</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Native shell for Apple hardware. This will mirror the Android runtime with
|
||||||
|
device-grade entitlements and mobile UX tuning.
|
||||||
|
</p>
|
||||||
|
<div className="text-xs uppercase tracking-widest text-secondary mb-2">Status</div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
|
Coming soon. Placeholder only until Apple hardware validation is complete.
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" disabled>
|
||||||
|
Coming Soon
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-16" id="verification">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="h-px flex-1 bg-secondary/30" />
|
||||||
|
<h2 className="text-sm uppercase tracking-[0.4em] text-secondary">Verification</h2>
|
||||||
|
<div className="h-px flex-1 bg-secondary/30" />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="border border-secondary/30 bg-card/60 p-6">
|
||||||
|
<h3 className="font-display text-lg uppercase text-white mb-3">ISO Integrity</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Run the verification script to confirm checksums and boot assets before
|
||||||
|
you ship or demo.
|
||||||
|
</p>
|
||||||
|
<pre className="bg-black/40 border border-white/10 p-4 text-xs text-muted-foreground overflow-x-auto">
|
||||||
|
{`./script/verify-iso.sh -i aethex-linux-build/AeThex-Linux-amd64.iso
|
||||||
|
./script/verify-iso.sh -i AeThex-OS-Full-amd64.iso --mount`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div className="border border-primary/30 bg-card/60 p-6">
|
||||||
|
<h3 className="font-display text-lg uppercase text-white mb-3">Artifact Checks</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Always keep the ISO next to its checksum file. If the SHA changes, rebuild.
|
||||||
|
</p>
|
||||||
|
<pre className="bg-black/40 border border-white/10 p-4 text-xs text-muted-foreground overflow-x-auto">
|
||||||
|
{`ls -lh aethex-linux-build/*.iso
|
||||||
|
sha256sum -c aethex-linux-build/*.sha256`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-16" id="iso-build">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="h-px flex-1 bg-primary/30" />
|
||||||
|
<h2 className="text-sm uppercase tracking-[0.4em] text-primary">ISO Build</h2>
|
||||||
|
<div className="h-px flex-1 bg-primary/30" />
|
||||||
|
</div>
|
||||||
|
<div className="border border-primary/30 bg-card/60 p-6">
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
The ISO build is scripted and reproducible. Use the full build script for a
|
||||||
|
complete OS image with the desktop runtime.
|
||||||
|
</p>
|
||||||
|
<pre className="bg-black/40 border border-white/10 p-4 text-xs text-muted-foreground overflow-x-auto">
|
||||||
|
{`sudo bash script/build-linux-iso.sh
|
||||||
|
# Output: aethex-linux-build/AeThex-Linux-amd64.iso`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-16" id="android-build">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="h-px flex-1 bg-secondary/30" />
|
||||||
|
<h2 className="text-sm uppercase tracking-[0.4em] text-secondary">Android Build</h2>
|
||||||
|
<div className="h-px flex-1 bg-secondary/30" />
|
||||||
|
</div>
|
||||||
|
<div className="border border-secondary/30 bg-card/60 p-6">
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Build the web bundle first, then sync to Capacitor and run the Gradle build.
|
||||||
|
</p>
|
||||||
|
<pre className="bg-black/40 border border-white/10 p-4 text-xs text-muted-foreground overflow-x-auto">
|
||||||
|
{`npm install
|
||||||
|
npm run build
|
||||||
|
npx cap sync android
|
||||||
|
cd android
|
||||||
|
./gradlew assembleDebug`}
|
||||||
|
</pre>
|
||||||
|
<p className="text-xs text-muted-foreground mt-4">
|
||||||
|
The APK output will be in `android/app/build/outputs/apk/`.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-16" id="web-build">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="h-px flex-1 bg-primary/30" />
|
||||||
|
<h2 className="text-sm uppercase tracking-[0.4em] text-primary">Web Client</h2>
|
||||||
|
<div className="h-px flex-1 bg-primary/30" />
|
||||||
|
</div>
|
||||||
|
<div className="border border-primary/30 bg-card/60 p-6">
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
The web OS runs on Vite + React. Use dev mode for iteration, build for production.
|
||||||
|
</p>
|
||||||
|
<pre className="bg-black/40 border border-white/10 p-4 text-xs text-muted-foreground overflow-x-auto">
|
||||||
|
{`npm install
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
npm run build`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-8">
|
||||||
|
<div className="border border-white/10 bg-card/60 p-6">
|
||||||
|
<h2 className="font-display text-2xl uppercase text-white mb-4">The Big Explainer</h2>
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed mb-4">
|
||||||
|
AeThex OS is not a single app. It is a multi-surface operating system that
|
||||||
|
treats the browser, desktop, and phone as interchangeable launch nodes for
|
||||||
|
the same living runtime. The web client is the living core. The Linux ISO
|
||||||
|
proves the OS can boot, isolate a runtime, and ship offline. The Android app
|
||||||
|
turns the OS into a pocket terminal with native bridge hooks. iOS is planned
|
||||||
|
to mirror the mobile stack once Apple hardware validation is complete.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||||
|
If you are an investor or partner, this is a platform bet: an OS that ships
|
||||||
|
across formats, built on a single codebase, with verifiable artifacts and
|
||||||
|
a real deployment pipeline. The deliverable is not hype. It is a build matrix
|
||||||
|
you can reproduce, verify, and ship.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Link } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network,
|
Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network,
|
||||||
|
|
@ -13,6 +13,7 @@ import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { startTutorial, hasCompletedTutorial, isActive } = useTutorial();
|
const { startTutorial, hasCompletedTutorial, isActive } = useTutorial();
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
const { data: metrics } = useQuery({
|
const { data: metrics } = useQuery({
|
||||||
queryKey: ["metrics"],
|
queryKey: ["metrics"],
|
||||||
|
|
@ -24,6 +25,13 @@ export default function Home() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground font-mono selection:bg-primary selection:text-background relative overflow-hidden">
|
<div className="min-h-screen bg-background text-foreground font-mono selection:bg-primary selection:text-background relative overflow-hidden">
|
||||||
|
{/* Mobile Back Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/mobile')}
|
||||||
|
className="fixed top-4 left-4 z-50 md:hidden p-3 rounded-full bg-emerald-600 active:bg-emerald-700 shadow-lg"
|
||||||
|
>
|
||||||
|
<ArrowRight className="w-6 h-6 rotate-180" />
|
||||||
|
</button>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 opacity-20 pointer-events-none z-0"
|
className="absolute inset-0 opacity-20 pointer-events-none z-0"
|
||||||
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
||||||
|
|
|
||||||
|
|
@ -78,54 +78,67 @@ export default function Marketplace() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
<div className="bg-slate-950 border-b border-slate-700 px-3 md:px-6 py-3 md:py-4 sticky top-0 z-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Link href="/">
|
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
|
||||||
<button className="text-slate-400 hover:text-white">
|
<Link href="/">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white shrink-0">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<h1 className="text-2xl font-bold text-white">Marketplace</h1>
|
</Link>
|
||||||
</div>
|
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
|
||||||
<div className="flex items-center gap-4">
|
</div>
|
||||||
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700">
|
<div className="flex items-center gap-2 md:gap-4 shrink-0">
|
||||||
<p className="text-sm text-slate-400">Balance</p>
|
<div className="bg-slate-800 px-2 md:px-4 py-1.5 md:py-2 rounded-lg border border-slate-700">
|
||||||
<p className="text-xl font-bold text-cyan-400">{balance} LP</p>
|
<p className="text-xs text-slate-400 hidden sm:block">Balance</p>
|
||||||
|
<p className="text-sm md:text-xl font-bold text-cyan-400">{balance} LP</p>
|
||||||
|
</div>
|
||||||
|
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-1 md:gap-2 text-xs md:text-sm px-2 md:px-4 h-8 md:h-10">
|
||||||
|
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
|
<span className="hidden sm:inline">Sell Item</span>
|
||||||
|
<span className="sm:hidden">Sell</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2">
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
Sell Item
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-3 md:p-6 max-w-7xl mx-auto">
|
||||||
{/* Category Tabs */}
|
{/* Category Tabs */}
|
||||||
<Tabs value={selectedCategory} onValueChange={setSelectedCategory} className="mb-6">
|
<Tabs value={selectedCategory} onValueChange={setSelectedCategory} className="mb-6">
|
||||||
<TabsList className="bg-slate-800 border-b border-slate-700">
|
<TabsList className="bg-slate-800 border-b border-slate-700 w-full overflow-x-auto flex-nowrap">
|
||||||
<TabsTrigger value="all" className="text-slate-300">
|
<TabsTrigger value="all" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||||
All Items
|
All Items
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="code" className="text-slate-300">
|
<TabsTrigger value="code" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||||
Code & Snippets
|
Code
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="achievement" className="text-slate-300">
|
<TabsTrigger value="achievement" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||||
Achievements
|
Achievements
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="service" className="text-slate-300">
|
<TabsTrigger value="service" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||||
Services
|
Services
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="credential" className="text-slate-300">
|
<TabsTrigger value="credential" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||||
Credentials
|
Credentials
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value={selectedCategory} className="mt-6">
|
<TabsContent value={selectedCategory} className="mt-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="w-8 h-8 text-cyan-400 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : filteredListings.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-slate-400">
|
||||||
|
<ShoppingCart className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||||
|
<p>No items found in this category</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-4">
|
||||||
{filteredListings.map((listing) => (
|
{filteredListings.map((listing) => (
|
||||||
<Card
|
<Card
|
||||||
key={listing.id}
|
key={listing.id}
|
||||||
className="bg-slate-800 border-slate-700 p-5 hover:border-cyan-500 transition-all group cursor-pointer"
|
className="bg-slate-800 border-slate-700 p-4 md:p-5 hover:border-cyan-500 transition-all group cursor-pointer active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
{/* Category Badge */}
|
{/* Category Badge */}
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
|
|
@ -139,13 +152,13 @@ export default function Marketplace() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h3 className="text-white font-bold mb-2 text-lg group-hover:text-cyan-400 transition-colors">
|
<h3 className="text-white font-bold mb-2 text-base md:text-lg group-hover:text-cyan-400 transition-colors line-clamp-2">
|
||||||
{listing.title}
|
{listing.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Seller Info */}
|
{/* Seller Info */}
|
||||||
<div className="mb-3 text-sm">
|
<div className="mb-3 text-sm">
|
||||||
<p className="text-slate-400">by {listing.seller}</p>
|
<p className="text-slate-400 truncate">by {listing.seller}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rating & Purchases */}
|
{/* Rating & Purchases */}
|
||||||
|
|
@ -158,30 +171,31 @@ export default function Marketplace() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price & Button */}
|
{/* Price & Button */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="text-2xl font-bold text-cyan-400">
|
<div className="text-xl md:text-2xl font-bold text-cyan-400">
|
||||||
{listing.price}
|
{listing.price}
|
||||||
<span className="text-sm text-slate-400 ml-1">LP</span>
|
<span className="text-xs md:text-sm text-slate-400 ml-1">LP</span>
|
||||||
</div>
|
</div>
|
||||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2 h-9 px-3">
|
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-1 md:gap-2 h-8 md:h-9 px-2 md:px-3 text-xs md:text-sm">
|
||||||
<ShoppingCart className="w-4 h-4" />
|
<ShoppingCart className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
Buy
|
Buy
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* Featured Section */}
|
{/* Featured Section */}
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<h2 className="text-2xl font-bold text-white mb-4">Featured Sellers</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-white mb-4">Featured Sellers</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 md:gap-4">
|
||||||
{["CodeMaster", "TechGuru", "AchievmentHunter"].map((seller) => (
|
{["CodeMaster", "TechGuru", "AchievmentHunter"].map((seller) => (
|
||||||
<Card
|
<Card
|
||||||
key={seller}
|
key={seller}
|
||||||
className="bg-slate-800 border-slate-700 p-4 hover:border-cyan-500 transition-colors"
|
className="bg-slate-800 border-slate-700 p-4 hover:border-cyan-500 transition-colors cursor-pointer active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="w-12 h-12 rounded-full bg-cyan-600 mx-auto mb-3"></div>
|
<div className="w-12 h-12 rounded-full bg-cyan-600 mx-auto mb-3"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
|
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
|
||||||
|
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
@ -97,8 +98,13 @@ export default function Messaging() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-slate-900">
|
<div className="h-screen flex flex-col bg-slate-900">
|
||||||
{/* Header */}
|
{/* Mobile Header */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
<div className="md:hidden">
|
||||||
|
<MobileHeader title="Messages" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Header */}
|
||||||
|
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="text-slate-400 hover:text-white">
|
<button className="text-slate-400 hover:text-white">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from "wouter";
|
import { Link, useLocation } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
|
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
|
||||||
|
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
@ -103,8 +104,13 @@ export default function Projects() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||||
{/* Header */}
|
{/* Mobile Header */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
<div className="md:hidden">
|
||||||
|
<MobileHeader title="Projects" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Header */}
|
||||||
|
<div className="hidden md:block bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="text-slate-400 hover:text-white transition-colors">
|
<button className="text-slate-400 hover:text-white transition-colors">
|
||||||
|
|
|
||||||
159
client/src/pages/mobile-camera.tsx
Normal file
159
client/src/pages/mobile-camera.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Camera, X, FlipHorizontal, Image as ImageIcon, Send } from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { useDeviceCamera } from '@/hooks/use-device-camera';
|
||||||
|
import { useNativeFeatures } from '@/hooks/use-native-features';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
|
||||||
|
export default function MobileCamera() {
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
const { photo, isLoading, error, takePhoto, pickPhoto } = useDeviceCamera();
|
||||||
|
const native = useNativeFeatures();
|
||||||
|
const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!isMobile()) {
|
||||||
|
navigate('/home');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTakePhoto = async () => {
|
||||||
|
haptics.medium();
|
||||||
|
const result = await takePhoto();
|
||||||
|
if (result) {
|
||||||
|
setCapturedImage(result.webPath || result.path || '');
|
||||||
|
native.showToast('Photo captured!');
|
||||||
|
haptics.success();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickFromGallery = async () => {
|
||||||
|
haptics.light();
|
||||||
|
const result = await pickPhoto();
|
||||||
|
if (result) {
|
||||||
|
setCapturedImage(result.webPath || result.path || '');
|
||||||
|
native.showToast('Photo selected!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
if (capturedImage) {
|
||||||
|
haptics.medium();
|
||||||
|
await native.shareText('Check out my photo!', 'Photo from AeThex OS');
|
||||||
|
haptics.success();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
haptics.light();
|
||||||
|
navigate('/mobile');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="fixed top-0 left-0 right-0 z-50 bg-black/90 backdrop-blur-xl border-b border-emerald-500/30">
|
||||||
|
<div className="flex items-center justify-between px-4 py-4 safe-area-inset-top">
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="p-3 rounded-full bg-emerald-600 active:bg-emerald-700 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<h1 className="text-xl font-bold text-white">
|
||||||
|
Camera
|
||||||
|
</h1>
|
||||||
|
{capturedImage ? (
|
||||||
|
<button
|
||||||
|
onClick={handleShare}
|
||||||
|
className="p-3 rounded-full bg-blue-600 active:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Send className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="w-12" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="pt-20 pb-32 px-4">
|
||||||
|
{capturedImage ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={capturedImage}
|
||||||
|
alt="Captured"
|
||||||
|
className="w-full rounded-2xl shadow-2xl"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCapturedImage(null);
|
||||||
|
haptics.light();
|
||||||
|
}}
|
||||||
|
className="flex-1 py-3 bg-gray-800 hover:bg-gray-700 rounded-xl font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
Retake
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleShare}
|
||||||
|
className="flex-1 py-3 bg-gradient-to-r from-emerald-500 to-cyan-500 hover:from-emerald-400 hover:to-cyan-400 rounded-xl font-semibold transition-colors"
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Camera placeholder */}
|
||||||
|
<div className="aspect-[3/4] bg-gradient-to-br from-gray-900 to-gray-800 rounded-2xl flex items-center justify-center border-2 border-dashed border-emerald-500/30">
|
||||||
|
<div className="text-center">
|
||||||
|
<Camera className="w-16 h-16 mx-auto mb-4 text-emerald-400" />
|
||||||
|
<p className="text-sm text-gray-400">Tap button below to capture</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-900/30 border border-red-500/50 rounded-xl p-4">
|
||||||
|
<p className="text-sm text-red-300">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Camera controls */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={handlePickFromGallery}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1 py-4 bg-gray-800 hover:bg-gray-700 rounded-xl font-semibold transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<ImageIcon className="w-5 h-5" />
|
||||||
|
Gallery
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleTakePhoto}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1 py-4 bg-gradient-to-r from-emerald-500 to-cyan-500 hover:from-emerald-400 hover:to-cyan-400 rounded-xl font-semibold transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Camera className="w-5 h-5" />
|
||||||
|
{isLoading ? 'Capturing...' : 'Capture'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="bg-gradient-to-r from-cyan-900/30 to-emerald-900/30 border border-cyan-500/30 rounded-xl p-4">
|
||||||
|
<p className="text-xs text-cyan-200 font-mono">
|
||||||
|
📸 <strong>Camera Access:</strong> This feature uses your device camera.
|
||||||
|
Grant permission when prompted to capture photos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
285
client/src/pages/mobile-dashboard.tsx
Normal file
285
client/src/pages/mobile-dashboard.tsx
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Home, Camera, Bell, Settings, Zap, Battery, Wifi,
|
||||||
|
MessageSquare, Package, User, CheckCircle, Star, Award
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { PullToRefresh } from '@/components/mobile/PullToRefresh';
|
||||||
|
import { SwipeableCardList } from '@/components/mobile/SwipeableCard';
|
||||||
|
import { MobileBottomNav, DEFAULT_MOBILE_TABS } from '@/components/MobileBottomNav';
|
||||||
|
import { MobileNativeBridge } from '@/components/MobileNativeBridge';
|
||||||
|
import { MobileQuickActions } from '@/components/MobileQuickActions';
|
||||||
|
import { useNativeFeatures } from '@/hooks/use-native-features';
|
||||||
|
import { useTouchGestures } from '@/hooks/use-touch-gestures';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
|
||||||
|
interface DashboardCard {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
badge?: number;
|
||||||
|
action: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileDashboard() {
|
||||||
|
const [location, navigate] = useLocation();
|
||||||
|
const [activeTab, setActiveTab] = useState('home');
|
||||||
|
const [showQuickActions, setShowQuickActions] = useState(false);
|
||||||
|
const native = useNativeFeatures();
|
||||||
|
|
||||||
|
// Redirect non-mobile users
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
navigate('/home');
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const [cards, setCards] = useState<DashboardCard[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Projects',
|
||||||
|
description: 'View and manage your active projects',
|
||||||
|
icon: <Package className="w-6 h-6" />,
|
||||||
|
color: 'from-blue-500 to-cyan-500',
|
||||||
|
badge: 3,
|
||||||
|
action: () => navigate('/hub/projects')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Messages',
|
||||||
|
description: 'Check your recent conversations',
|
||||||
|
icon: <MessageSquare className="w-6 h-6" />,
|
||||||
|
color: 'from-purple-500 to-pink-500',
|
||||||
|
badge: 5,
|
||||||
|
action: () => navigate('/hub/messaging')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Achievements',
|
||||||
|
description: 'Track your progress and milestones',
|
||||||
|
icon: <Award className="w-6 h-6" />,
|
||||||
|
color: 'from-yellow-500 to-orange-500',
|
||||||
|
action: () => navigate('/achievements')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: 'Network',
|
||||||
|
description: 'Connect with other architects',
|
||||||
|
icon: <User className="w-6 h-6" />,
|
||||||
|
color: 'from-green-500 to-emerald-500',
|
||||||
|
action: () => navigate('/network')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
title: 'Notifications',
|
||||||
|
description: 'View all your notifications',
|
||||||
|
icon: <Bell className="w-6 h-6" />,
|
||||||
|
color: 'from-red-500 to-pink-500',
|
||||||
|
badge: 2,
|
||||||
|
action: () => navigate('/hub/notifications')
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Redirect non-mobile users
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
navigate('/home');
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
haptics.light();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
native.showToast('Dashboard refreshed!');
|
||||||
|
haptics.success();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardSwipeLeft = (card: DashboardCard) => {
|
||||||
|
haptics.medium();
|
||||||
|
setCards(prev => prev.filter(c => c.id !== card.id));
|
||||||
|
native.showToast(`${card.title} removed`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardSwipeRight = (card: DashboardCard) => {
|
||||||
|
haptics.medium();
|
||||||
|
native.showToast(`${card.title} favorited`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardTap = (card: DashboardCard) => {
|
||||||
|
haptics.light();
|
||||||
|
card.action();
|
||||||
|
};
|
||||||
|
|
||||||
|
useTouchGestures({
|
||||||
|
onSwipeDown: () => {
|
||||||
|
setShowQuickActions(true);
|
||||||
|
haptics.light();
|
||||||
|
},
|
||||||
|
onSwipeUp: () => {
|
||||||
|
setShowQuickActions(false);
|
||||||
|
haptics.light();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isMobile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white pb-20">
|
||||||
|
{/* Native bridge status */}
|
||||||
|
<MobileNativeBridge />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 z-30 bg-black/80 backdrop-blur-xl border-b border-emerald-500/30">
|
||||||
|
<div className="safe-area-inset-top px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-emerald-300 font-mono">
|
||||||
|
AETHEX MOBILE
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-cyan-200 font-mono uppercase tracking-wide">
|
||||||
|
Device Dashboard
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowQuickActions(!showQuickActions);
|
||||||
|
haptics.light();
|
||||||
|
}}
|
||||||
|
className="p-3 rounded-full bg-gradient-to-r from-emerald-500 to-cyan-500 hover:from-emerald-400 hover:to-cyan-400 transition-all"
|
||||||
|
>
|
||||||
|
<Zap className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content with pull-to-refresh */}
|
||||||
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
|
<div className="px-4 py-6 space-y-6">
|
||||||
|
{/* Quick stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<StatCard
|
||||||
|
icon={<Star className="w-5 h-5" />}
|
||||||
|
label="Points"
|
||||||
|
value="1,234"
|
||||||
|
color="from-yellow-500 to-orange-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={<CheckCircle className="w-5 h-5" />}
|
||||||
|
label="Tasks"
|
||||||
|
value="42"
|
||||||
|
color="from-green-500 to-emerald-500"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={<Zap className="w-5 h-5" />}
|
||||||
|
label="Streak"
|
||||||
|
value="7d"
|
||||||
|
color="from-purple-500 to-pink-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swipeable cards */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-emerald-300 mb-3 font-mono uppercase tracking-wide">
|
||||||
|
Quick Access
|
||||||
|
</h2>
|
||||||
|
<SwipeableCardList
|
||||||
|
items={cards}
|
||||||
|
keyExtractor={(card) => card.id}
|
||||||
|
onItemSwipeLeft={handleCardSwipeLeft}
|
||||||
|
onItemSwipeRight={handleCardSwipeRight}
|
||||||
|
renderItem={(card) => (
|
||||||
|
<div
|
||||||
|
onClick={() => handleCardTap(card)}
|
||||||
|
className="bg-gradient-to-r from-gray-900 to-gray-800 border border-emerald-500/20 rounded-xl p-4 cursor-pointer active:scale-95 transition-transform"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className={`p-3 rounded-lg bg-gradient-to-r ${card.color}`}>
|
||||||
|
{card.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<h3 className="font-semibold text-white">{card.title}</h3>
|
||||||
|
{card.badge && (
|
||||||
|
<span className="px-2 py-1 text-xs font-bold bg-red-500 text-white rounded-full">
|
||||||
|
{card.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400">{card.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
emptyMessage="No quick access cards available"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Helpful tip */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="bg-gradient-to-r from-cyan-900/30 to-emerald-900/30 border border-cyan-500/30 rounded-xl p-4"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-cyan-200 font-mono">
|
||||||
|
💡 <strong>TIP:</strong> Swipe cards left to remove, right to favorite.
|
||||||
|
Pull down to refresh. Swipe down from top for quick actions.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</PullToRefresh>
|
||||||
|
|
||||||
|
{/* Quick actions overlay */}
|
||||||
|
{showQuickActions && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -50 }}
|
||||||
|
className="fixed top-20 left-4 right-4 z-40"
|
||||||
|
>
|
||||||
|
<MobileQuickActions />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bottom navigation */}
|
||||||
|
<MobileBottomNav
|
||||||
|
tabs={DEFAULT_MOBILE_TABS}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
haptics.selection();
|
||||||
|
navigate(tabId === 'home' ? '/mobile' : `/${tabId}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
color
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 border border-emerald-500/20 rounded-xl p-3">
|
||||||
|
<div className={`inline-flex p-2 rounded-lg bg-gradient-to-r ${color} mb-2`}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-1">{value}</div>
|
||||||
|
<div className="text-[10px] text-gray-400 uppercase tracking-wide">{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
178
client/src/pages/mobile-messaging.tsx
Normal file
178
client/src/pages/mobile-messaging.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Send, MessageCircle } from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
sender: string;
|
||||||
|
text: string;
|
||||||
|
timestamp: string;
|
||||||
|
unread?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileMessaging() {
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
|
||||||
|
const fetchMessages = async () => {
|
||||||
|
try {
|
||||||
|
if (!user) {
|
||||||
|
setMessages([
|
||||||
|
{
|
||||||
|
id: 'demo',
|
||||||
|
sender: 'AeThex Team',
|
||||||
|
text: 'Sign in to view your messages',
|
||||||
|
timestamp: 'now',
|
||||||
|
unread: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for messages where user is recipient or sender
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('messages')
|
||||||
|
.select('*')
|
||||||
|
.or(`sender_id.eq.${user.id},recipient_id.eq.${user.id}`)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(50);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
const mapped = data.map(m => ({
|
||||||
|
id: m.id.toString(),
|
||||||
|
sender: m.sender_name || 'Unknown',
|
||||||
|
text: m.content || '',
|
||||||
|
timestamp: formatTime(m.created_at),
|
||||||
|
unread: m.recipient_id === user.id && !m.read,
|
||||||
|
created_at: m.created_at
|
||||||
|
}));
|
||||||
|
setMessages(mapped);
|
||||||
|
} else {
|
||||||
|
setMessages([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching messages:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const now = new Date();
|
||||||
|
const created = new Date(timestamp);
|
||||||
|
const diffMs = now.getTime() - created.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'just now';
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMessages();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const unreadCount = messages.filter(m => m.unread).length;
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (newMessage.trim()) {
|
||||||
|
haptics.light();
|
||||||
|
setNewMessage('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 z-30 bg-black/80 backdrop-blur-xl border-b border-cyan-500/20">
|
||||||
|
<div className="px-4 py-4 safe-area-inset-top">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/');
|
||||||
|
haptics.light();
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg bg-cyan-600/20 hover:bg-cyan-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6 text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black text-white uppercase tracking-wider">
|
||||||
|
MESSAGES
|
||||||
|
</h1>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<p className="text-xs text-cyan-300 font-mono">{unreadCount} unread</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages List */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-3">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<button
|
||||||
|
key={message.id}
|
||||||
|
onClick={() => haptics.light()}
|
||||||
|
className={`w-full text-left rounded-lg p-4 border transition-all ${
|
||||||
|
message.unread
|
||||||
|
? 'bg-gradient-to-r from-cyan-900/40 to-emerald-900/40 border-cyan-500/40'
|
||||||
|
: 'bg-gray-900/40 border-gray-500/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-cyan-500 to-emerald-500 flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<h3 className="font-bold text-white">{message.sender}</h3>
|
||||||
|
{message.unread && (
|
||||||
|
<span className="w-2 h-2 bg-cyan-400 rounded-full flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={`text-sm truncate ${message.unread ? 'text-gray-200' : 'text-gray-400'}`}>
|
||||||
|
{message.text}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{message.timestamp}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="sticky bottom-0 bg-black/80 backdrop-blur-xl border-t border-cyan-500/20 px-4 py-3 safe-area-inset-bottom">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newMessage}
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
className="flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-cyan-500"
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
className="p-3 bg-cyan-600 hover:bg-cyan-500 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Send className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
client/src/pages/mobile-modules.tsx
Normal file
106
client/src/pages/mobile-modules.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { X, Code2, Star, Download } from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
|
||||||
|
interface Module {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
language: string;
|
||||||
|
stars: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileModules() {
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
|
const modules: Module[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Auth Guard',
|
||||||
|
description: 'Secure authentication middleware',
|
||||||
|
language: 'TypeScript',
|
||||||
|
stars: 234
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Data Mapper',
|
||||||
|
description: 'ORM and database abstraction',
|
||||||
|
language: 'TypeScript',
|
||||||
|
stars: 456
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'API Builder',
|
||||||
|
description: 'RESTful API framework',
|
||||||
|
language: 'TypeScript',
|
||||||
|
stars: 789
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'State Manager',
|
||||||
|
description: 'Reactive state management',
|
||||||
|
language: 'TypeScript',
|
||||||
|
stars: 345
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 z-30 bg-black/80 backdrop-blur-xl border-b border-cyan-500/20">
|
||||||
|
<div className="px-4 py-4 safe-area-inset-top">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/');
|
||||||
|
haptics.light();
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg bg-cyan-600/20 hover:bg-cyan-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6 text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black text-white uppercase tracking-wider">
|
||||||
|
MODULES
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-cyan-300 font-mono">{modules.length} available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modules Grid */}
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{modules.map((module) => (
|
||||||
|
<button
|
||||||
|
key={module.id}
|
||||||
|
onClick={() => haptics.light()}
|
||||||
|
className="w-full text-left rounded-lg p-4 bg-gradient-to-br from-emerald-900/40 to-cyan-900/40 border border-emerald-500/40 hover:border-emerald-400/60 transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<div className="p-2 rounded-lg bg-emerald-600/30">
|
||||||
|
<Code2 className="w-5 h-5 text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-bold text-white uppercase">{module.name}</h3>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">{module.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-mono bg-gray-800 px-2 py-1 rounded">
|
||||||
|
{module.language}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
|
||||||
|
<span className="text-xs font-bold text-yellow-400">{module.stars}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
301
client/src/pages/mobile-notifications.tsx
Normal file
301
client/src/pages/mobile-notifications.tsx
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Bell, Check, Trash2, X, Clock, AlertCircle, Info, CheckCircle } from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { PullToRefresh } from '@/components/mobile/PullToRefresh';
|
||||||
|
import { SwipeableCardList } from '@/components/mobile/SwipeableCard';
|
||||||
|
import { useNativeFeatures } from '@/hooks/use-native-features';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
type: 'info' | 'success' | 'warning' | 'error';
|
||||||
|
time: string;
|
||||||
|
read: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileNotifications() {
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
const native = useNativeFeatures();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Fetch notifications from Supabase
|
||||||
|
const fetchNotifications = async () => {
|
||||||
|
try {
|
||||||
|
if (!user) {
|
||||||
|
// Show welcome notifications for non-logged in users
|
||||||
|
setNotifications([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Welcome to AeThex Mobile',
|
||||||
|
message: 'Sign in to sync your data across devices.',
|
||||||
|
type: 'info',
|
||||||
|
time: 'now',
|
||||||
|
read: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.limit(50);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
const mapped = data.map(n => ({
|
||||||
|
id: n.id.toString(),
|
||||||
|
title: n.title || 'Notification',
|
||||||
|
message: n.message || '',
|
||||||
|
type: (n.type || 'info') as 'info' | 'success' | 'warning' | 'error',
|
||||||
|
time: formatTime(n.created_at),
|
||||||
|
read: n.read || false,
|
||||||
|
created_at: n.created_at
|
||||||
|
}));
|
||||||
|
setNotifications(mapped);
|
||||||
|
} else {
|
||||||
|
// No notifications - show empty state
|
||||||
|
setNotifications([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching notifications:', error);
|
||||||
|
native.showToast('Failed to load notifications');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp: string) => {
|
||||||
|
const now = new Date();
|
||||||
|
const created = new Date(timestamp);
|
||||||
|
const diffMs = now.getTime() - created.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'just now';
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchNotifications();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile()) {
|
||||||
|
navigate('/home');
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
haptics.light();
|
||||||
|
native.showToast('Refreshing notifications...');
|
||||||
|
await fetchNotifications();
|
||||||
|
haptics.success();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkAsRead = async (id: string) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.update({ read: true })
|
||||||
|
.eq('id', id)
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(n => n.id === id ? { ...n, read: true } : n)
|
||||||
|
);
|
||||||
|
haptics.selection();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking as read:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (notification: Notification) => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.delete()
|
||||||
|
.eq('id', notification.id)
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
setNotifications(prev => prev.filter(n => n.id !== notification.id));
|
||||||
|
native.showToast('Notification deleted');
|
||||||
|
haptics.medium();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting notification:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkAllRead = async () => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('notifications')
|
||||||
|
.update({ read: true })
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.eq('read', false);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
setNotifications(prev => prev.map(n => ({ ...n, read: true })));
|
||||||
|
native.showToast('All marked as read');
|
||||||
|
haptics.success();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking all as read:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => !n.read).length;
|
||||||
|
|
||||||
|
const getIcon = (type: Notification['type']) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return <CheckCircle className="w-5 h-5 text-green-400" />;
|
||||||
|
case 'warning': return <AlertCircle className="w-5 h-5 text-yellow-400" />;
|
||||||
|
case 'error': return <AlertCircle className="w-5 h-5 text-red-400" />;
|
||||||
|
default: return <Info className="w-5 h-5 text-blue-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isMobile()) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 z-30 bg-black/80 backdrop-blur-xl border-b border-cyan-500/20">
|
||||||
|
<div className="px-4 py-4 safe-area-inset-top">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/');
|
||||||
|
haptics.light();
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg bg-cyan-600/20 hover:bg-cyan-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6 text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black text-white uppercase tracking-wider">
|
||||||
|
ALERTS
|
||||||
|
</h1>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<p className="text-xs text-cyan-300 font-mono">
|
||||||
|
{unreadCount} new events
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleMarkAllRead}
|
||||||
|
className="px-3 py-2 bg-cyan-600 hover:bg-cyan-500 rounded-lg text-xs font-black uppercase tracking-wide transition-colors"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notifications list */}
|
||||||
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
<SwipeableCardList
|
||||||
|
items={notifications}
|
||||||
|
keyExtractor={(n) => n.id}
|
||||||
|
onItemSwipeLeft={handleDelete}
|
||||||
|
renderItem={(notification) => (
|
||||||
|
<div
|
||||||
|
onClick={() => !notification.read && handleMarkAsRead(notification.id)}
|
||||||
|
className={`relative overflow-hidden rounded-lg transition-all ${
|
||||||
|
notification.read
|
||||||
|
? 'bg-gray-900/40 border border-gray-800 opacity-60'
|
||||||
|
: 'bg-gradient-to-r from-cyan-900/40 to-emerald-900/40 border border-cyan-500/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Accent line */}
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-cyan-400 to-emerald-400" />
|
||||||
|
|
||||||
|
<div className="p-4 pl-4">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
{getIcon(notification.type)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
|
<h3 className={`font-bold uppercase text-sm tracking-wide ${
|
||||||
|
notification.read ? 'text-gray-500' : 'text-white'
|
||||||
|
}`}>
|
||||||
|
{notification.title}
|
||||||
|
</h3>
|
||||||
|
{!notification.read && (
|
||||||
|
<span className="w-2 h-2 bg-cyan-400 rounded-full flex-shrink-0 mt-2" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className={`text-sm mb-2 line-clamp-2 ${
|
||||||
|
notification.read ? 'text-gray-600' : 'text-gray-300'
|
||||||
|
}`}>
|
||||||
|
{notification.message}
|
||||||
|
</p>
|
||||||
|
<div className={`flex items-center gap-2 text-xs font-mono ${
|
||||||
|
notification.read ? 'text-gray-600' : 'text-cyan-400'
|
||||||
|
}`}>
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
{notification.time}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
emptyMessage="No notifications"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{notifications.length === 0 && (
|
||||||
|
<div className="text-center py-16">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-cyan-500/10 flex items-center justify-center">
|
||||||
|
<Bell className="w-8 h-8 text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<p className="text-white font-black text-lg uppercase">All Caught Up</p>
|
||||||
|
<p className="text-xs text-cyan-300/60 mt-2 font-mono">No new events</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tip */}
|
||||||
|
{notifications.length > 0 && (
|
||||||
|
<div className="mt-6 bg-gradient-to-r from-cyan-900/20 to-emerald-900/20 border border-cyan-500/30 rounded-lg p-4">
|
||||||
|
<p className="text-xs text-cyan-200 font-mono leading-relaxed">
|
||||||
|
← SWIPE LEFT TO DISMISS<br/>
|
||||||
|
TAP TO MARK AS READ
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PullToRefresh>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
client/src/pages/mobile-projects.tsx
Normal file
152
client/src/pages/mobile-projects.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X, Plus, Folder, GitBranch } from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
status: 'active' | 'completed' | 'archived';
|
||||||
|
progress: number;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MobileProjects() {
|
||||||
|
const [, navigate] = useLocation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
try {
|
||||||
|
if (!user) {
|
||||||
|
setProjects([
|
||||||
|
{
|
||||||
|
id: 'demo',
|
||||||
|
name: 'Sign in to view projects',
|
||||||
|
description: 'Create and manage your development projects',
|
||||||
|
status: 'active',
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
const mapped = data.map(p => ({
|
||||||
|
id: p.id.toString(),
|
||||||
|
name: p.name || 'Untitled Project',
|
||||||
|
description: p.description || 'No description',
|
||||||
|
status: (p.status || 'active') as 'active' | 'completed' | 'archived',
|
||||||
|
progress: p.progress || 0,
|
||||||
|
created_at: p.created_at
|
||||||
|
}));
|
||||||
|
setProjects(mapped);
|
||||||
|
} else {
|
||||||
|
setProjects([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching projects:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProjects();
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active': return 'bg-emerald-900/40 border-emerald-500/40';
|
||||||
|
case 'completed': return 'bg-cyan-900/40 border-cyan-500/40';
|
||||||
|
default: return 'bg-gray-900/40 border-gray-500/40';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressColor = (progress: number) => {
|
||||||
|
if (progress === 100) return 'bg-cyan-500';
|
||||||
|
if (progress >= 75) return 'bg-emerald-500';
|
||||||
|
if (progress >= 50) return 'bg-yellow-500';
|
||||||
|
return 'bg-red-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 z-30 bg-black/80 backdrop-blur-xl border-b border-cyan-500/20">
|
||||||
|
<div className="px-4 py-4 safe-area-inset-top">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/');
|
||||||
|
haptics.light();
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg bg-cyan-600/20 hover:bg-cyan-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6 text-cyan-400" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black text-white uppercase tracking-wider">
|
||||||
|
PROJECTS
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-cyan-300 font-mono">{projects.length} items</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="p-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 transition-colors">
|
||||||
|
<Plus className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Projects List */}
|
||||||
|
<div className="px-4 py-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<button
|
||||||
|
key={project.id}
|
||||||
|
onClick={() => haptics.light()}
|
||||||
|
className={`w-full text-left rounded-lg p-4 border transition-all ${getStatusColor(project.status)}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<div className="p-2 rounded-lg bg-cyan-600/30">
|
||||||
|
<Folder className="w-5 h-5 text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-bold text-white uppercase">{project.name}</h3>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">{project.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-mono px-2 py-1 bg-gray-800 rounded">
|
||||||
|
{project.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="w-full h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${getProgressColor(project.progress)} transition-all`}
|
||||||
|
style={{ width: `${project.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mt-2">{project.progress}% complete</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
396
client/src/pages/mobile-simple.tsx
Normal file
396
client/src/pages/mobile-simple.tsx
Normal file
|
|
@ -0,0 +1,396 @@
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Camera,
|
||||||
|
Bell,
|
||||||
|
FileText,
|
||||||
|
Users,
|
||||||
|
Settings,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
|
Zap,
|
||||||
|
Code,
|
||||||
|
MessageSquare,
|
||||||
|
Package,
|
||||||
|
ShieldCheck,
|
||||||
|
Activity,
|
||||||
|
Sparkles,
|
||||||
|
MonitorSmartphone,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useLocation } from 'wouter';
|
||||||
|
import { isMobile } from '@/lib/platform';
|
||||||
|
import { App as CapApp } from '@capacitor/app';
|
||||||
|
import { haptics } from '@/lib/haptics';
|
||||||
|
import { PullToRefresh } from '@/components/mobile/PullToRefresh';
|
||||||
|
import { SwipeableCardList } from '@/components/mobile/SwipeableCard';
|
||||||
|
import { MobileBottomNav, DEFAULT_MOBILE_TABS } from '@/components/MobileBottomNav';
|
||||||
|
|
||||||
|
export default function SimpleMobileDashboard() {
|
||||||
|
const [location, navigate] = useLocation();
|
||||||
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState('home');
|
||||||
|
const [cards, setCards] = useState(() => defaultCards());
|
||||||
|
|
||||||
|
// Handle Android back button
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile()) return;
|
||||||
|
|
||||||
|
const backHandler = CapApp.addListener('backButton', ({ canGoBack }) => {
|
||||||
|
if (location === '/' || location === '/mobile') {
|
||||||
|
CapApp.exitApp();
|
||||||
|
} else {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
backHandler.remove();
|
||||||
|
};
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
haptics.light();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 900));
|
||||||
|
haptics.success();
|
||||||
|
};
|
||||||
|
|
||||||
|
const quickStats = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: 'Projects', value: '5', icon: <Package className="w-4 h-4" />, tone: 'from-cyan-500 to-emerald-500' },
|
||||||
|
{ label: 'Alerts', value: '3', icon: <Bell className="w-4 h-4" />, tone: 'from-red-500 to-pink-500' },
|
||||||
|
{ label: 'Messages', value: '12', icon: <MessageSquare className="w-4 h-4" />, tone: 'from-violet-500 to-blue-500' },
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNav = (path: string) => {
|
||||||
|
haptics.light();
|
||||||
|
navigate(path);
|
||||||
|
setShowMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isMobile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white overflow-hidden pb-20">
|
||||||
|
{/* Animated background */}
|
||||||
|
<div className="fixed inset-0 opacity-30">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-cyan-600/10 via-transparent to-emerald-600/10" />
|
||||||
|
<div className="absolute top-0 left-1/4 w-96 h-96 bg-cyan-500/5 rounded-full blur-3xl" />
|
||||||
|
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-emerald-500/5 rounded-full blur-3xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="fixed top-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-xl border-b border-cyan-500/20">
|
||||||
|
<div className="flex items-center justify-between px-4 py-4 safe-area-inset-top">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-12 h-12 bg-gradient-to-br from-cyan-400 to-emerald-400 rounded-lg flex items-center justify-center font-black text-black shadow-lg shadow-cyan-500/50">
|
||||||
|
Æ
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-black text-white uppercase tracking-widest">AeThex</h1>
|
||||||
|
<p className="text-xs text-cyan-300 font-mono">Mobile OS</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/notifications')}
|
||||||
|
className="p-3 rounded-lg bg-cyan-500/10 border border-cyan-500/30 hover:bg-cyan-500/20 transition-colors"
|
||||||
|
>
|
||||||
|
<Bell className="w-5 h-5 text-cyan-200" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
haptics.light();
|
||||||
|
setShowMenu(!showMenu);
|
||||||
|
}}
|
||||||
|
className="p-3 hover:bg-cyan-500/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{showMenu ? <X className="w-6 h-6 text-cyan-400" /> : <Menu className="w-6 h-6 text-cyan-400" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<PullToRefresh onRefresh={handleRefresh}>
|
||||||
|
<div className="relative pt-28 pb-8 px-4 space-y-6">
|
||||||
|
{/* Welcome */}
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-cyan-300/80 font-mono uppercase">AeThex OS · Android</p>
|
||||||
|
<h2 className="text-3xl font-black text-white uppercase tracking-wider">Launchpad</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Primary Resume */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/hub/projects')}
|
||||||
|
className="w-full relative overflow-hidden rounded-2xl group border border-emerald-500/30 bg-gradient-to-r from-emerald-600/30 to-cyan-600/30"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-emerald-500/40 to-cyan-500/40 opacity-0 group-hover:opacity-20 blur-xl transition-opacity" />
|
||||||
|
<div className="relative px-6 py-5 flex items-center justify-between">
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-xs text-emerald-100 font-mono mb-1">RESUME</p>
|
||||||
|
<p className="text-2xl font-black text-white uppercase">Projects</p>
|
||||||
|
<p className="text-xs text-emerald-200 mt-1">Continue where you left off</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-emerald-200 font-bold">
|
||||||
|
<Activity className="w-6 h-6" />
|
||||||
|
Go
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Full OS entry point */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/os')}
|
||||||
|
className="w-full relative overflow-hidden rounded-xl group border border-cyan-500/30 bg-gradient-to-r from-cyan-900/50 to-emerald-900/40"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500/30 to-emerald-500/20 opacity-0 group-hover:opacity-20 blur-xl transition-opacity" />
|
||||||
|
<div className="relative px-5 py-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-3 rounded-lg bg-cyan-500/20 border border-cyan-400/30">
|
||||||
|
<MonitorSmartphone className="w-5 h-5 text-cyan-200" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-xs text-cyan-200 font-mono uppercase">Full OS</p>
|
||||||
|
<p className="text-sm text-white font-semibold">Open desktop UI on mobile</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-cyan-200 text-sm font-bold">Go</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Quick stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{quickStats.map((stat) => (
|
||||||
|
<div key={stat.label} className="bg-gray-900/80 border border-cyan-500/20 rounded-xl p-3">
|
||||||
|
<div className={`inline-flex p-2 rounded-lg bg-gradient-to-r ${stat.tone} mb-2`}>{stat.icon}</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-1">{stat.value}</div>
|
||||||
|
<div className="text-[10px] text-gray-400 uppercase tracking-wide">{stat.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<QuickTile icon={<Camera className="w-7 h-7" />} label="Capture" color="from-blue-900/40 to-purple-900/40" onPress={() => handleNav('/camera')} />
|
||||||
|
<QuickTile icon={<Bell className="w-7 h-7" />} label="Alerts" color="from-red-900/40 to-pink-900/40" badge="3" onPress={() => handleNav('/notifications')} />
|
||||||
|
<QuickTile icon={<Code className="w-7 h-7" />} label="Modules" color="from-emerald-900/40 to-cyan-900/40" onPress={() => handleNav('/hub/code-gallery')} />
|
||||||
|
<QuickTile icon={<MessageSquare className="w-7 h-7" />} label="Messages" color="from-violet-900/40 to-purple-900/40" onPress={() => handleNav('/hub/messaging')} />
|
||||||
|
<QuickTile icon={<MonitorSmartphone className="w-7 h-7" />} label="Desktop OS" color="from-cyan-900/40 to-emerald-900/40" onPress={() => handleNav('/os')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Swipeable shortcuts */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-cyan-300/70 font-mono uppercase">Shortcuts</p>
|
||||||
|
<h3 className="text-lg font-bold text-white">Move fast</h3>
|
||||||
|
</div>
|
||||||
|
<Sparkles className="w-5 h-5 text-cyan-300" />
|
||||||
|
</div>
|
||||||
|
<SwipeableCardList
|
||||||
|
items={cards}
|
||||||
|
keyExtractor={(card) => card.id}
|
||||||
|
onItemSwipeLeft={(card) => {
|
||||||
|
haptics.medium();
|
||||||
|
setCards((prev) => prev.filter((c) => c.id !== card.id));
|
||||||
|
}}
|
||||||
|
onItemSwipeRight={(card) => {
|
||||||
|
haptics.medium();
|
||||||
|
setCards((prev) => prev.map((c) => (c.id === card.id ? { ...c, pinned: true } : c)));
|
||||||
|
}}
|
||||||
|
renderItem={(card) => (
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav(card.path)}
|
||||||
|
className="w-full text-left bg-gradient-to-r from-gray-900 to-gray-800 border border-cyan-500/20 rounded-xl p-4 active:scale-98 transition-transform"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-3 rounded-lg bg-gradient-to-r ${card.color}`}>{card.icon}</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="font-semibold text-white">{card.title}</p>
|
||||||
|
{card.badge ? (
|
||||||
|
<span className="px-2 py-1 text-xs font-bold bg-red-500 text-white rounded-full">{card.badge}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400">{card.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
emptyMessage="No shortcuts yet"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Bar */}
|
||||||
|
<div className="bg-gradient-to-r from-cyan-900/20 to-emerald-900/20 border border-cyan-500/20 rounded-xl p-4 font-mono text-xs space-y-2">
|
||||||
|
<div className="flex justify-between text-cyan-300">
|
||||||
|
<span>PLATFORM</span>
|
||||||
|
<span className="text-cyan-100 font-bold">ANDROID</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-emerald-300">
|
||||||
|
<span>STATUS</span>
|
||||||
|
<span className="text-emerald-100 font-bold">READY</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-cyan-200">
|
||||||
|
<span>SYNC</span>
|
||||||
|
<span className="text-cyan-100 font-bold">LIVE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PullToRefresh>
|
||||||
|
|
||||||
|
{/* Bottom navigation */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-40">
|
||||||
|
<MobileBottomNav
|
||||||
|
tabs={DEFAULT_MOBILE_TABS}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={(tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
haptics.selection();
|
||||||
|
navigate(tabId === 'home' ? '/' : `/${tabId}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slide-out Menu */}
|
||||||
|
{showMenu && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
|
||||||
|
onClick={() => setShowMenu(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed top-0 right-0 bottom-0 w-72 bg-black/95 backdrop-blur-xl border-l border-cyan-500/30 z-50 flex flex-col safe-area-inset-top">
|
||||||
|
<div className="flex-1 p-6 overflow-y-auto space-y-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/')}
|
||||||
|
className="w-full text-left px-4 py-3 bg-cyan-600 rounded-lg font-bold text-white flex items-center gap-3 mb-4"
|
||||||
|
>
|
||||||
|
<Zap className="w-5 h-5" />
|
||||||
|
Home
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/camera')}
|
||||||
|
className="w-full text-left px-4 py-3 hover:bg-cyan-600/20 rounded-lg text-cyan-300 flex items-center gap-3 transition-colors"
|
||||||
|
>
|
||||||
|
<Camera className="w-5 h-5" />
|
||||||
|
Capture
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/notifications')}
|
||||||
|
className="w-full text-left px-4 py-3 hover:bg-cyan-600/20 rounded-lg text-cyan-300 flex items-center gap-3 transition-colors"
|
||||||
|
>
|
||||||
|
<Bell className="w-5 h-5" />
|
||||||
|
Alerts
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/hub/projects')}
|
||||||
|
className="w-full text-left px-4 py-3 hover:bg-cyan-600/20 rounded-lg text-cyan-300 flex items-center gap-3 transition-colors"
|
||||||
|
>
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
Projects
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/hub/messaging')}
|
||||||
|
className="w-full text-left px-4 py-3 hover:bg-cyan-600/20 rounded-lg text-cyan-300 flex items-center gap-3 transition-colors"
|
||||||
|
>
|
||||||
|
<Users className="w-5 h-5" />
|
||||||
|
Messages
|
||||||
|
</button>
|
||||||
|
<div className="border-t border-cyan-500/20 my-4" />
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/hub/settings')}
|
||||||
|
className="w-full text-left px-4 py-3 hover:bg-cyan-600/20 rounded-lg text-cyan-300 flex items-center gap-3 transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="w-5 h-5" />
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNav('/os')}
|
||||||
|
className="w-full text-left px-4 py-3 hover:bg-cyan-600/20 rounded-lg text-cyan-300 flex items-center gap-3 transition-colors"
|
||||||
|
>
|
||||||
|
<MonitorSmartphone className="w-5 h-5" />
|
||||||
|
Desktop OS (Full)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultCards() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'projects',
|
||||||
|
title: 'Projects',
|
||||||
|
description: 'View and manage builds',
|
||||||
|
icon: <Package className="w-6 h-6" />,
|
||||||
|
color: 'from-blue-500 to-cyan-500',
|
||||||
|
badge: 3,
|
||||||
|
path: '/hub/projects',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'messages',
|
||||||
|
title: 'Messages',
|
||||||
|
description: 'Recent conversations',
|
||||||
|
icon: <MessageSquare className="w-6 h-6" />,
|
||||||
|
color: 'from-purple-500 to-pink-500',
|
||||||
|
badge: 5,
|
||||||
|
path: '/hub/messaging',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alerts',
|
||||||
|
title: 'Alerts',
|
||||||
|
description: 'System notifications',
|
||||||
|
icon: <ShieldCheck className="w-6 h-6" />,
|
||||||
|
color: 'from-red-500 to-orange-500',
|
||||||
|
path: '/notifications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'modules',
|
||||||
|
title: 'Modules',
|
||||||
|
description: 'Code gallery and tools',
|
||||||
|
icon: <Code className="w-6 h-6" />,
|
||||||
|
color: 'from-emerald-500 to-cyan-500',
|
||||||
|
path: '/hub/code-gallery',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickTile({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
badge,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
badge?: string;
|
||||||
|
onPress: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onPress}
|
||||||
|
className={`group relative overflow-hidden rounded-xl p-5 bg-gradient-to-br ${color} border border-cyan-500/20 hover:border-cyan-400/40 active:opacity-80 transition-all`}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/0 to-emerald-500/0 group-hover:from-cyan-500/10 group-hover:to-emerald-500/10 transition-all" />
|
||||||
|
<div className="relative flex flex-col items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
{icon}
|
||||||
|
{badge ? (
|
||||||
|
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-black w-5 h-5 rounded-full flex items-center justify-center">
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold text-white">{label}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
52
deploy-to-phone.ps1
Normal file
52
deploy-to-phone.ps1
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# AeThex OS Mobile App Deployment Script
|
||||||
|
# Deploys the app directly to your Samsung phone
|
||||||
|
|
||||||
|
$adbPath = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
|
||||||
|
$apkPath = "c:\Users\PCOEM\AeThexOS\AeThex-OS\android\app\build\outputs\apk\debug\app-debug.apk"
|
||||||
|
|
||||||
|
Write-Host "🚀 AeThex OS Mobile Deployment" -ForegroundColor Cyan
|
||||||
|
Write-Host "================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Check if phone is connected
|
||||||
|
Write-Host "📱 Checking for connected devices..."
|
||||||
|
& $adbPath devices
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📦 Building APK..."
|
||||||
|
cd "c:\Users\PCOEM\AeThexOS\AeThex-OS\android"
|
||||||
|
|
||||||
|
# Set Java home if needed
|
||||||
|
$jdkPath = Get-ChildItem "C:\Program Files\Android\Android Studio\jre" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if ($jdkPath) {
|
||||||
|
$env:JAVA_HOME = $jdkPath.FullName
|
||||||
|
Write-Host "✓ Java found at: $env:JAVA_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build with gradlew
|
||||||
|
Write-Host "⏳ This may take 2-5 minutes..."
|
||||||
|
& ".\gradlew.bat" assembleDebug 2>&1 | Tee-Object -FilePath "build.log"
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "✓ APK built successfully!" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📲 Installing on your phone..."
|
||||||
|
& $adbPath install -r $apkPath
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "✓ App installed successfully!" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "🎉 Launching AeThex OS..."
|
||||||
|
& $adbPath shell am start -n "com.aethex.os/com.aethex.os.MainActivity"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "✓ App launched on your phone!" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ Installation failed" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "❌ Build failed. Check build.log for details" -ForegroundColor Red
|
||||||
|
Get-Content build.log -Tail 50
|
||||||
|
}
|
||||||
48
docs/ISO_VERIFICATION.md
Normal file
48
docs/ISO_VERIFICATION.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# AeThex OS ISO Verification
|
||||||
|
|
||||||
|
Use this guide to verify that a built ISO is real, intact, and contains the expected boot assets.
|
||||||
|
|
||||||
|
## Quick Verify (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./script/verify-iso.sh -i aethex-linux-build/AeThex-Linux-amd64.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
What it checks:
|
||||||
|
- File exists + size
|
||||||
|
- SHA256 checksum (if `.sha256` or `.sha256.txt` file exists)
|
||||||
|
- Key boot files inside the ISO
|
||||||
|
|
||||||
|
## Enforce a Specific Checksum
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./script/verify-iso.sh -i AeThex-OS-Full-amd64.iso -s <expected_sha256>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deep Verify (Mounted Contents)
|
||||||
|
|
||||||
|
Mount the ISO to confirm file layout directly (requires sudo/root):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./script/verify-iso.sh -i AeThex-OS-Full-amd64.iso --mount
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Success Output
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
- A calculated SHA256
|
||||||
|
- `SHA256 matches ...` if a checksum is provided or discovered
|
||||||
|
- `[✓] Kernel`, `[✓] Initrd`, `[✓] SquashFS`, `[✓] GRUB config`, `[✓] ISOLINUX config`
|
||||||
|
- Final line: `ISO verification complete.`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Missing files:** Rebuild the ISO and check `script/build-linux-iso.sh` or `script/build-linux-iso-full.sh`.
|
||||||
|
- **SHA mismatch:** Re-download the ISO artifact or copy the correct `.sha256` file next to the ISO.
|
||||||
|
- **No inspection tool:** Install `xorriso` (preferred) or `isoinfo` and re-run.
|
||||||
|
|
||||||
|
## Related Docs
|
||||||
|
|
||||||
|
- `LINUX_QUICKSTART.md`
|
||||||
|
- `ISO_BUILD_FIXED.md`
|
||||||
|
- `docs/FLASH_USB.md`
|
||||||
32
package-lock.json
generated
32
package-lock.json
generated
|
|
@ -64,7 +64,6 @@
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bufferutil": "4.1.0",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|
@ -127,6 +126,7 @@
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"playwright-chromium": "^1.57.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.5",
|
||||||
|
|
@ -7644,6 +7644,36 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright-chromium": {
|
||||||
|
"version": "1.57.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.57.0.tgz",
|
||||||
|
"integrity": "sha512-GCVVTbmIDrZuBxWYoQ70rehRXMb3Q7ccENe63a+rGTWwypeVAgh/DD5o5QQ898oer5pdIv3vGINUlEkHtOZQEw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.57.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.57.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
|
||||||
|
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/plist": {
|
"node_modules/plist": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"playwright-chromium": "^1.57.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tsx": "^4.20.5",
|
"tsx": "^4.20.5",
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,13 @@ done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "┌─────────────────────────────────────────────────────────────┐"
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
||||||
echo "│ LAYER 1: Base OS (Ubuntu 24.04 LTS) │"
|
echo "│ LAYER 1: Base OS (Ubuntu 22.04 LTS) - HP Compatible │"
|
||||||
echo "└─────────────────────────────────────────────────────────────┘"
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "[+] Bootstrapping Ubuntu 24.04 base system..."
|
echo "[+] Bootstrapping Ubuntu 22.04 base system (older kernel 5.15)..."
|
||||||
echo " (debootstrap takes ~10-15 minutes...)"
|
echo " (debootstrap takes ~10-15 minutes...)"
|
||||||
debootstrap --arch=amd64 --variant=minbase noble "$ROOTFS_DIR" http://archive.ubuntu.com/ubuntu/ 2>&1 | tail -20
|
debootstrap --arch=amd64 --variant=minbase jammy "$ROOTFS_DIR" http://archive.ubuntu.com/ubuntu/ 2>&1 | tail -20
|
||||||
|
|
||||||
echo "[+] Configuring base system..."
|
echo "[+] Configuring base system..."
|
||||||
echo "aethex-os" > "$ROOTFS_DIR/etc/hostname"
|
echo "aethex-os" > "$ROOTFS_DIR/etc/hostname"
|
||||||
|
|
@ -62,18 +62,19 @@ chroot "$ROOTFS_DIR" bash -c '
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Add universe repository
|
# Add universe repository
|
||||||
echo "deb http://archive.ubuntu.com/ubuntu noble main restricted universe multiverse" > /etc/apt/sources.list
|
echo "deb http://archive.ubuntu.com/ubuntu jammy main restricted universe multiverse" > /etc/apt/sources.list
|
||||||
echo "deb http://archive.ubuntu.com/ubuntu noble-updates main restricted universe multiverse" >> /etc/apt/sources.list
|
echo "deb http://archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse" >> /etc/apt/sources.list
|
||||||
echo "deb http://archive.ubuntu.com/ubuntu noble-security main restricted universe multiverse" >> /etc/apt/sources.list
|
echo "deb http://archive.ubuntu.com/ubuntu jammy-security main restricted universe multiverse" >> /etc/apt/sources.list
|
||||||
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
linux-image-generic linux-headers-generic \
|
linux-image-generic linux-headers-generic \
|
||||||
|
casper \
|
||||||
grub-pc-bin grub-efi-amd64-bin grub-common xorriso \
|
grub-pc-bin grub-efi-amd64-bin grub-common xorriso \
|
||||||
systemd-sysv dbus \
|
systemd-sysv dbus \
|
||||||
network-manager wpasupplicant \
|
network-manager wpasupplicant \
|
||||||
sudo curl wget git ca-certificates gnupg \
|
sudo curl wget git ca-certificates gnupg \
|
||||||
pipewire-audio wireplumber \
|
pipewire wireplumber \
|
||||||
xorg xserver-xorg-video-all \
|
xorg xserver-xorg-video-all \
|
||||||
xfce4 xfce4-goodies lightdm \
|
xfce4 xfce4-goodies lightdm \
|
||||||
firefox thunar xfce4-terminal \
|
firefox thunar xfce4-terminal \
|
||||||
|
|
@ -149,7 +150,7 @@ chroot "$ROOTFS_DIR" bash -c '
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list
|
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" > /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
@ -319,6 +320,14 @@ echo "│ ISO Packaging │"
|
||||||
echo "└─────────────────────────────────────────────────────────────┘"
|
echo "└─────────────────────────────────────────────────────────────┘"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
echo "[+] Regenerating initramfs with casper..."
|
||||||
|
chroot "$ROOTFS_DIR" bash -c '
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
KERNEL_VERSION=$(ls /boot/vmlinuz-* | sed "s|/boot/vmlinuz-||" | head -n 1)
|
||||||
|
echo " Rebuilding initramfs for kernel $KERNEL_VERSION with casper..."
|
||||||
|
update-initramfs -u -k "$KERNEL_VERSION"
|
||||||
|
' 2>&1 | tail -10
|
||||||
|
|
||||||
echo "[+] Extracting kernel and initrd..."
|
echo "[+] Extracting kernel and initrd..."
|
||||||
KERNEL="$(ls -1 $ROOTFS_DIR/boot/vmlinuz-* 2>/dev/null | head -n 1)"
|
KERNEL="$(ls -1 $ROOTFS_DIR/boot/vmlinuz-* 2>/dev/null | head -n 1)"
|
||||||
INITRD="$(ls -1 $ROOTFS_DIR/boot/initrd.img-* 2>/dev/null | head -n 1)"
|
INITRD="$(ls -1 $ROOTFS_DIR/boot/initrd.img-* 2>/dev/null | head -n 1)"
|
||||||
|
|
@ -334,6 +343,13 @@ cp "$INITRD" "$ISO_DIR/casper/initrd.img"
|
||||||
echo "[✓] Kernel: $(basename "$KERNEL")"
|
echo "[✓] Kernel: $(basename "$KERNEL")"
|
||||||
echo "[✓] Initrd: $(basename "$INITRD")"
|
echo "[✓] Initrd: $(basename "$INITRD")"
|
||||||
|
|
||||||
|
echo "[+] Verifying casper in initrd..."
|
||||||
|
if lsinitramfs "$ISO_DIR/casper/initrd.img" | grep -q "scripts/casper"; then
|
||||||
|
echo "[✓] Casper scripts found in initrd"
|
||||||
|
else
|
||||||
|
echo "[!] WARNING: Casper scripts NOT found in initrd!"
|
||||||
|
fi
|
||||||
|
|
||||||
# Unmount chroot filesystems
|
# Unmount chroot filesystems
|
||||||
echo "[+] Unmounting chroot..."
|
echo "[+] Unmounting chroot..."
|
||||||
umount -lf "$ROOTFS_DIR/dev/pts" 2>/dev/null || true
|
umount -lf "$ROOTFS_DIR/dev/pts" 2>/dev/null || true
|
||||||
|
|
@ -354,7 +370,12 @@ DEFAULT linux
|
||||||
LABEL linux
|
LABEL linux
|
||||||
MENU LABEL AeThex OS - Full Stack
|
MENU LABEL AeThex OS - Full Stack
|
||||||
KERNEL /casper/vmlinuz
|
KERNEL /casper/vmlinuz
|
||||||
APPEND initrd=/casper/initrd.img boot=casper quiet splash
|
APPEND initrd=/casper/initrd.img boot=casper quiet splash ---
|
||||||
|
|
||||||
|
LABEL safe
|
||||||
|
MENU LABEL AeThex OS - Safe Mode (No ACPI)
|
||||||
|
KERNEL /casper/vmlinuz
|
||||||
|
APPEND initrd=/casper/initrd.img boot=casper acpi=off noapic nomodeset ---
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cp /usr/lib/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || \
|
cp /usr/lib/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || \
|
||||||
|
|
@ -368,12 +389,17 @@ set timeout=10
|
||||||
set default=0
|
set default=0
|
||||||
|
|
||||||
menuentry "AeThex OS - Full Stack" {
|
menuentry "AeThex OS - Full Stack" {
|
||||||
linux /casper/vmlinuz boot=casper quiet splash
|
linux /casper/vmlinuz boot=casper quiet splash ---
|
||||||
initrd /casper/initrd.img
|
initrd /casper/initrd.img
|
||||||
}
|
}
|
||||||
|
|
||||||
menuentry "AeThex OS - Safe Mode" {
|
menuentry "AeThex OS - Safe Mode (No ACPI)" {
|
||||||
linux /casper/vmlinuz boot=casper nomodeset
|
linux /casper/vmlinuz boot=casper acpi=off noapic nomodeset ---
|
||||||
|
initrd /casper/initrd.img
|
||||||
|
}
|
||||||
|
|
||||||
|
menuentry "AeThex OS - Debug Mode" {
|
||||||
|
linux /casper/vmlinuz boot=casper debug ignore_loglevel earlyprintk=vga ---
|
||||||
initrd /casper/initrd.img
|
initrd /casper/initrd.img
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -408,9 +434,10 @@ echo "┃ AeThex OS - Full Stack Edition ┃"
|
||||||
echo "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
echo "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
||||||
echo ""
|
echo ""
|
||||||
echo "ARCHITECTURE:"
|
echo "ARCHITECTURE:"
|
||||||
echo " ├── Base OS: Ubuntu 24.04 LTS (5-year support)"
|
echo " ├── Base OS: Ubuntu 22.04 LTS (kernel 5.15 - better hardware compat)"
|
||||||
echo " ├── Runtime: Windows (Wine 9.0 + DXVK)"
|
echo " ├── Runtime: Windows (Wine 9.0 + DXVK)"
|
||||||
echo " ├── Runtime: Linux Dev (Docker + VSCode + Node + Python + Rust)"
|
echo " ├── Runtime: Linux Dev (Docker + VSCode + Node + Python + Rust)"
|
||||||
|
echo " ├── Live Boot: Casper (full live USB support)"
|
||||||
echo " └── Shell: Mode switching + file associations"
|
echo " └── Shell: Mode switching + file associations"
|
||||||
echo ""
|
echo ""
|
||||||
echo "INSTALLED RUNTIMES:"
|
echo "INSTALLED RUNTIMES:"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# AeThex Linux ISO Builder - Containerized Edition
|
# AeThex Linux ISO Builder - Containerized Edition
|
||||||
# Creates a bootable ISO using Ubuntu base image (no debootstrap/chroot needed)
|
# Creates a bootable ISO using debootstrap + chroot
|
||||||
|
|
||||||
WORK_DIR="${1:-.}"
|
WORK_DIR="${1:-.}"
|
||||||
BUILD_DIR="$WORK_DIR/aethex-linux-build"
|
BUILD_DIR="$WORK_DIR/aethex-linux-build"
|
||||||
|
ROOTFS_DIR="$BUILD_DIR/rootfs"
|
||||||
|
ISO_DIR="$BUILD_DIR/iso"
|
||||||
ISO_NAME="AeThex-Linux-amd64.iso"
|
ISO_NAME="AeThex-Linux-amd64.iso"
|
||||||
|
|
||||||
echo "[*] AeThex ISO Builder - Containerized Edition"
|
echo "[*] AeThex ISO Builder - Containerized Edition"
|
||||||
|
|
@ -13,31 +15,40 @@ echo "[*] Build directory: $BUILD_DIR"
|
||||||
echo "[*] This build method works in Docker without privileged mode"
|
echo "[*] This build method works in Docker without privileged mode"
|
||||||
|
|
||||||
# Clean and prepare
|
# Clean and prepare
|
||||||
rm -rf "$BUILD_DIR"
|
if [ -d "$BUILD_DIR" ]; then
|
||||||
mkdir -p "$BUILD_DIR"/{iso,rootfs}
|
sudo rm -rf "$BUILD_DIR" 2>/dev/null || {
|
||||||
|
find "$BUILD_DIR" -type f -exec chmod 644 {} \; 2>/dev/null || true
|
||||||
|
find "$BUILD_DIR" -type d -exec chmod 755 {} \; 2>/dev/null || true
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
mkdir -p "$ROOTFS_DIR" "$ISO_DIR" "$ISO_DIR"/casper "$ISO_DIR"/isolinux "$ISO_DIR"/boot/grub
|
||||||
|
|
||||||
# Check dependencies
|
# Check dependencies
|
||||||
echo "[*] Checking dependencies..."
|
echo "[*] Checking dependencies..."
|
||||||
for cmd in xorriso genisoimage mksquashfs; do
|
apt-get update -qq
|
||||||
if ! command -v "$cmd" &> /dev/null; then
|
apt-get install -y -qq \
|
||||||
echo "[!] Missing: $cmd - installing..."
|
debootstrap squashfs-tools xorriso grub-common grub-pc-bin grub-efi-amd64-bin \
|
||||||
apt-get update -qq
|
syslinux-common isolinux mtools dosfstools wget ca-certificates 2>&1 | tail -10
|
||||||
apt-get install -y -qq "$cmd" 2>&1 | tail -5
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "[+] Downloading Ubuntu Mini ISO base..."
|
echo "[+] Bootstrapping Ubuntu base (noble)..."
|
||||||
# Use Ubuntu mini.iso as base (much smaller, pre-built)
|
debootstrap --arch=amd64 noble "$ROOTFS_DIR" http://archive.ubuntu.com/ubuntu/
|
||||||
if [ ! -f "$BUILD_DIR/ubuntu-mini.iso" ]; then
|
cp /etc/resolv.conf "$ROOTFS_DIR/etc/resolv.conf"
|
||||||
wget -q --show-progress -O "$BUILD_DIR/ubuntu-mini.iso" \
|
|
||||||
http://archive.ubuntu.com/ubuntu/dists/noble/main/installer-amd64/current/legacy-images/netboot/mini.iso \
|
cleanup_mounts() {
|
||||||
|| echo "[!] Download failed, creating minimal ISO instead"
|
umount -lf "$ROOTFS_DIR/proc" 2>/dev/null || true
|
||||||
fi
|
umount -lf "$ROOTFS_DIR/sys" 2>/dev/null || true
|
||||||
|
umount -lf "$ROOTFS_DIR/dev/pts" 2>/dev/null || true
|
||||||
|
umount -lf "$ROOTFS_DIR/dev" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup_mounts EXIT
|
||||||
|
|
||||||
echo "[+] Building AeThex application layer..."
|
echo "[+] Building AeThex application layer..."
|
||||||
mount -t proc /proc "$ROOTFS_DIR/proc" || true
|
mkdir -p "$ROOTFS_DIR/proc" "$ROOTFS_DIR/sys" "$ROOTFS_DIR/dev/pts"
|
||||||
mount -t sysfs /sys "$ROOTFS_DIR/sys" || true
|
mount -t proc proc "$ROOTFS_DIR/proc" || true
|
||||||
|
mount -t sysfs sys "$ROOTFS_DIR/sys" || true
|
||||||
mount --bind /dev "$ROOTFS_DIR/dev" || true
|
mount --bind /dev "$ROOTFS_DIR/dev" || true
|
||||||
|
mount --bind /dev/pts "$ROOTFS_DIR/dev/pts" || true
|
||||||
|
|
||||||
echo "[+] Installing Xfce desktop, Firefox, and system tools..."
|
echo "[+] Installing Xfce desktop, Firefox, and system tools..."
|
||||||
echo " (packages installing, ~15-20 minutes...)"
|
echo " (packages installing, ~15-20 minutes...)"
|
||||||
|
|
@ -53,6 +64,7 @@ chroot "$ROOTFS_DIR" bash -c '
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
linux-image-generic \
|
linux-image-generic \
|
||||||
grub-pc-bin grub-efi-amd64-bin grub-common xorriso \
|
grub-pc-bin grub-efi-amd64-bin grub-common xorriso \
|
||||||
|
casper live-boot live-boot-initramfs-tools \
|
||||||
xorg xfce4 xfce4-goodies lightdm \
|
xorg xfce4 xfce4-goodies lightdm \
|
||||||
firefox network-manager \
|
firefox network-manager \
|
||||||
sudo curl wget git ca-certificates gnupg \
|
sudo curl wget git ca-certificates gnupg \
|
||||||
|
|
@ -61,6 +73,12 @@ chroot "$ROOTFS_DIR" bash -c '
|
||||||
xfce4-terminal mousepad ristretto \
|
xfce4-terminal mousepad ristretto \
|
||||||
dbus-x11
|
dbus-x11
|
||||||
apt-get clean
|
apt-get clean
|
||||||
|
|
||||||
|
# Verify kernel was installed
|
||||||
|
if ! ls /boot/vmlinuz-* 2>/dev/null | grep -q .; then
|
||||||
|
echo "ERROR: linux-image-generic failed to install!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
' 2>&1 | tail -50
|
' 2>&1 | tail -50
|
||||||
|
|
||||||
echo "[+] Installing Node.js 20.x from NodeSource..."
|
echo "[+] Installing Node.js 20.x from NodeSource..."
|
||||||
|
|
@ -203,27 +221,43 @@ echo " - Firefox launches in kiosk mode"
|
||||||
echo " - Xfce desktop with auto-login"
|
echo " - Xfce desktop with auto-login"
|
||||||
echo " - Ingress-style mobile interface"
|
echo " - Ingress-style mobile interface"
|
||||||
|
|
||||||
echo "[+] Extracting kernel and initrd..."
|
echo "[+] Extracting kernel and initrd from rootfs..."
|
||||||
KERNEL="$(ls -1 $ROOTFS_DIR/boot/vmlinuz-* 2>/dev/null | head -n 1)"
|
KERNEL="$(ls -1 $ROOTFS_DIR/boot/vmlinuz-* 2>/dev/null | head -n 1)"
|
||||||
INITRD="$(ls -1 $ROOTFS_DIR/boot/initrd.img-* 2>/dev/null | head -n 1)"
|
INITRD="$(ls -1 $ROOTFS_DIR/boot/initrd.img-* 2>/dev/null | head -n 1)"
|
||||||
|
|
||||||
if [ -z "$KERNEL" ] || [ -z "$INITRD" ]; then
|
if [ -z "$KERNEL" ] || [ -z "$INITRD" ]; then
|
||||||
echo "[!] Kernel or initrd not found."
|
echo "[!] FATAL: Kernel or initrd not found in $ROOTFS_DIR/boot/"
|
||||||
|
echo "[!] Contents of $ROOTFS_DIR/boot/:"
|
||||||
ls -la "$ROOTFS_DIR/boot/" || true
|
ls -la "$ROOTFS_DIR/boot/" || true
|
||||||
mkdir -p "$BUILD_DIR"
|
|
||||||
echo "No kernel found in rootfs" > "$BUILD_DIR/README.txt"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp "$KERNEL" "$ISO_DIR/casper/vmlinuz"
|
echo "[+] Copying kernel and initrd to $ISO_DIR/casper/..."
|
||||||
cp "$INITRD" "$ISO_DIR/casper/initrd.img"
|
cp -v "$KERNEL" "$ISO_DIR/casper/vmlinuz" || { echo "[!] Failed to copy kernel"; exit 1; }
|
||||||
echo "[✓] Kernel: $(basename "$KERNEL")"
|
cp -v "$INITRD" "$ISO_DIR/casper/initrd.img" || { echo "[!] Failed to copy initrd"; exit 1; }
|
||||||
echo "[✓] Initrd: $(basename "$INITRD")"
|
|
||||||
|
# Verify files exist
|
||||||
|
if [ ! -f "$ISO_DIR/casper/vmlinuz" ]; then
|
||||||
|
echo "[!] ERROR: vmlinuz not found after copy"
|
||||||
|
ls -la "$ISO_DIR/casper/" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$ISO_DIR/casper/initrd.img" ]; then
|
||||||
|
echo "[!] ERROR: initrd.img not found after copy"
|
||||||
|
ls -la "$ISO_DIR/casper/" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[✓] Kernel: $(basename "$KERNEL") ($(du -h "$ISO_DIR/casper/vmlinuz" | cut -f1))"
|
||||||
|
echo "[✓] Initrd: $(basename "$INITRD") ($(du -h "$ISO_DIR/casper/initrd.img" | cut -f1))"
|
||||||
|
echo "[✓] Initrd -> $ISO_DIR/casper/initrd.img"
|
||||||
|
echo "[✓] Files verified in ISO directory"
|
||||||
|
|
||||||
# Unmount before squashfs
|
# Unmount before squashfs
|
||||||
echo "[+] Unmounting chroot filesystems..."
|
echo "[+] Unmounting chroot filesystems..."
|
||||||
umount -lf "$ROOTFS_DIR/proc" 2>/dev/null || true
|
umount -lf "$ROOTFS_DIR/proc" 2>/dev/null || true
|
||||||
umount -lf "$ROOTFS_DIR/sys" 2>/dev/null || true
|
umount -lf "$ROOTFS_DIR/sys" 2>/dev/null || true
|
||||||
|
umount -lf "$ROOTFS_DIR/dev/pts" 2>/dev/null || true
|
||||||
umount -lf "$ROOTFS_DIR/dev" 2>/dev/null || true
|
umount -lf "$ROOTFS_DIR/dev" 2>/dev/null || true
|
||||||
|
|
||||||
echo "[+] Creating SquashFS filesystem..."
|
echo "[+] Creating SquashFS filesystem..."
|
||||||
|
|
@ -237,36 +271,135 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[+] Setting up BIOS boot (isolinux)..."
|
echo "[+] Final verification before ISO creation..."
|
||||||
cat > "$ISO_DIR/isolinux/isolinux.cfg" << 'EOF'
|
for file in "$ISO_DIR/casper/vmlinuz" "$ISO_DIR/casper/initrd.img" "$ISO_DIR/casper/filesystem.squashfs"; do
|
||||||
PROMPT 0
|
if [ ! -f "$file" ]; then
|
||||||
TIMEOUT 50
|
echo "[!] CRITICAL: Missing $file"
|
||||||
DEFAULT linux
|
echo "[!] ISO directory contents:"
|
||||||
|
find "$ISO_DIR" -type f 2>/dev/null | head -20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[✓] $(basename "$file") - $(du -h "$file" | cut -f1)"
|
||||||
|
done
|
||||||
|
echo "[+] Final verification before ISO creation..."
|
||||||
|
for f in "$ISO_DIR/casper/vmlinuz" "$ISO_DIR/casper/initrd.img" "$ISO_DIR/casper/filesystem.squashfs"; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "[!] CRITICAL: Missing $f"
|
||||||
|
ls -la "$ISO_DIR/casper/" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[✓] $(basename "$f") $(du -h "$f" | awk '{print $1}')"
|
||||||
|
done
|
||||||
|
|
||||||
LABEL linux
|
echo "[+] Creating live boot manifest..."
|
||||||
MENU LABEL AeThex OS
|
printf $(du -sx --block-size=1 "$ROOTFS_DIR" | cut -f1) > "$ISO_DIR/casper/filesystem.size"
|
||||||
|
cat > "$ISO_DIR/casper/filesystem.manifest" << 'MANIFEST'
|
||||||
|
casper
|
||||||
|
live-boot
|
||||||
|
live-boot-initramfs-tools
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
echo "[+] Setting up BIOS boot (isolinux)..."
|
||||||
|
mkdir -p "$ISO_DIR/isolinux"
|
||||||
|
cat > "$ISO_DIR/isolinux/isolinux.cfg" << 'EOF'
|
||||||
|
DEFAULT vesamenu.c32
|
||||||
|
PROMPT 0
|
||||||
|
TIMEOUT 100
|
||||||
|
|
||||||
|
LABEL live
|
||||||
|
MENU LABEL ^AeThex OS
|
||||||
KERNEL /casper/vmlinuz
|
KERNEL /casper/vmlinuz
|
||||||
APPEND initrd=/casper/initrd.img boot=casper quiet splash
|
APPEND initrd=/casper/initrd.img root=/dev/sr0 ro live-media=/dev/sr0 boot=live config ip=dhcp live-config hostname=aethex
|
||||||
|
|
||||||
|
LABEL safe
|
||||||
|
MENU LABEL AeThex OS (^Safe Mode)
|
||||||
|
KERNEL /casper/vmlinuz
|
||||||
|
APPEND initrd=/casper/initrd.img root=/dev/sr0 ro live-media=/dev/sr0 boot=live nomodeset config ip=dhcp live-config hostname=aethex
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cp /usr/lib/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || \
|
# Copy syslinux binaries
|
||||||
cp /usr/share/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || echo "[!] isolinux.bin missing"
|
for src in /usr/lib/syslinux/modules/bios /usr/lib/ISOLINUX /usr/share/syslinux; do
|
||||||
cp /usr/lib/syslinux/ldlinux.c32 "$ISO_DIR/isolinux/" 2>/dev/null || \
|
if [ -f "$src/isolinux.bin" ]; then
|
||||||
cp /usr/share/syslinux/ldlinux.c32 "$ISO_DIR/isolinux/" 2>/dev/null || echo "[!] ldlinux.c32 missing"
|
cp "$src/isolinux.bin" "$ISO_DIR/isolinux/" 2>/dev/null
|
||||||
|
cp "$src/ldlinux.c32" "$ISO_DIR/isolinux/" 2>/dev/null || true
|
||||||
|
cp "$src/vesamenu.c32" "$ISO_DIR/isolinux/" 2>/dev/null || true
|
||||||
|
cp "$src/libcom32.c32" "$ISO_DIR/isolinux/" 2>/dev/null || true
|
||||||
|
cp "$src/libutil.c32" "$ISO_DIR/isolinux/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo "[+] Setting up UEFI boot (GRUB)..."
|
echo "[+] Setting up UEFI boot (GRUB)..."
|
||||||
|
mkdir -p "$ISO_DIR/boot/grub"
|
||||||
cat > "$ISO_DIR/boot/grub/grub.cfg" << 'EOF'
|
cat > "$ISO_DIR/boot/grub/grub.cfg" << 'EOF'
|
||||||
set timeout=10
|
set timeout=10
|
||||||
set default=0
|
set default=0
|
||||||
|
|
||||||
menuentry "AeThex OS" {
|
menuentry "AeThex OS" {
|
||||||
linux /casper/vmlinuz boot=casper quiet splash
|
linux /casper/vmlinuz root=/dev/sr0 ro live-media=/dev/sr0 boot=live config ip=dhcp live-config hostname=aethex
|
||||||
|
initrd /casper/initrd.img
|
||||||
|
}
|
||||||
|
|
||||||
|
menuentry "AeThex OS (safe mode)" {
|
||||||
|
linux /casper/vmlinuz root=/dev/sr0 ro live-media=/dev/sr0 boot=live nomodeset config ip=dhcp live-config hostname=aethex
|
||||||
initrd /casper/initrd.img
|
initrd /casper/initrd.img
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "[+] Creating hybrid ISO with grub-mkrescue..."
|
echo "[+] Verifying ISO structure before xorriso..."
|
||||||
grub-mkrescue -o "$BUILD_DIR/$ISO_NAME" "$ISO_DIR" --verbose 2>&1 | tail -20
|
echo "[*] Checking ISO_DIR contents:"
|
||||||
|
ls -lh "$ISO_DIR/" || echo "ISO_DIR missing!"
|
||||||
|
echo "[*] Checking casper contents:"
|
||||||
|
ls -lh "$ISO_DIR/casper/" || echo "casper dir missing!"
|
||||||
|
echo "[*] Checking isolinux contents:"
|
||||||
|
ls -lh "$ISO_DIR/isolinux/" || echo "isolinux dir missing!"
|
||||||
|
|
||||||
|
if [ ! -f "$ISO_DIR/casper/vmlinuz" ]; then
|
||||||
|
echo "[!] CRITICAL: vmlinuz not in ISO_DIR/casper!"
|
||||||
|
find "$ISO_DIR" -name "vmlinuz*" 2>/dev/null || echo "vmlinuz not found anywhere in ISO_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$ISO_DIR/casper/initrd.img" ]; then
|
||||||
|
echo "[!] CRITICAL: initrd.img not in ISO_DIR/casper!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[✓] All casper files verified in place"
|
||||||
|
|
||||||
|
echo "[+] Creating EFI boot image..."
|
||||||
|
mkdir -p "$ISO_DIR/EFI/boot"
|
||||||
|
grub-mkstandalone \
|
||||||
|
--format=x86_64-efi \
|
||||||
|
--output="$ISO_DIR/EFI/boot/bootx64.efi" \
|
||||||
|
--locales="" \
|
||||||
|
--fonts="" \
|
||||||
|
"boot/grub/grub.cfg=$ISO_DIR/boot/grub/grub.cfg" 2>&1 | tail -5
|
||||||
|
|
||||||
|
# Create EFI image for ISO
|
||||||
|
dd if=/dev/zero of="$ISO_DIR/boot/grub/efi.img" bs=1M count=10 2>/dev/null
|
||||||
|
mkfs.vfat "$ISO_DIR/boot/grub/efi.img" >/dev/null 2>&1
|
||||||
|
EFI_MOUNT=$(mktemp -d)
|
||||||
|
mount -o loop "$ISO_DIR/boot/grub/efi.img" "$EFI_MOUNT"
|
||||||
|
mkdir -p "$EFI_MOUNT/EFI/boot"
|
||||||
|
cp "$ISO_DIR/EFI/boot/bootx64.efi" "$EFI_MOUNT/EFI/boot/"
|
||||||
|
umount "$EFI_MOUNT"
|
||||||
|
rmdir "$EFI_MOUNT"
|
||||||
|
|
||||||
|
echo "[+] Creating hybrid ISO with xorriso (El Torito boot)..."
|
||||||
|
xorriso -as mkisofs \
|
||||||
|
-iso-level 3 \
|
||||||
|
-full-iso9660-filenames \
|
||||||
|
-volid "AeThex-OS" \
|
||||||
|
-eltorito-boot isolinux/isolinux.bin \
|
||||||
|
-eltorito-catalog isolinux/boot.cat \
|
||||||
|
-no-emul-boot -boot-load-size 4 -boot-info-table \
|
||||||
|
-eltorito-alt-boot \
|
||||||
|
-e boot/grub/efi.img \
|
||||||
|
-no-emul-boot \
|
||||||
|
-isohybrid-mbr /usr/lib/ISOLINUX/isohdpfx.bin \
|
||||||
|
-isohybrid-gpt-basdat \
|
||||||
|
-output "$BUILD_DIR/$ISO_NAME" \
|
||||||
|
"$ISO_DIR" 2>&1 | tail -20
|
||||||
|
|
||||||
echo "[+] Computing SHA256 checksum..."
|
echo "[+] Computing SHA256 checksum..."
|
||||||
if [ -f "$BUILD_DIR/$ISO_NAME" ]; then
|
if [ -f "$BUILD_DIR/$ISO_NAME" ]; then
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,14 @@ const allowlist = [
|
||||||
async function buildAll() {
|
async function buildAll() {
|
||||||
await rm("dist", { recursive: true, force: true });
|
await rm("dist", { recursive: true, force: true });
|
||||||
|
|
||||||
|
const enableSourcemap = process.argv.includes("--sourcemap");
|
||||||
|
|
||||||
console.log("building client...");
|
console.log("building client...");
|
||||||
await viteBuild();
|
await viteBuild({
|
||||||
|
build: {
|
||||||
|
sourcemap: enableSourcemap,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
console.log("building server...");
|
console.log("building server...");
|
||||||
const pkg = JSON.parse(await readFile("package.json", "utf-8"));
|
const pkg = JSON.parse(await readFile("package.json", "utf-8"));
|
||||||
|
|
@ -56,6 +62,7 @@ async function buildAll() {
|
||||||
"process.env.NODE_ENV": '"production"',
|
"process.env.NODE_ENV": '"production"',
|
||||||
},
|
},
|
||||||
minify: true,
|
minify: true,
|
||||||
|
sourcemap: enableSourcemap,
|
||||||
external: externals,
|
external: externals,
|
||||||
logLevel: "info",
|
logLevel: "info",
|
||||||
banner: {
|
banner: {
|
||||||
|
|
|
||||||
160
script/verify-iso.sh
Normal file
160
script/verify-iso.sh
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat << 'USAGE'
|
||||||
|
Usage: ./script/verify-iso.sh -i <path/to.iso> [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-i <path> Path to ISO file
|
||||||
|
-s <sha256> Expected SHA256 hash (overrides .sha256 file lookup)
|
||||||
|
--mount Mount ISO to verify contents (requires sudo/root)
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./script/verify-iso.sh -i aethex-linux-build/AeThex-Linux-amd64.iso
|
||||||
|
./script/verify-iso.sh -i AeThex-OS-Full-amd64.iso -s <sha256>
|
||||||
|
./script/verify-iso.sh -i AeThex-Linux-amd64.iso --mount
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
ISO=""
|
||||||
|
EXPECTED_SHA=""
|
||||||
|
MOUNT_CHECK=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-i)
|
||||||
|
ISO="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-s)
|
||||||
|
EXPECTED_SHA="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--mount)
|
||||||
|
MOUNT_CHECK=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown arg: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$ISO" ]]; then
|
||||||
|
echo "Missing ISO path." >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$ISO" ]]; then
|
||||||
|
echo "ISO not found: $ISO" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ISO_DIR=$(cd "$(dirname "$ISO")" && pwd)
|
||||||
|
ISO_BASE=$(basename "$ISO")
|
||||||
|
|
||||||
|
printf "\n[+] Verifying ISO: %s\n" "$ISO"
|
||||||
|
ls -lh "$ISO"
|
||||||
|
|
||||||
|
SHA_CALC=$(sha256sum "$ISO" | awk '{print $1}')
|
||||||
|
printf "SHA256 (calculated): %s\n" "$SHA_CALC"
|
||||||
|
|
||||||
|
if [[ -n "$EXPECTED_SHA" ]]; then
|
||||||
|
if [[ "$SHA_CALC" == "$EXPECTED_SHA" ]]; then
|
||||||
|
echo "[✓] SHA256 matches provided value."
|
||||||
|
else
|
||||||
|
echo "[!] SHA256 mismatch. Expected: $EXPECTED_SHA" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -f "$ISO_DIR/$ISO_BASE.sha256" ]]; then
|
||||||
|
echo "[+] Found checksum file: $ISO_DIR/$ISO_BASE.sha256"
|
||||||
|
(cd "$ISO_DIR" && sha256sum -c "$ISO_BASE.sha256")
|
||||||
|
elif [[ -f "$ISO_DIR/$ISO_BASE.sha256.txt" ]]; then
|
||||||
|
echo "[+] Found checksum file: $ISO_DIR/$ISO_BASE.sha256.txt"
|
||||||
|
(cd "$ISO_DIR" && sha256sum -c "$ISO_BASE.sha256.txt")
|
||||||
|
else
|
||||||
|
echo "[!] No checksum file found; provide one with -s to enforce." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_path() {
|
||||||
|
local label="$1"
|
||||||
|
local needle="$2"
|
||||||
|
|
||||||
|
if command -v xorriso >/dev/null 2>&1; then
|
||||||
|
if xorriso -indev "$ISO" -find / -name "$(basename "$needle")" -print 2>/dev/null | grep -q "$needle"; then
|
||||||
|
echo "[✓] $label: $needle"
|
||||||
|
else
|
||||||
|
echo "[!] Missing $label: $needle" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v isoinfo >/dev/null 2>&1; then
|
||||||
|
if isoinfo -i "$ISO" -f 2>/dev/null | grep -q "^$needle$"; then
|
||||||
|
echo "[✓] $label: $needle"
|
||||||
|
else
|
||||||
|
echo "[!] Missing $label: $needle" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[!] No ISO inspection tool found (xorriso/isoinfo). Skipping: $label" >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
check_path "Kernel" "/casper/vmlinuz" || FAIL=1
|
||||||
|
check_path "Initrd" "/casper/initrd.img" || FAIL=1
|
||||||
|
check_path "SquashFS" "/casper/filesystem.squashfs" || FAIL=1
|
||||||
|
check_path "GRUB config" "/boot/grub/grub.cfg" || FAIL=1
|
||||||
|
check_path "ISOLINUX config" "/isolinux/isolinux.cfg" || FAIL=1
|
||||||
|
|
||||||
|
if [[ "$MOUNT_CHECK" -eq 1 ]]; then
|
||||||
|
MOUNT_DIR=$(mktemp -d)
|
||||||
|
cleanup() {
|
||||||
|
if mountpoint -q "$MOUNT_DIR"; then
|
||||||
|
sudo umount "$MOUNT_DIR" || true
|
||||||
|
fi
|
||||||
|
rmdir "$MOUNT_DIR" || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "[+] Mounting ISO to $MOUNT_DIR"
|
||||||
|
if [[ $EUID -eq 0 ]]; then
|
||||||
|
mount -o loop "$ISO" "$MOUNT_DIR"
|
||||||
|
else
|
||||||
|
sudo mount -o loop "$ISO" "$MOUNT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for path in \
|
||||||
|
"$MOUNT_DIR/boot/grub/grub.cfg" \
|
||||||
|
"$MOUNT_DIR/isolinux/isolinux.cfg" \
|
||||||
|
"$MOUNT_DIR/casper/filesystem.squashfs"; do
|
||||||
|
if [[ -f "$path" ]]; then
|
||||||
|
echo "[✓] Mounted file present: $path"
|
||||||
|
else
|
||||||
|
echo "[!] Missing mounted file: $path" >&2
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FAIL" -eq 1 ]]; then
|
||||||
|
echo "\n[!] ISO verification failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "\n[✓] ISO verification complete."
|
||||||
1
wget-log
Normal file
1
wget-log
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
2025-12-29 09:54:25 URL:http://archive.ubuntu.com/ubuntu/dists/jammy/InRelease [270087/270087] -> "/home/mrpiglr/aethex-build/aethex-linux-build/rootfs/var/lib/apt/lists/partial/archive.ubuntu.com_ubuntu_dists_jammy_InRelease" [1]
|
||||||
1
wget-log.1
Normal file
1
wget-log.1
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
2025-12-29 09:54:27 URL:http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/by-hash/SHA256/37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11 [1394768/1394768] -> "/home/mrpiglr/aethex-build/aethex-linux-build/rootfs/var/lib/apt/lists/partial/archive.ubuntu.com_ubuntu_dists_jammy_main_binary-amd64_Packages.xz" [1]
|
||||||
1
wget-log.2
Normal file
1
wget-log.2
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
2025-12-30 03:11:32 URL:http://archive.ubuntu.com/ubuntu/dists/jammy/InRelease [270087/270087] -> "/home/mrpiglr/aethex-build/aethex-linux-build/rootfs/var/lib/apt/lists/partial/archive.ubuntu.com_ubuntu_dists_jammy_InRelease" [1]
|
||||||
1
wget-log.3
Normal file
1
wget-log.3
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
2025-12-30 03:11:34 URL:http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/by-hash/SHA256/37cb57f1554cbfa71c5a29ee9ffee18a9a8c1782bb0568e0874b7ff4ce8f9c11 [1394768/1394768] -> "/home/mrpiglr/aethex-build/aethex-linux-build/rootfs/var/lib/apt/lists/partial/archive.ubuntu.com_ubuntu_dists_jammy_main_binary-amd64_Packages.xz" [1]
|
||||||
Loading…
Reference in a new issue