mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 14:17:21 +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.runDevServer": 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
|
||||
- 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
|
||||
|
||||
```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>
|
||||
<SelectionState runConfigName="app">
|
||||
<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">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R5CW217D49H" />
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<targets>
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\PCOEM\.android\avd\Medium_Phone.avd" />
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=R5CW217D49H" />
|
||||
</handle>
|
||||
</Target>
|
||||
</targets>
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ android {
|
|||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
// flatDir{
|
||||
// dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
// }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -36,10 +36,13 @@ dependencies {
|
|||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
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"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
// implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:windowLayoutInDisplayCutoutMode="shortEdges">
|
||||
android:windowLayoutInDisplayCutoutMode="shortEdges"
|
||||
android:screenOrientation="portrait">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
|
|
|||
|
|
@ -1,35 +1,79 @@
|
|||
package com.aethex.os;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import com.google.firebase.FirebaseApp;
|
||||
import com.google.firebase.FirebaseOptions;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Enable edge-to-edge display
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
// Get window insets controller
|
||||
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||
|
||||
if (controller != null) {
|
||||
// Hide system bars (status bar and navigation bar)
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||
|
||||
// Set behavior for when user swipes to show system bars
|
||||
controller.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
);
|
||||
}
|
||||
|
||||
// Keep screen on for gaming/OS experience
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Enable fullscreen immersive mode
|
||||
enableImmersiveMode();
|
||||
|
||||
// Ensure Firebase is ready before any Capacitor plugin requests it; stay resilient if config is missing
|
||||
try {
|
||||
if (FirebaseApp.getApps(this).isEmpty()) {
|
||||
FirebaseOptions options = null;
|
||||
try {
|
||||
options = FirebaseOptions.fromResource(this);
|
||||
} catch (Exception ignored) {
|
||||
// No google-services.json resources, we'll fall back below
|
||||
}
|
||||
|
||||
if (options != null) {
|
||||
FirebaseApp.initializeApp(getApplicationContext(), options);
|
||||
} else {
|
||||
// Minimal placeholder so Firebase-dependent plugins don't crash when config is absent
|
||||
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,
|
||||
splashImmersive: true
|
||||
},
|
||||
StatusBar: {
|
||||
style: 'DARK',
|
||||
backgroundColor: '#000000',
|
||||
overlaysWebView: true
|
||||
},
|
||||
App: {
|
||||
backButtonEnabled: true
|
||||
},
|
||||
PushNotifications: {
|
||||
presentationOptions: ['badge', 'sound', 'alert']
|
||||
},
|
||||
|
|
@ -30,7 +38,13 @@ const config: CapacitorConfig = {
|
|||
iconColor: '#488AFF',
|
||||
sound: 'beep.wav'
|
||||
}
|
||||
},
|
||||
android: {
|
||||
allowMixedContent: true,
|
||||
captureInput: true,
|
||||
webContentsDebuggingEnabled: true
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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="apple-touch-icon" href="/favicon.png" />
|
||||
<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="apple-mobile-web-app-capable" content="yes" />
|
||||
<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.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">
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<!-- Unregister any existing service workers -->
|
||||
<!-- Register service worker for PWA -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for(let registration of registrations) {
|
||||
registration.unregister();
|
||||
console.log('Unregistered service worker:', registration.scope);
|
||||
}
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('[SW] Registered:', registration.scope);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('[SW] Registration failed:', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
</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.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0F172A",
|
||||
"theme_color": "#06B6D4",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#10b981",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
|
|
@ -13,10 +13,42 @@
|
|||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["productivity", "utilities", "developer", "entertainment"],
|
||||
"prefer_related_applications": false,
|
||||
"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
|
||||
// This file unregisters any existing service workers
|
||||
// Service Worker for PWA functionality
|
||||
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();
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
self.registration.unregister().then(() => {
|
||||
return self.clients.matchAll();
|
||||
}).then((clients) => {
|
||||
clients.forEach(client => client.navigate(client.url));
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
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 Admin from "@/pages/admin";
|
||||
import Pitch from "@/pages/pitch";
|
||||
import Builds from "@/pages/builds";
|
||||
import AdminArchitects from "@/pages/admin-architects";
|
||||
import AdminProjects from "@/pages/admin-projects";
|
||||
import AdminCredentials from "@/pages/admin-credentials";
|
||||
|
|
@ -40,12 +41,31 @@ import HubCodeGallery from "@/pages/hub/code-gallery";
|
|||
import HubNotifications from "@/pages/hub/notifications";
|
||||
import HubAnalytics from "@/pages/hub/analytics";
|
||||
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";
|
||||
|
||||
|
||||
function HomeRoute() {
|
||||
// On mobile devices, show the native mobile app
|
||||
// On desktop/web, show the web OS
|
||||
return <AeThexOS />;
|
||||
}
|
||||
|
||||
function Router() {
|
||||
return (
|
||||
<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="/passport" component={Passport} />
|
||||
<Route path="/achievements" component={Achievements} />
|
||||
|
|
@ -67,6 +87,7 @@ function Router() {
|
|||
<Route path="/admin/activity">{() => <ProtectedRoute><AdminActivity /></ProtectedRoute>}</Route>
|
||||
<Route path="/admin/notifications">{() => <ProtectedRoute><AdminNotifications /></ProtectedRoute>}</Route>
|
||||
<Route path="/pitch" component={Pitch} />
|
||||
<Route path="/builds" component={Builds} />
|
||||
<Route path="/os" component={AeThexOS} />
|
||||
<Route path="/os/link">{() => <ProtectedRoute><OsLink /></ProtectedRoute>}</Route>
|
||||
<Route path="/network" component={Network} />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from "react";
|
|||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { MessageCircle, X, Send, Bot, User, Loader2 } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
import { isMobile } from "@/lib/platform";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
|
|
@ -63,7 +64,7 @@ export function Chatbot() {
|
|||
};
|
||||
|
||||
// Don't render chatbot on the OS page - it has its own environment
|
||||
if (location === "/os") {
|
||||
if (location === "/os" || isMobile()) {
|
||||
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 { motion } from "framer-motion";
|
||||
import { Link } from "wouter";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Shield, FileCode, Terminal as TerminalIcon, ChevronRight, BarChart3, Network,
|
||||
|
|
@ -13,6 +13,7 @@ import { ThemeToggle } from "@/components/ThemeToggle";
|
|||
|
||||
export default function Home() {
|
||||
const { startTutorial, hasCompletedTutorial, isActive } = useTutorial();
|
||||
const [, navigate] = useLocation();
|
||||
|
||||
const { data: metrics } = useQuery({
|
||||
queryKey: ["metrics"],
|
||||
|
|
@ -24,6 +25,13 @@ export default function Home() {
|
|||
|
||||
return (
|
||||
<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
|
||||
className="absolute inset-0 opacity-20 pointer-events-none z-0"
|
||||
style={{ backgroundImage: `url(${gridBg})`, backgroundSize: 'cover' }}
|
||||
|
|
|
|||
|
|
@ -78,54 +78,67 @@ export default function Marketplace() {
|
|||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||
{/* 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="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-white">Marketplace</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700">
|
||||
<p className="text-sm text-slate-400">Balance</p>
|
||||
<p className="text-xl font-bold text-cyan-400">{balance} LP</p>
|
||||
<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 justify-between gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
|
||||
<Link href="/">
|
||||
<button className="text-slate-400 hover:text-white shrink-0">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Link>
|
||||
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4 shrink-0">
|
||||
<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-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>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Sell Item
|
||||
</Button>
|
||||
</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 */}
|
||||
<Tabs value={selectedCategory} onValueChange={setSelectedCategory} className="mb-6">
|
||||
<TabsList className="bg-slate-800 border-b border-slate-700">
|
||||
<TabsTrigger value="all" className="text-slate-300">
|
||||
<TabsList className="bg-slate-800 border-b border-slate-700 w-full overflow-x-auto flex-nowrap">
|
||||
<TabsTrigger value="all" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||
All Items
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code" className="text-slate-300">
|
||||
Code & Snippets
|
||||
<TabsTrigger value="code" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||
Code
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="achievement" className="text-slate-300">
|
||||
<TabsTrigger value="achievement" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||
Achievements
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="service" className="text-slate-300">
|
||||
<TabsTrigger value="service" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||
Services
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="credential" className="text-slate-300">
|
||||
<TabsTrigger value="credential" className="text-slate-300 text-xs md:text-sm whitespace-nowrap">
|
||||
Credentials
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<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) => (
|
||||
<Card
|
||||
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 */}
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
|
|
@ -139,13 +152,13 @@ export default function Marketplace() {
|
|||
</div>
|
||||
|
||||
{/* 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}
|
||||
</h3>
|
||||
|
||||
{/* Seller Info */}
|
||||
<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>
|
||||
|
||||
{/* Rating & Purchases */}
|
||||
|
|
@ -158,30 +171,31 @@ export default function Marketplace() {
|
|||
</div>
|
||||
|
||||
{/* Price & Button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-2xl font-bold text-cyan-400">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-xl md:text-2xl font-bold text-cyan-400">
|
||||
{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>
|
||||
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2 h-9 px-3">
|
||||
<ShoppingCart className="w-4 h-4" />
|
||||
<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-3 h-3 md:w-4 md:h-4" />
|
||||
Buy
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Featured Section */}
|
||||
<div className="mt-12">
|
||||
<h2 className="text-2xl font-bold text-white mb-4">Featured Sellers</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<h2 className="text-xl md:text-2xl font-bold text-white mb-4">Featured Sellers</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 md:gap-4">
|
||||
{["CodeMaster", "TechGuru", "AchievmentHunter"].map((seller) => (
|
||||
<Card
|
||||
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="w-12 h-12 rounded-full bg-cyan-600 mx-auto mb-3"></div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Link } from "wouter";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
|
||||
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { nanoid } from "nanoid";
|
||||
|
|
@ -97,8 +98,13 @@ export default function Messaging() {
|
|||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-slate-900">
|
||||
{/* Header */}
|
||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
||||
{/* Mobile Header */}
|
||||
<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="/">
|
||||
<button className="text-slate-400 hover:text-white">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Link } from "wouter";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
|
||||
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { nanoid } from "nanoid";
|
||||
|
|
@ -103,8 +104,13 @@ export default function Projects() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||
{/* 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">
|
||||
{/* Mobile Header */}
|
||||
<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">
|
||||
<Link href="/">
|
||||
<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",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bufferutil": "4.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
|
|
@ -127,6 +126,7 @@
|
|||
"concurrently": "^9.2.1",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"esbuild": "^0.25.0",
|
||||
"playwright-chromium": "^1.57.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.20.5",
|
||||
|
|
@ -7644,6 +7644,36 @@
|
|||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"concurrently": "^9.2.1",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"esbuild": "^0.25.0",
|
||||
"playwright-chromium": "^1.57.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.20.5",
|
||||
|
|
|
|||
|
|
@ -35,13 +35,13 @@ done
|
|||
|
||||
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 "[+] Bootstrapping Ubuntu 24.04 base system..."
|
||||
echo "[+] Bootstrapping Ubuntu 22.04 base system (older kernel 5.15)..."
|
||||
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 "aethex-os" > "$ROOTFS_DIR/etc/hostname"
|
||||
|
|
@ -62,18 +62,19 @@ chroot "$ROOTFS_DIR" bash -c '
|
|||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 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 noble-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 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-audio wireplumber \
|
||||
pipewire wireplumber \
|
||||
xorg xserver-xorg-video-all \
|
||||
xfce4 xfce4-goodies lightdm \
|
||||
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
|
||||
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 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 "[+] 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)"
|
||||
|
|
@ -334,6 +343,13 @@ 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
|
||||
|
|
@ -354,7 +370,12 @@ DEFAULT linux
|
|||
LABEL linux
|
||||
MENU LABEL AeThex OS - Full Stack
|
||||
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
|
||||
|
||||
cp /usr/lib/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || \
|
||||
|
|
@ -368,12 +389,17 @@ set timeout=10
|
|||
set default=0
|
||||
|
||||
menuentry "AeThex OS - Full Stack" {
|
||||
linux /casper/vmlinuz boot=casper quiet splash
|
||||
linux /casper/vmlinuz boot=casper quiet splash ---
|
||||
initrd /casper/initrd.img
|
||||
}
|
||||
|
||||
menuentry "AeThex OS - Safe Mode" {
|
||||
linux /casper/vmlinuz boot=casper nomodeset
|
||||
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
|
||||
|
|
@ -408,9 +434,10 @@ echo "┃ AeThex OS - Full Stack Edition ┃"
|
|||
echo "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
|
||||
echo ""
|
||||
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: 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:"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
set -e
|
||||
|
||||
# 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:-.}"
|
||||
BUILD_DIR="$WORK_DIR/aethex-linux-build"
|
||||
ROOTFS_DIR="$BUILD_DIR/rootfs"
|
||||
ISO_DIR="$BUILD_DIR/iso"
|
||||
ISO_NAME="AeThex-Linux-amd64.iso"
|
||||
|
||||
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"
|
||||
|
||||
# Clean and prepare
|
||||
rm -rf "$BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR"/{iso,rootfs}
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
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
|
||||
echo "[*] Checking dependencies..."
|
||||
for cmd in xorriso genisoimage mksquashfs; 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
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq \
|
||||
debootstrap squashfs-tools xorriso grub-common grub-pc-bin grub-efi-amd64-bin \
|
||||
syslinux-common isolinux mtools dosfstools wget ca-certificates 2>&1 | tail -10
|
||||
|
||||
echo "[+] Downloading Ubuntu Mini ISO base..."
|
||||
# Use Ubuntu mini.iso as base (much smaller, pre-built)
|
||||
if [ ! -f "$BUILD_DIR/ubuntu-mini.iso" ]; then
|
||||
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 \
|
||||
|| echo "[!] Download failed, creating minimal ISO instead"
|
||||
fi
|
||||
echo "[+] Bootstrapping Ubuntu base (noble)..."
|
||||
debootstrap --arch=amd64 noble "$ROOTFS_DIR" http://archive.ubuntu.com/ubuntu/
|
||||
cp /etc/resolv.conf "$ROOTFS_DIR/etc/resolv.conf"
|
||||
|
||||
cleanup_mounts() {
|
||||
umount -lf "$ROOTFS_DIR/proc" 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
|
||||
}
|
||||
trap cleanup_mounts EXIT
|
||||
|
||||
echo "[+] Building AeThex application layer..."
|
||||
mount -t proc /proc "$ROOTFS_DIR/proc" || true
|
||||
mount -t sysfs /sys "$ROOTFS_DIR/sys" || true
|
||||
mkdir -p "$ROOTFS_DIR/proc" "$ROOTFS_DIR/sys" "$ROOTFS_DIR/dev/pts"
|
||||
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/pts "$ROOTFS_DIR/dev/pts" || true
|
||||
|
||||
echo "[+] Installing Xfce desktop, Firefox, and system tools..."
|
||||
echo " (packages installing, ~15-20 minutes...)"
|
||||
|
|
@ -53,6 +64,7 @@ chroot "$ROOTFS_DIR" bash -c '
|
|||
apt-get install -y \
|
||||
linux-image-generic \
|
||||
grub-pc-bin grub-efi-amd64-bin grub-common xorriso \
|
||||
casper live-boot live-boot-initramfs-tools \
|
||||
xorg xfce4 xfce4-goodies lightdm \
|
||||
firefox network-manager \
|
||||
sudo curl wget git ca-certificates gnupg \
|
||||
|
|
@ -61,6 +73,12 @@ chroot "$ROOTFS_DIR" bash -c '
|
|||
xfce4-terminal mousepad ristretto \
|
||||
dbus-x11
|
||||
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
|
||||
|
||||
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 " - 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)"
|
||||
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."
|
||||
echo "[!] FATAL: Kernel or initrd not found in $ROOTFS_DIR/boot/"
|
||||
echo "[!] Contents of $ROOTFS_DIR/boot/:"
|
||||
ls -la "$ROOTFS_DIR/boot/" || true
|
||||
mkdir -p "$BUILD_DIR"
|
||||
echo "No kernel found in rootfs" > "$BUILD_DIR/README.txt"
|
||||
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 "[+] Copying kernel and initrd to $ISO_DIR/casper/..."
|
||||
cp -v "$KERNEL" "$ISO_DIR/casper/vmlinuz" || { echo "[!] Failed to copy kernel"; exit 1; }
|
||||
cp -v "$INITRD" "$ISO_DIR/casper/initrd.img" || { echo "[!] Failed to copy initrd"; exit 1; }
|
||||
|
||||
# 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
|
||||
echo "[+] Unmounting chroot filesystems..."
|
||||
umount -lf "$ROOTFS_DIR/proc" 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
|
||||
|
||||
echo "[+] Creating SquashFS filesystem..."
|
||||
|
|
@ -237,36 +271,135 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] Setting up BIOS boot (isolinux)..."
|
||||
cat > "$ISO_DIR/isolinux/isolinux.cfg" << 'EOF'
|
||||
PROMPT 0
|
||||
TIMEOUT 50
|
||||
DEFAULT linux
|
||||
echo "[+] Final verification before ISO creation..."
|
||||
for file in "$ISO_DIR/casper/vmlinuz" "$ISO_DIR/casper/initrd.img" "$ISO_DIR/casper/filesystem.squashfs"; do
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "[!] CRITICAL: Missing $file"
|
||||
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
|
||||
MENU LABEL AeThex OS
|
||||
echo "[+] Creating live boot manifest..."
|
||||
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
|
||||
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
|
||||
|
||||
cp /usr/lib/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || \
|
||||
cp /usr/share/syslinux/isolinux.bin "$ISO_DIR/isolinux/" 2>/dev/null || echo "[!] isolinux.bin missing"
|
||||
cp /usr/lib/syslinux/ldlinux.c32 "$ISO_DIR/isolinux/" 2>/dev/null || \
|
||||
cp /usr/share/syslinux/ldlinux.c32 "$ISO_DIR/isolinux/" 2>/dev/null || echo "[!] ldlinux.c32 missing"
|
||||
# Copy syslinux binaries
|
||||
for src in /usr/lib/syslinux/modules/bios /usr/lib/ISOLINUX /usr/share/syslinux; do
|
||||
if [ -f "$src/isolinux.bin" ]; then
|
||||
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)..."
|
||||
mkdir -p "$ISO_DIR/boot/grub"
|
||||
cat > "$ISO_DIR/boot/grub/grub.cfg" << 'EOF'
|
||||
set timeout=10
|
||||
set default=0
|
||||
|
||||
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
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[+] Creating hybrid ISO with grub-mkrescue..."
|
||||
grub-mkrescue -o "$BUILD_DIR/$ISO_NAME" "$ISO_DIR" --verbose 2>&1 | tail -20
|
||||
echo "[+] Verifying ISO structure before xorriso..."
|
||||
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..."
|
||||
if [ -f "$BUILD_DIR/$ISO_NAME" ]; then
|
||||
|
|
|
|||
|
|
@ -35,8 +35,14 @@ const allowlist = [
|
|||
async function buildAll() {
|
||||
await rm("dist", { recursive: true, force: true });
|
||||
|
||||
const enableSourcemap = process.argv.includes("--sourcemap");
|
||||
|
||||
console.log("building client...");
|
||||
await viteBuild();
|
||||
await viteBuild({
|
||||
build: {
|
||||
sourcemap: enableSourcemap,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("building server...");
|
||||
const pkg = JSON.parse(await readFile("package.json", "utf-8"));
|
||||
|
|
@ -56,6 +62,7 @@ async function buildAll() {
|
|||
"process.env.NODE_ENV": '"production"',
|
||||
},
|
||||
minify: true,
|
||||
sourcemap: enableSourcemap,
|
||||
external: externals,
|
||||
logLevel: "info",
|
||||
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