new file: .gemini/settings.json

This commit is contained in:
MrPiglr 2026-01-03 23:56:43 -07:00
parent 308b047be0
commit 7e275b020c
59 changed files with 5139 additions and 467 deletions

8
.gemini/settings.json Normal file
View file

@ -0,0 +1,8 @@
{
"mcpServers": {
"myLocalServer": {
"command": "python my_mcp_server.py",
"cwd": "./mcp_server"
}
}
}

View file

@ -3,5 +3,6 @@
"builder.command": "npm run dev",
"builder.runDevServer": true,
"builder.autoDetectDevServer": true,
"builder.launchType": "desktop"
"builder.launchType": "desktop",
"chatgpt.openOnStartup": true
}

View file

@ -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

View 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()}*

View file

@ -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>

View file

@ -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'

View file

@ -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" />

View file

@ -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
);
}
}

View file

@ -1,2 +1,2 @@
npx cap sync
npx cap syncnpx cap syncnpx cap sync npx cap sync

476
build-fixed.sh Normal file
View 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
View 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

Binary file not shown.

32
capacitor.config.json Normal file
View 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"
}
}
}

View file

@ -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;

View file

@ -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>

View 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

View file

@ -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"
}
}
}

View file

@ -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'));
}
});

View file

@ -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} />

View file

@ -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;
}

View 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>
);
}

View 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" /> },
];

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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,
};
}

View 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,
};
}

View 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,
};
}

View 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,
};
}

View 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,
};
}

View 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,
};
}

View 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
View 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
View 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>
);
}

View file

@ -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' }}

View file

@ -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>

View file

@ -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" />

View file

@ -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">

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View file

@ -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",

View file

@ -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",

View file

@ -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:"

View file

@ -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

View file

@ -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
View 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
View 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
View 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
View 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
View 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]