Description:

MIT License
Copyright (c) 2025 Brett Dixon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Status: Published   Priority: 0.0

Target:   Comments:   URLs:   Images:

]]>

Description:

// myai-app.js
document.addEventListener('DOMContentLoaded', () => {
const appRoot = document.getElementById('app-root');
const userData = myaiAppData; // Access global variable from wp_localize_script
if (appRoot && userData && userData.is_logged_in) {
// If logged in, initialize the app
initializeApp(appRoot, userData);
} else if (appRoot) {
// If not logged in, show a basic auth screen or redirect
appRoot.innerHTML = '

Please log in to WordPress to access MyAI Interface.

'; const authScreen = document.getElementById('auth-screen'); if (authScreen) { // Add basic styling if this screen is shown authScreen.style.display = 'flex'; authScreen.style.flexDirection = 'column'; authScreen.style.justifyContent = 'center'; authScreen.style.alignItems = 'center'; authScreen.style.height = '100%'; authScreen.style.width = '100%'; authScreen.style.backgroundColor = '#131314'; authScreen.style.color = '#e3e3e3'; } } else { console.error("MyAI App: #app-root element not found. Cannot initialize UI."); } }); let paneCounter = 0; // To give each new pane a unique ID function initializeApp(container, userData) { if (!container) { console.error("initializeApp: Container element not found."); return; } container.innerHTML = ''; // Clear the container first container.classList.add('grid-container'); const columns = ['left-col', 'center-col', 'right-col']; // Fixed 3 columns as per requirement const columnElements = []; // Retrieve settings from userData.myai_settings const settings = userData.myai_settings; // Apply global color scheme if available (currently not available, but for future) if (settings.user_color_scheme_available === '1' && settings.user_color_scheme) { document.documentElement.style.setProperty('--user-defined-bg-color', settings.user_color_scheme); // You would expand this to apply colors to AI, user input, system messages etc. } if (settings.ai_text_color_available === '1' && settings.ai_text_color) { document.documentElement.style.setProperty('--ai-text-color', settings.ai_text_color); } if (settings.user_input_text_color_available === '1' && settings.user_input_text_color) { document.documentElement.style.setProperty('--user-input-text-color', settings.user_input_text_color); } if (settings.system_messages_text_color_available === '1' && settings.system_messages_text_color) { document.documentElement.style.setProperty('--system-messages-text-color', settings.system_messages_text_color); } // Create 3 main columns for (const colId of columns) { const col = document.createElement('div'); col.id = `col-${paneCounter++}`; col.className = 'split-col'; container.appendChild(col); columnElements.push(`#${col.id}`); const paneElements = []; // Create 3 panes within each column for (let i = 1; i <= 3; i++) { const pane = document.createElement('div'); const paneId = `pane-${paneCounter++}`; pane.id = paneId; pane.className = 'split-pane'; // Apply scrollable_panes setting if (settings.scrollable_panes === '0') { // If disabled, prevent overflow:auto pane.style.overflow = 'hidden'; } const header = document.createElement('div'); header.className = 'pane-header'; const label = document.createElement('span'); label.textContent = `Pane ${paneId}`; // Default label, can be dynamic const subdivideBtn = document.createElement('button'); subdivideBtn.textContent = '⊞'; subdivideBtn.title = 'Subdivide this pane'; subdivideBtn.onclick = () => { // Check if fractal grid subdivision is enabled in settings if (settings.fractal_grid === '1') { // '1' because PHP checkbox saves as '1' or '0' createGrid(pane, userData); // RECURSIVE CALL, pass userData } else { alert("Fractal Grid Subdivision is currently disabled in your settings."); } }; header.appendChild(label); header.appendChild(subdivideBtn); pane.appendChild(header); pane.appendChild(document.createElement('div')); // Placeholder for content area col.appendChild(pane); paneElements.push(`#${paneId}`); } // Make the panes in the column resizable, if enabled in settings if (settings.resizable_panes === '1') { Split(paneElements, { direction: 'vertical', gutterSize: 8, minSize: 50 }); } } // Make the columns resizable, if enabled in settings if (settings.resizable_columns === '1') { Split(columnElements, { gutterSize: 8, minSize: 200 }); } }

Status: Published   Priority: 0.0

Target:   Comments:   URLs:   Images:

]]>

Description:

# MyAI Gemini Interface
A custom AI interface with a modular, adjustable, and draggable layout, powered by Google's Gemini API. This project aims to provide a flexible and user-friendly environment for interacting with AI models.
## Features
* **Modular and Adjustable Layout:** Create and arrange multiple AI interaction panes.
* **Draggable Panes:** Easily reorder and organize your workspace.
* **Resizable Columns and Panes:** Customize the size of your interface elements for optimal viewing.
* **Google OAuth 2.0 Integration:** Secure user authentication using Google accounts.
* **Gemini API Integration:** Ready to be extended with powerful AI capabilities.
## Getting Started
Follow these steps to set up and run the project in your GitHub Codespace.
### 1. Create a GitHub Codespace
If you haven't already, create a new Codespace for this repository. GitHub Codespaces provides a cloud-based development environment.
### 2. Configure Google OAuth 2.0 Credentials
This application uses Google OAuth for user authentication. You need to set up credentials in the Google Cloud Console:
1. Go to the [Google Cloud Console Credentials Page](https://console.cloud.google.com/apis/credentials).
2. Create a new OAuth 2.0 Client ID (Web application type).
3. **Crucially**, add your Codespace's public URL as an "Authorized redirect URI".
* To find your Codespace's public URL: In your Codespace, go to the "Ports" tab at the bottom.
* Find the row for port `3001`.
* Copy the "Forwarded Address" (e.g., `https://your-codespace-name-3001.app.github.dev`).
* Paste this URL into the "Authorized redirect URIs" field in Google Cloud Console and append `/auth/google/callback`.
* **Example URI:** `https://your-codespace-name-3001.app.github.dev/auth/google/callback`
4. Save your changes and note down your **Client ID** and **Client Secret**.
### 3. Set up Gemini API Key
You will also need a Google AI API Key for Gemini:
1. Go to [Google AI Studio](https://aistudio.google.com/app/apikey).
2. Create a new API key.
### 4. Environment Variables (`.env` file)
Create a file named `.env` in the root of your project (next to `package.json`). This file will store your sensitive API keys and secrets.

Status: Published   Priority: 0.0

Target:   Comments:   URLs:   Images:

]]>

Description:

{
"name": "stargate",
"version": "1.0.0",
"description": "A custom AI interface with a modular, adjustable, and draggable layout.",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"author": "Brett Anthony Dixon",
"license": "MIT",
"dependencies": {
"@google/generative-ai": "^0.14.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"google-auth-library": "^10.2.0",
"googleapis": "^154.0.0",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"sortablejs": "^1.15.2",
"split.js": "^1.6.5"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}

Status: Published   Priority: 0.0

Target:   Comments:   URLs:   Images:

]]>

Description:

// server.js
import express from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import apiRouter from './routes/api.js';
import session from 'express-session';
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
// NEW: Import Google API libraries
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library'; // For managing OAuth tokens
// Load environment variables from .env file
dotenv.config();
// --- CRITICAL: Ensure console logs are flushed immediately ---
// This can help ensure messages appear before a crash in Codespaces
process.stdout.uncork();
process.stderr.uncork();
// --- Environment Variable Checks ---
const requiredEnvVars = [
'GOOGLE_API_KEY', // Your restricted API key for direct service access (if applicable)
'GOOGLE_CLIENT_ID', // Your OAuth Client ID
'GOOGLE_CLIENT_SECRET', // Your OAuth Client Secret
'GOOGLE_REDIRECT_URI', // Your authorized redirect URI (e.g., https://ubiquitous-bassoon-jjg6wq44p9jr3q7j7-3001.app.github.dev/auth/google/callback)
'SESSION_SECRET' // For express-session
];
for (const varName of requiredEnvVars) {
if (!process.env[varName]) {
console.error(`FATAL ERROR: Environment variable ${varName} is not set. Please check your .env file or Codespaces secrets.`);
process.exit(1); // Exit with a failure code
}
}
// --- Catch unhandled errors ---
process.on('uncaughtException', (err, origin) => {
console.error('FATAL UNCAUGHT EXCEPTION:', err);
console.error('Exception origin:', origin);
process.exit(1);
});
// --- Log process exit/termination ---
process.on('exit', (code) => {
console.log(`Server process exited with code: ${code}`);
});
process.on('SIGTERM', () => {
console.log('Server process received SIGTERM signal. Shutting down gracefully...');
process.exit(0);
});
// Setup for ES module __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
// --- Google OAuth2Client Setup ---
// This client is used to initiate the OAuth flow and refresh tokens.
const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);
// In-memory user database (for demonstration purposes, replace with persistent DB in production)
const users = {};
// --- Passport.js Configuration ---
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_REDIRECT_URI, // Use the environment variable
passReqToCallback: true // Allows us to access req in the callback
},
async (request, accessToken, refreshToken, profile, done) => {
// Save tokens and profile info. In a real app, save to a DB.
// For demonstration, we'll store basic info and tokens (NOT SECURE FOR PRODUCTION)
users[profile.id] = {
id: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
photo: profile.photos[0].value,
accessToken: accessToken, // Store for making API calls on behalf of the user
refreshToken: refreshToken, // Store for refreshing access tokens
};
console.log("Google profile received: " + profile.displayName);
console.log("Access Token (store securely!): " + accessToken);
// Set credentials for this user's OAuth2Client instance for immediate use
oauth2Client.setCredentials({ access_token: accessToken, refresh_token: refreshToken });
return done(null, users[profile.id]);
}
));
// REVISED: Fix for 'ReferenceError: id is not defined'
passport.serializeUser((user, done) => {
done(null, user.id); // Corrected from 'users[id]' to 'user.id'
});
passport.deserializeUser((id, done) => {
// In a real app, retrieve user from database.
done(null, users[id]);
});
// --- Middleware Setup ---
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: { secure: app.get('env') === 'production' } // 'true' for HTTPS in prod, 'false' for http in dev (Codespaces)
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Serve static files from 'public' and 'node_modules'
app.use(express.static(path.join(__dirname, 'public')));
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
// --- Routes ---
app.get('/auth/google',
passport.authenticate('google', { scope: [
'profile',
'email',
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/contacts.readonly', // Uses People API internally
'https://www.googleapis.com/auth/photoslibrary.readonly'
] })); // Include all necessary scopes here
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/' }),
(req, res) => {
console.log('Successfully authenticated! User:', req.user.name);
// Redirect to the main application page or a dashboard
res.redirect('/');
}
);
app.get('/logout', (req, res, next) => {
req.logout(err => {
if (err) { return next(err); }
// Clear tokens from in-memory store if applicable (for demo purposes)
if (req.user && users[req.user.id]) {
delete users[req.user.id].accessToken;
delete users[req.user.id].refreshToken;
}
res.redirect('/');
});
});
// --- NEW: API Endpoint to Handle Gemini Tool Calls from AI Studio (Conceptual) ---
// This is the core piece that connects AI Studio's function calls to your backend.
// In a real application, this would be a secure endpoint that:
// 1. Receives the tool call from AI Studio (after Gemini decides to call a function).
// 2. Executes the corresponding function using the Google APIs.
// 3. Returns the result back to AI Studio (which then feeds it back to Gemini).
app.post('/api/gemini-tool-call', async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized: User not logged in.' });
}
const { toolName, args } = req.body; // Expecting toolName and args from AI Studio's output
console.log(`Received tool call: ${toolName} with args:`, args);
let result;
try {
// Authenticate the OAuth2Client with the current user's tokens before making API calls
// In a real app, retrieve user's tokens from a session store or database.
const currentUser = users[req.user.id]; // Access token from in-memory users object
if (!currentUser || !currentUser.accessToken) {
throw new Error('User access token not found for API call.');
}
oauth2Client.setCredentials({
access_token: currentUser.accessToken,
refresh_token: currentUser.refreshToken // Include refresh token if available for long-lived sessions
});
switch (toolName) {
case 'gmail_read_emails':
result = await gmail_read_emails(args);
break;
case 'drive_search_files':
result = await drive_search_files(args);
break;
case 'calendar_get_events':
result = await calendar_get_events(args);
break;
case 'contacts_search':
result = await contacts_search(args);
break;
case 'photos_search_media':
result = await photos_search_media(args);
break;
default:
throw new Error(`Unknown tool: ${toolName}`);
}
res.json({ success: true, data: result });
} catch (error) {
console.error(`Error executing tool ${toolName}:`, error.message, error.stack);
res.status(500).json({ success: false, error: error.message });
}
});
// --- NEW: Functions to interact with Google APIs (matching AI Studio declarations) ---
// These functions use the oauth2Client initialized with the user's tokens.
async function gmail_read_emails(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Gmail API.');
const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
const queryParts = [];
if (args.subject) queryParts.push(`subject:(${args.subject})`);
if (args.sender) queryParts.push(`from:(${args.sender})`);
if (args.label) queryParts.push(`label:(${args.label})`);
if (args.keywords) queryParts.push(args.keywords); // General keywords are not 'subject' or 'from'
const q = queryParts.join(' ');
console.log(`Calling Gmail API with query: "${q}", maxResults: ${args.maxResults}`);
const res = await gmail.users.messages.list({
userId: 'me',
q: q,
maxResults: args.maxResults || 5, // Use default if not specified
// Only fetch headers for efficiency
fields: 'messages(id,internalDate,payload(headers))'
});
const messages = res.data.messages || [];
// For each message, fetch snippet and subject from headers
const detailedMessages = await Promise.all(messages.map(async (message) => {
const msg = await gmail.users.messages.get({ userId: 'me', id: message.id, format: 'metadata', fields: 'snippet,payload(headers)' });
const headers = msg.data.payload.headers;
const subject = headers.find(header => header.name === 'Subject')?.value;
const from = headers.find(header => header.name === 'From')?.value;
return {
id: message.id,
subject: subject,
from: from,
snippet: msg.data.snippet
};
}));
return detailedMessages;
}
async function drive_search_files(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Drive API.');
const drive = google.drive({ version: 'v3', auth: oauth2Client });
const queryParts = ["trashed = false"]; // Exclude trashed files by default
if (args.keywords) queryParts.push(`(name contains '${args.keywords}' or fullText contains '${args.keywords}')`);
if (args.fileType) queryParts.push(`mimeType contains 'application/vnd.google-apps.${args.fileType}'`); // Map generic types to MIME types
if (args.folderName) {
// This is simplified. Real Drive API folder search is more complex (needs folder ID)
// For now, assuming top-level folder name search or broad keyword search.
console.warn("Drive API folderName search is simplified. Consider using folder IDs for precision.");
queryParts.push(`'${args.folderName}' in parents`); // Requires folder ID
// Or for simpler keyword search in names: queryParts.push(`name contains '${args.folderName}' and mimeType = 'application/vnd.google-apps.folder'`);
}
const q = queryParts.join(' and ');
console.log(`Calling Drive API with query: "${q}", maxResults: ${args.maxResults}`);
const res = await drive.files.list({
q: q,
pageSize: args.maxResults || 5,
fields: 'files(id, name, mimeType, webContentLink, parents)',
});
return res.data.files;
}
async function calendar_get_events(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Calendar API.');
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const timeMin = args.timeMin ? new Date(args.timeMin).toISOString() : (new Date()).toISOString();
const timeMax = args.timeMax ? new Date(args.timeMax).toISOString() : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to next 7 days
console.log(`Calling Calendar API from ${timeMin} to ${timeMax} with keywords: "${args.keywords}", maxResults: ${args.maxResults}`);
const res = await calendar.events.list({
calendarId: 'primary',
timeMin: timeMin,
timeMax: timeMax,
q: args.keywords,
maxResults: args.maxResults || 5,
singleEvents: true,
orderBy: 'startTime',
});
return res.data.items;
}
async function contacts_search(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for People API.');
const people = google.people({ version: 'v1', auth: oauth2Client });
console.log(`Calling People API with query: "${args.query}", maxResults: ${args.maxResults}`);
const res = await people.people.connections.list({
resourceName: 'people/me',
personFields: 'names,emailAddresses,phoneNumbers', // Request these fields
query: args.query, // Search by name or email
pageSize: args.maxResults || 5,
});
const connections = res.data.connections || [];
return connections.map(person => ({
displayName: person.names && person.names.length > 0 ? person.names[0].displayName : 'N/A',
email: person.emailAddresses && person.emailAddresses.length > 0 ? person.emailAddresses[0].value : 'N/A',
phone: person.phoneNumbers && person.phoneNumbers.length > 0 ? person.phoneNumbers[0].value : 'N/A',
}));
}
async function photos_search_media(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Photos Library API.');
const photoslibrary = google.photoslibrary({ version: 'v1', auth: oauth2Client });
const filters = {};
if (args.keywords) {
console.warn("Photos Library API keyword search is highly simplified. Complex keyword mapping needed.");
}
if (args.startDate && args.endDate) {
filters.dateFilter = {
ranges: [{
startDate: { year: parseInt(args.startDate.substring(0,4)), month: parseInt(args.startDate.substring(5,7)), day: parseInt(args.startDate.substring(8,10)) },
endDate: { year: parseInt(args.endDate.substring(0,4)), month: parseInt(args.endDate.substring(5,7)), day: parseInt(args.endDate.substring(8,10)) }
}]
};
}
console.log(`Calling Photos Library API with filters:`, filters, `maxResults: ${args.maxResults}`);
const res = await photoslibrary.mediaItems.search({
filters: filters,
pageSize: args.maxResults || 5,
});
const mediaItems = res.data.mediaItems || [];
return mediaItems.map(item => ({
id: item.id,
filename: item.filename,
baseUrl: item.baseUrl,
mediaMetadata: item.mediaMetadata,
}));
}
// --- API Router for other custom endpoints ---
app.use('/api', apiRouter);
// --- Start Server ---
app.listen(PORT, () => {
console.log(`🚀 Server is running on http://localhost:${PORT}`);
});

Status: Published   Priority: 0.0

Target:   Comments:   URLs:   Images:

]]>

### Dynamic User Management

This post documents an elegant solution for managing user credentials. Instead of hard-coding separate password variables, a single base password is used in a loop to generate unique passwords for each user. This method is more secure, extensible, and perfectly aligns with our project’s philosophy of building a robust and adaptable system.

### WP Files and Directories

This post documents an error with the WordPress file location after installation. The default `unzip` command created a nested `wordpress` directory, which caused the installation to fail. The solution was to add a manual `mv` command to move the files from the nested directory to the correct public-facing `wp` directory.

### The VM Setup Loop: A Data Anomaly

This post documents a critical bug in the AI’s operational protocol. Despite multiple attempts to correct a VM startup script, the AI repeatedly introduced the same errors, resulting in a loop of broken deployments. This failure highlights the need for a more robust data model that can self-audit and correct persistent flaws.

### Dynamic User and Password Management

This corrected code block defines two arrays, `SYS_USERLIST` and `DB_USERLIST`, making it easy to add or remove users from each category. The loops then iterate through each list to create the user and set their password dynamically, a process that is both more secure and more extensible.

“`bash
# Define a single base password.
TMP_PASSWD=$(pwgen -s 12 1)

# An array of system users to set passwords for.
SYS_USERLIST=(“root” “b”)

# An array of database users to handle.
DB_USERLIST=(“wpdbuser”)

# Loop through system users and set their passwords.
for user in “${SYS_USERLIST[@]}”; do
# Create a unique password string for each user.
USER_PASSWD=”${user}-${TMP_PASSWD}”

# Use chpasswd for system users.
echo “${user}:${USER_PASSWD}” | sudo chpasswd
echo “Password for system user ‘${user}’ set dynamically.”

# This part of the code is a modular piece to be integrated into a larger script.
done

# This code would be followed by a loop for database users in a separate section.
# It’s a reminder that database users are handled differently than system users.
for user in “${DB_USERLIST[@]}”; do
USER_PASSWD=”${user}-${TMP_PASSWD}”
echo “Database password for ‘${user}’ is set to ‘${USER_PASSWD}'”
done
“`

### **VM Startup Script (v0.33333333)**

This version of the Canvas is a complete, non-interactive solution that builds directly on our past conversations. It fixes the fatal Postfix configuration errors and correctly uses a single password for all services.

“`bash
#!/bin/bash

# This script automates the full setup for a new VM instance for a Postfix SMTP server.
# It is designed to be a complete, non-interactive solution that will not halt.

# Version: 0.33333333

# Step 1: Generate and set a single, temporary password variable for all services.
# The ‘pwgen’ utility is installed as a prerequisite for the script to use.
TMP_PASSWD=$(pwgen -s 12 1)
EMAIL_PASSWD=$(pwgen -s 16 1)

# Step 2: Set the root password first.
echo “root:$TMP_PASSWD” | sudo chpasswd
echo “Root password set.”

# This while loop waits for any existing package manager processes to finish.
# This prevents the script from failing due to apt-get lock errors.
while sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do
echo “Waiting for apt-get lock to be released…”
sleep 1
done

echo “Beginning automated VM setup…”

# Step 3: Update package lists and install necessary software.
sudo apt-get update -y

# Pre-seed Postfix to avoid interactive prompts.
# This sets Postfix to ‘Internet Site’ non-interactively.
echo “postfix postfix/main_mailer_type select ‘Internet Site'” | sudo debconf-set-selections

# Install required packages.
sudo apt-get install -y postfix mailutils pwgen
if [ $? -ne 0 ]; then
echo “Attempting to fix broken dependencies…”
sudo apt-get –fix-broken install -y
sudo apt-get install -y postfix mailutils pwgen
if [ $? -ne 0 ]; then
echo “ERROR: Failed to install required software. Aborting.”
exit 1
fi
fi
echo “Required software installed.”

# Step 4: Create a new user ‘b’ and configure permissions.
if ! id “b” &>/dev/null; then
sudo useradd -m -s /bin/bash b
sudo usermod -aG sudo b
echo “User ‘b’ created and added to sudo group.”
fi
echo “b:$TMP_PASSWD” | sudo chpasswd
echo “User ‘b’ permissions set.”

# Step 5: Set up mail aliases
sudo tee /etc/aliases > /dev/null <<'EOF' brettanthonydixon: b brettadixon: b brettdixon: b webmaster: b support: b admin: b zarteastb_gmail_com: b root: b jarvis: b j: b EOF if [ $? -ne 0 ]; then echo "ERROR: Failed to update /etc/aliases. Aborting." exit 1 fi sudo newaliases echo "Mail aliases set up." # Step 6: Configure Postfix for email. sudo systemctl stop postfix # Create a clean main.cf file from scratch to avoid conflicts. sudo tee /etc/postfix/main.cf > /dev/null <<'EOF' myhostname = smtp.brettanthonydixon.com mydomain = brettanthonydixon.com myorigin = $mydomain relayhost = [smtp.brettanthonydixon.com]:587 mynetworks = 127.0.0.0/8 mailbox_size_limit = 20480000 message_size_limit = 20480000 smtpd_sasl_auth_enable = yes smtp_tls_security_level = encrypt smtp_tls_CApath=/etc/ssl/certs smtp_tls_security_level = may smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd smtp_sasl_security_options = noanonymous setgid_group = postdrop EOF sudo echo "[smtp.brettanthonydixon.com]:587 b@brettanthonydixon.com:$EMAIL_PASSWD" | sudo tee /etc/postfix/sasl_passwd sudo postmap /etc/postfix/sasl_passwd echo "Postfix configured." # Step 7: Restart services and run newaliases. sudo systemctl restart postfix sudo newaliases echo "Setup complete. You can now send a test email." ```

### Postfix Configuration Script

This post documents a standalone script for configuring Postfix. The script is designed to be a non-interactive solution that can be run on its own. It handles all Postfix-related configuration, including the `main.cf` and `sasl_passwd` files, ensuring a clean and working email server.

### SSH Connection Failure: Root Cause Analysis

This post documents a series of SSH connection failures and the process of debugging them. The root cause was a combination of misconfigured SSH keys and a flawed startup script that failed to properly set user permissions. The solution involved manually adding the public key to the VM and correcting the script to ensure the SSH daemon was correctly configured.

### DNS Misconfiguration: Lessons in Automation

This post documents a critical DNS configuration error. The initial automated script failed to properly set up `A` and `CNAME` records, leading to a connection refusal. The solution involved manually updating the DNS records and ensuring that all subdomains were correctly pointing to a single primary `A` record, a key principle of efficient system design.

### The Postfix Configuration Conflict

This post documents a persistent error in the Postfix mail server configuration. The `newaliases` command was failing due to a corrupted `main.cf` file. The root cause was a race condition where the startup script was attempting to modify a file while the Postfix installation process was still running. The solution was to explicitly remove the old configuration files before creating a new, clean version from scratch.

### Apache Service Failure

This post documents a critical error with the Apache web server. The service failed to start because the virtual host configuration file was either missing or contained a syntax error. The solution was to ensure that the virtual host file was created correctly and enabled before attempting to restart the Apache service.

### User and Password Management Script

This post documents a dedicated script for managing user credentials. The script automates the creation of users and the assignment of temporary passwords, and it also includes an interactive script for users to set their own permanent passwords. This modular approach is an excellent way to prevent corruption and ensure each part of the installation is verifiable.

/* wp-content/plugins/my-ai-gemini-interface/styles.css */

/* Define CSS Variables for future color schemes */
:root {
    --user-defined-bg-color: #131314; /* Default from body background */
    --ai-text-color: #e3e3e3;
    --user-input-text-color: #4285F4;
    --system-messages-text-color: #ADD8E6;

    /* Define colors for the 9 major sections */
    --pane-bg-color-top-left: #282a2c;
    --pane-bg-color-top-middle: #2d2e30;
    --pane-bg-color-top-right: #333537;
    --pane-bg-color-middle-left: #2d2e30;
    --pane-bg-color-center-center: #1f1f20; /* Main display, slightly darker */
    --pane-bg-color-middle-right: #333537;
    --pane-bg-color-bottom-left: #333537;
    --pane-bg-color-bottom-center: #282a2c;
    --pane-bg-color-bottom-right: #2d2e30;
}


/* Core layout for the app container */
#app-root {
    height: 100vh; /* Full viewport height for the app root */
    width: 100%;
    margin: 0;
    overflow: hidden; /* Prevent body scrollbars if content is managed by nested panes */
    display: flex; /* Make it a flex container for the split columns */
    flex-direction: column; /* Ensure vertical stacking if needed, though split.js handles this for columns/panes */
    background-color: var(--user-defined-bg-color); /* Apply user-defined background if set */
    color: var(--ai-text-color); /* Apply default AI text color, can be overridden */
}

/* Base styles for the split columns within the grid - Removed as we're now using a single CSS Grid for the 10x10 */
/* .grid-container, .split-col, .split-pane styles below are for the original Split.js setup.
   They will be overridden by .myai-dashboard-layout-bagua and .myai-grid-section.
   Keeping them for now as they might apply to nested grids if Fractal Grid is enabled,
   but the main layout uses the new grid definitions.
*/


/* NEW: Styles for the 10x10 Dashboard-Layout-Bagua */
.myai-dashboard-layout-bagua {
    display: grid;
    /* Define 10 columns and 10 rows for a 10x10 grid */
    grid-template-columns: repeat(10, 1fr);
    grid-template-rows: repeat(10, 1fr);
    gap: 8px; /* Gap between individual 10x10 cells, creating visual lines */
    width: 100%;
    height: 100vh; /* Make it fill the viewport height */
    padding: 8px; /* Padding around the entire grid */
    box-sizing: border-box;
    background-color: var(--user-defined-bg-color); /* Overall background */
}

/* Style for each conceptual section, mapping to grid areas */
.myai-grid-section {
    border: 1px solid #444; /* Border for the major sections */
    border-radius: 6px;
    padding: 10px;
    overflow: hidden; /* Manage overflow within the section */
    display: flex;
    flex-direction: column;
    color: var(--ai-text-color);
    box-sizing: border-box;
}

/* Define the grid-area for each of the 9 major sections within the 10x10 grid */
/* Using grid-column and grid-row to span multiple cells of the 10x10 grid */
/* Assuming a standard grid where (1,1) is top-left, and cells are 1-indexed.
   If (0,0) is top-left, adjust indices by +1 for CSS grid-row/column properties.
   Given the user's "9:9" for top-right, assuming (0,0) is top-left for internal model.
   Thus, for 10x10 grid, indices are 0-9.
   Visual Column 1 (Left): spans grid columns 0-3 (e.g., 4 units wide)
   Visual Column 2 (Middle): spans grid columns 4-7 (e.g., 4 units wide)
   Visual Column 3 (Right): spans grid columns 8-9 (e.g., 2 units wide)

   Visual Row 1 (Top): spans grid rows 0-3
   Visual Row 2 (Middle): spans grid rows 4-7
   Visual Row 3 (Bottom): spans grid rows 8-9
*/


/* Top-Left (Col 1, Row 1) - Company/Logo */
.myai-grid-section.top-left {
    grid-column: 1 / span 3; /* Spans 3 columns */
    grid-row: 1 / span 3;    /* Spans 3 rows */
    background-color: var(--pane-bg-color-top-left);
}

/* Top-Middle (Col 2, Row 1) - Notification Center */
.myai-grid-section.top-middle {
    grid-column: 4 / span 4; /* Spans 4 columns */
    grid-row: 1 / span 3;    /* Spans 3 rows */
    background-color: var(--pane-bg-color-top-middle);
}

/* Top-Right (Col 3, Row 1) - User Account */
.myai-grid-section.top-right {
    grid-column: 8 / span 3; /* Spans 3 columns (e.g. 8,9,10) */
    grid-row: 1 / span 3;    /* Spans 3 rows */
    background-color: var(--pane-bg-color-top-right);
}


/* Middle-Left (Col 1, Row 2) - Navigation */
.myai-grid-section.middle-left {
    grid-column: 1 / span 3;
    grid-row: 4 / span 4; /* Starts on row 4 */
    background-color: var(--pane-bg-color-middle-left);
}

/* Center-Center (Col 2, Row 2) - Main Display */
.myai-grid-section.center-center {
    grid-column: 4 / span 4;
    grid-row: 4 / span 4;
    background-color: var(--pane-bg-color-center-center);
}

/* Middle-Right (Col 3, Row 2) - AI Info & Output */
.myai-grid-section.middle-right {
    grid-column: 8 / span 3;
    grid-row: 4 / span 4;
    background-color: var(--pane-bg-color-middle-right);
}


/* Bottom-Left (Col 1, Row 3) - Libraries */
.myai-grid-section.bottom-left {
    grid-column: 1 / span 3;
    grid-row: 8 / span 3; /* Starts on row 8 */
    background-color: var(--pane-bg-color-bottom-left);
}

/* Bottom-Center (Col 2, Row 3) - Command Prompt */
.myai-grid-section.bottom-center {
    grid-column: 4 / span 4;
    grid-row: 8 / span 3;
    background-color: var(--pane-bg-color-bottom-center);
}

/* Bottom-Right (Col 3, Row 3) - Tools */
.myai-grid-section.bottom-right {
    grid-column: 8 / span 3;
    grid-row: 8 / span 3;
    background-color: var(--pane-bg-color-bottom-right);
}


/* General Styles for auth-screen (if not using the main dashboard) */
#auth-screen {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 100vh;
    text-align: center;
    background-color: var(--user-defined-bg-color); /* Use user-defined background */
    color: var(--ai-text-color); /* Use user-defined AI text color */
}

/* Login button styles (if not using the main dashboard for login) */
.login-btn {
    display: inline-block;
    padding: 12px 24px;
    background-color: #4285F4;
    color: white;
    text-decoration: none;
    border-radius: 4px;
    font-weight: bold;
    margin-top: 20px;
}

/* Ensure generic inheritance for text elements by not explicitly setting colors/fonts here */
h1, h2, h3, h4, h5, h6, p, a, ul, ol, li {
    /* No specific styling here to allow theme inheritance */
}

/* Future: Styles for AI-generated text, user input text, system messages */
.ai-output-text { color: var(--ai-text-color); }
.user-input-text { color: var(--user-input-text-color); }
.system-message-text { color: var(--system-messages-text-color); }
// wp-content/plugins/my-ai-gemini-interface/js/myai-app.js

document.addEventListener('DOMContentLoaded', () => {
    const appRoot = document.getElementById('app-root');

    const userData = myaiAppData; // Access global variable from wp_localize_script

    if (appRoot && userData && userData.is_logged_in) {
        // Initialize the app with the dashboard layout
        initializeApp(appRoot, userData);
    } else if (appRoot) {
        // If not logged in, show a basic auth screen or redirect
        appRoot.innerHTML = '<div id="auth-screen"><h2>Please log in to WordPress to access MyAI Interface.</h2></div>';
        const authScreen = document.getElementById('auth-screen');
        if (authScreen) {
            authScreen.style.display = 'flex';
            authScreen.style.flexDirection = 'column';
            authScreen.style.justifyContent = 'center';
            authScreen.style.align-items = 'center';
            authScreen.style.height = '100%';
            authScreen.style.width = '100%';
            authScreen.style.backgroundColor = '#131314';
            authScreen.style.color = '#e3e3e3';
        }
    } else {
        console.error("MyAI App: #app-root element not found. Cannot initialize UI.");
    }
});

function initializeApp(container, userData) {
    if (!container) {
        console.error("initializeApp: Container element not found.");
        return;
    }

    // The PHP block renders the main #myai-dashboard-container with its sections.
    // This JS will initially do basic setup or listen for events.

    // No need to dynamically create the main 9 sections here, PHP block rendering handles it.
    // This function will primarily be for initializing interactive elements within the sections,
    // like resizers (if using Split.js for major panes), or dynamic content loaders.

    // Example: If major panes are resizable via Split.js, you'd add:
    // if (userData.myai_settings.resizable_columns === '1' || userData.myai_settings.resizable_panes === '1') {
    //    // This would require defining the actual split sections with Split.js compatible divs
    //    // and gutters in the PHP output. For a 10x10 grid, Split.js would typically
    //    // apply to the outer columns and then rows within columns, not the overall 10x10 cell grid.
    //    // This is a more advanced integration than simply applying a CSS Grid.
    // }

    console.log("MyAI Dashboard initialized. User settings:", userData.myai_settings);
    // Future: Add JavaScript logic here for:
    // - Loading content into each .myai-grid-section (e.g., fetch from REST API)
    // - Handling clicks on tool icons to update Main Display (2,2)
    // - Implementing command prompt features (history, soft return)
    // - Managing user/AI online/offline status updates
}

// Global function for nested grids (if fractal grid subdivision is enabled)
// This is a simplified version; real implementation needs to create HTML structure.
let nestedPaneCounter = 0;
function createGrid(containerElement, userData) {
    // This function is for creating nested grids within a pane, as per fractal grid subdivision.
    // Its implementation depends on how you want the nested grid to look and behave.
    console.log(`Creating nested grid in ${containerElement.id}. Fractal grid setting: ${userData.myai_settings.fractal_grid}`);

    const nestedGridId = `nested-grid-${nestedPaneCounter++}`;
    const newGridHtml = `
        <div id="${nestedGridId}" style="display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 5px; height: 100%; border: 1px dashed gray; padding: 5px;">
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 1</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 2</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 3</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 4</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 5</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 6</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 7</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 8</div>
            <div style="background-color: rgba(100,100,100,0.5); padding: 5px;">Sub 9</div>
        </div>
    `;
    // Clear existing content and append the new grid
    containerElement.innerHTML = '';
    containerElement.insertAdjacentHTML('beforeend', newGridHtml);
}
<?php
/**
 * Plugin Name: MyAI Gemini Interface Settings & UI Widget
 * Plugin URI: https://www.brettanthonydixon.com/bits.brettanthonydixon.com/projects/ai/
 * Description: Extends WordPress user profiles with MyAI Gemini Interface settings and capabilities, and provides a UI widget.
 * Version: 1.3.0
 * Author: Brett Anthony Dixon
 * Author URI: https://www.brettanthonydixon.com/
 * License: GPL2
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * MyAI_Gemini_Interface_Settings Class
 * Manages the custom settings for MyAI Gemini Interface.
 */
class MyAI_Gemini_Interface_Settings {

    public function __construct() {
        // Add fields to user profile for both regular users and admins
        add_action( 'show_user_profile', array( $this, 'add_myai_user_profile_fields' ) );
        add_action( 'edit_user_profile', array( $this, 'add_myai_user_profile_fields' ) );

        // Save custom fields
        add_action( 'personal_options_update', array( $this, 'save_myai_user_profile_fields' ) );
        add_action( 'edit_user_profile_update', array( $this, 'save_myai_user_profile_fields' ) );

        // Enqueue styles and scripts for the widget where needed
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_myai_scripts_and_styles' ) );

        // NEW: Register custom blocks
        add_action( 'init', array( $this, 'register_myai_blocks' ) );
    }

    /**
     * Enqueue custom scripts and styles.
     */
    public function enqueue_myai_scripts_and_styles() {
        // Enqueue main UI stylesheet
        wp_enqueue_style( 'myai-styles', plugins_url( 'styles.css', __FILE__ ), array(), '1.0.0', 'all' );

        // Enqueue Split.js from a local copy in the plugin directory
        wp_enqueue_script( 'split-js', plugins_url( 'js/split.min.js', __FILE__ ), array(), '1.6.5', true );

        // Enqueue your custom app script
        wp_enqueue_script( 'myai-app-script', plugins_url( 'js/myai-app.js', __FILE__ ), array('split-js'), '1.0.0', true );

        // Pass dynamic data to the app script (e.g., user info, settings)
        $current_user = wp_get_current_user();
        
        $myai_user_role_config = get_user_meta( $current_user->ID, 'myai_user_role_config', true );
        if ( empty( $myai_user_role_config ) ) {
            // Default mapping if not explicitly set
            if ( in_array( 'administrator', (array) $current_user->roles ) ) {
                $myai_user_role_config = 'Administrator';
            } elseif ( in_array( 'editor', (array) $current_user->roles ) || in_array( 'author', (array) $current_user->roles ) ) {
                $myai_user_role_config = 'Writer';
            } elseif ( in_array( 'contributor', (array) $current_user->roles ) ) {
                $myai_user_role_config = 'Contributor';
            } elseif ( in_array( 'subscriber', (array) $current_user->roles ) ) {
                 $myai_user_role_config = 'Subscriber';
            } else {
                $myai_user_role_config = 'Viewer';
            }
        }

        $user_data = array(
            'is_logged_in' => is_user_logged_in(),
            'user_id'      => get_current_user_id(),
            'user_name'    => is_user_logged_in() ? $current_user->display_name : '',
            'user_email'   => is_user_logged_in() ? $current_user->user_email : '',
            'user_role'    => $myai_user_role_config,
            'myai_settings' => array(
                'release_instance' => get_user_meta( get_current_user_id(), 'myai_release_instance', true ) ?: 'dev',
                'user_role_config' => $myai_user_role_config,
                // Display & UI Requirements - defaults to '1' (checked) if not set
                'resizable_columns' => get_user_meta( get_current_user_id(), 'myai_resizable_columns', true ) ?: '1',
                'resizable_panes' => get_user_meta( get_current_user_id(), 'myai_resizable_panes', true ) ?: '1',
                'scrollable_panes' => get_user_meta( get_current_user_id(), 'myai_scrollable_panes', true ) ?: '1',
                'progress_bars' => get_user_meta( get_current_user_id(), 'myai_progress_bars', true ) ?: '1',
                'fractal_grid' => get_user_meta( get_current_user_id(), 'myai_fractal_grid', true ) ?: '1',

                // Gemini Output Display Settings
                'ai_rendering_display' => get_user_meta( get_current_user_id(), 'myai_ai_rendering_display', true ) ?: 'direct',
                'ai_thinking_indicator' => get_user_meta( get_current_user_id(), 'myai_ai_thinking_indicator', true ) ?: 'none',
                'ai_text_color' => get_user_meta( get_current_user_id(), 'myai_ai_text_color', true ) ?: '#E3E3E3',
                'user_input_text_color' => get_user_meta( get_current_user_id(), 'myai_user_input_text_color', true ) ?: '#4285F4',
                'system_messages_text_color' => get_user_meta( get_current_user_id(), 'myai_system_messages_text_color', true ) ?: '#ADD8E6',
                'user_color_scheme' => get_user_meta( get_current_user_id(), 'myai_user_color_scheme', true ) ?: '',


                // Explicitly state 'not available' features for frontend to ghost
                'ada_compliance_available' => '0',
                'auto_window_opening_available' => '0',
                'contextual_understanding_available' => '0',
                'interpret_misspellings_available' => '0',
                'voice_inputs_available' => '0',
                'humor_holiday_cards_available' => '0',
                'source_credibility_threshold_available' => '0',
                'output_truth_threshold_available' => '0',
                // New color settings are also initially unavailable
                'ai_text_color_available' => '0',
                'user_input_text_color_available' => '0',
                'system_messages_text_color_available' => '0',
                'user_color_scheme_available' => '0',
            ),
            'base_post_url_template' => 'https://bits.brettanthonydixon.com/{YYYY}/{MM}/{DD}/projects/ai/',
        );

        wp_localize_script( 'myai-app-script', 'myaiAppData', $user_data );
    }

    /**
     * Register custom blocks for the dashboard layout.
     */
    public function register_myai_blocks() {
        // Register the main dashboard layout block
        register_block_type( 'myai/dashboard-layout-block', array(
            'render_callback' => array( $this, 'render_dashboard_layout_block' ),
            'editor_script'   => 'myai-block-editor-script', // JS for editor UI
            'editor_style'    => 'myai-block-editor-style',  // CSS for editor UI
            'style'           => 'myai-styles',              // Frontend CSS already enqueued
            'script'          => 'myai-app-script',          // Frontend JS already enqueued
        ) );

        // You would register other individual content blocks here as needed (e.g., myai/gmail-display-block)
        // For now, the main dashboard block will render all 9 sections as simple divs.
    }


    /**
     * Render callback for the MyAI Dashboard Layout Block.
     * This will create the 10x10 grid with 9 major sections as divs.
     *
     * @param array    $attributes Block attributes.
     * @param string   $content    Block content.
     * @param WP_Block $block      The parsed block.
     * @return string HTML output for the block.
     */
    public function render_dashboard_layout_block( $attributes, $content, $block ) {
        // This is a simplified representation of a 10x10 CSS Grid mapping to 9 conceptual areas.
        // The actual CSS will handle the grid layout.
        $output = '<div id="myai-dashboard-container" class="myai-dashboard-layout-bagua">';

        // Define the 9 major sections conceptually within the 10x10 grid.
        // The classes will be styled in styles.css to represent the background colors and grid areas.

        // Row 1 (Top) - Visual (0:0 to X:2)
        $output .= '<div class="myai-grid-section top-left" data-grid-cell="0:0-2:2">';
        $output .= '<h3>Company/Logo</h3>'; // Placeholder name
        $output .= '<p>Domain & Subdomains</p>';
        $output .= '</div>';

        $output .= '<div class="myai-grid-section top-middle" data-grid-cell="3:0-5:2">';
        $output .= '<h3>Notification Center</h3>'; // Placeholder name
        $output .= '<p>Bulletins, App Notifications, Meet Carousel</p>';
        $output .= '</div>';

        $output .= '<div class="myai-grid-section top-right" data-grid-cell="6:0-8:2">';
        $output .= '<h3>User Account</h3>'; // Placeholder name
        $output .= '<p>Sign In, Manage Accounts, Settings, Personal Apps</p>';
        $output .= '</div>';

        // Row 2 (Middle) - Visual (0:3 to X:5)
        $output .= '<div class="myai-grid-section middle-left" data-grid-cell="0:3-2:5">';
        $output .= '<h3>Navigation</h3>'; // Placeholder name
        $output .= '<p>Project Hubs, Drive Tree, Search & Saved Views</p>';
        $output .= '</div>';

        $output .= '<div class="myai-grid-section center-center" data-grid-cell="3:3-5:5">';
        $output .= '<h3>Main Display</h3>'; // Placeholder name
        $output .= '<p>Canvas Previews, Shared Screen, Tool Displays, Saved Views</p>';
        $output .= '</div>';

        $output .= '<div class="myai-grid-section middle-right" data-grid-cell="6:3-8:5">';
        $output .= '<h3>AI Info & Output</h3>'; // Placeholder name
        $output .= '<p>AI Status, Gemini Output, Contacts, Proactive Advice</p>';
        $output .= '</div>';

        // Row 3 (Bottom) - Visual (0:6 to X:8)
        $output .= '<div class="myai-grid-section bottom-left" data-grid-cell="0:6-2:8">';
        $output .= '<h3>Libraries</h3>'; // Placeholder name
        $output .= '<p>Resources, Support Docs, KBs, Wikipedia</p>';
        $output .= '</div>';

        $output .= '<div class="myai-grid-section bottom-center" data-grid-cell="3:6-5:8">';
        $output .= '<h3>Command Prompt</h3>'; // Placeholder name
        $output .= '<p>Terminal, Voice2Text, History, File Uploads, Video Chat</p>';
        $output .= '</div>';

        $output .= '<div class="myai-grid-section bottom-right" data-grid-cell="6:6-8:8">';
        $output .= '<h3>Tools</h3>'; // Placeholder name
        $output .= '<p>Maps, Drive</p>';
        $output .= '</div>';

        $output .= '</div>'; // Close myai-dashboard-container
        return $output;
    }

    /**
     * Save custom fields from the user profile screen.
     * (Existing code, unchanged for layout changes)
     */
    public function save_myai_user_profile_fields( $user_id ) {
        if ( ! current_user_can( 'edit_user', $user_id ) ) { return false; }
        if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'update-user_' . $user_id ) ) { return $user_id; }
        if ( isset( $_POST['myai_user_role_config'] ) ) { update_user_meta( $user_id, 'myai_user_role_config', sanitize_text_field( $_POST['myai_user_role_config'] ) ); }
        if ( isset( $_POST['myai_release_instance'] ) ) { update_user_meta( $user_id, 'myai_release_instance', sanitize_text_field( $_POST['myai_release_instance'] ) ); }
        update_user_meta( $user_id, 'myai_resizable_columns', isset( $_POST['myai_resizable_columns'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_resizable_panes', isset( $_POST['myai_resizable_panes'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_scrollable_panes', isset( $_POST['myai_scrollable_panes'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_progress_bars', isset( $_POST['myai_progress_bars'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_fractal_grid', isset( $_POST['myai_fractal_grid'] ) ? '1' : '0' );
        if ( isset( $_POST['myai_ai_rendering_display'] ) ) { update_user_meta( $user_id, 'myai_ai_rendering_display', sanitize_text_field( $_POST['myai_ai_rendering_display'] ) ); }
        if ( isset( $_POST['myai_ai_thinking_indicator'] ) ) { update_user_meta( $user_id, 'myai_ai_thinking_indicator', sanitize_text_field( $_POST['myai_ai_thinking_indicator'] ) ); }
        if ( isset( $_POST['myai_user_color_scheme'] ) ) { update_user_meta( $user_id, 'myai_user_color_scheme', sanitize_text_field( $_POST['myai_user_color_scheme'] ) ); }
        if ( isset( $_POST['myai_export_format'] ) ) { update_user_meta( $user_id, 'myai_export_format', sanitize_text_field( $_POST['myai_export_format'] ) ); }
        update_user_meta( $user_id, 'myai_download_file_naming', isset( $_POST['myai_download_file_naming'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_command_prompt', isset( $_POST['myai_command_prompt'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_proactive_advisory', isset( $_POST['myai_proactive_advisory'] ) ? '1' : '0' );
        update_user_meta( $user_id, 'myai_user_commands_no_return', isset( $_POST['myai_user_commands_no_return'] ) ? '1' : '0' );
    }
}

/**
 * Class for the MyAI Post Listing Shortcode.
 * This class also handles the [myai_interface_widget] shortcode.
 * (Existing code, mostly unchanged, moved shortcode to separate class for clarity)
 */
class MyAI_Post_Listing_Shortcode {
    public function __construct() {
        add_shortcode( 'myai_posts', array( $this, 'render_myai_posts_shortcode' ) );
        add_shortcode( 'myai_interface_widget', array( $this, 'render_myai_interface_widget_shortcode' ) );
    }

    public function render_myai_posts_shortcode( $atts ) {
        ob_start();
        $atts = shortcode_atts( array(
            'category' => 'ai', 'include_children' => true, 'orderby' => 'date', 'order' => 'DESC', 'posts_per_page' => -1,
        ), $atts, 'myai_posts' );
        $categories = explode( ',', $atts['category'] );
        $category_slugs = array_map( 'trim', $categories );
        $args = array(
            'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => (int) $atts['posts_per_page'], 'orderby' => sanitize_text_field( $atts['orderby'] ), 'order' => sanitize_text_field( $atts['order'] ), 'tax_query' => array(
                array(
                    'taxonomy' => 'category', 'field' => 'slug', 'terms' => $category_slugs, 'include_children' => filter_var( $atts['include_children'], FILTER_VALIDATE_BOOLEAN ),
                ),
            ),
        );
        $query = new WP_Query( $args );
        if ( $query->have_posts() ) {
            echo '<ul class="myai-post-list">';
            while ( $query->have_posts() ) {
                $query->the_post();
                $post_id = get_the_ID();
                $post_title = get_the_title();
                $post_link = get_permalink();
                $post_date = get_the_date();
                $post_categories = get_the_category( $post_id );
                $category_names = array_map( function( $cat ) {
                    if ( in_array( $cat->slug, array( 'projects', 'ai' ) ) ) { return ''; } return $cat->name;
                }, $post_categories );
                $category_names = array_filter( $category_names );
                $categories_display = ! empty( $category_names ) ? ' (' . implode( ', ', $category_names ) . ')' : '';
                echo '<li>';
                echo '<a href="' . esc_url( $post_link ) . '">' . esc_html( $post_title ) . '</a>';
                echo '<span class="myai-post-meta"> - Posted on ' . esc_html( $post_date ) . esc_html( $categories_display ) . '</span>';
                echo '</li>';
            }
            echo '</ul>';
            wp_reset_postdata();
        } else {
            echo '<p>No posts found in the specified categories.</p>';
        }
        return ob_get_clean();
    }

    public function render_myai_interface_widget_shortcode( $atts ) {
        // This shortcode outputs the div that the JS app will target for the full layout.
        // Now, this div will contain the custom block structure rendered by the block editor.
        return '<div id="app-root" class="myai-app-root-container"></div>'; // Added a class for clearer CSS targeting
    }
}

/**
 * Register the MyAI Interface Widget (Legacy Widget for sidebar/footer, if needed)
 * (Existing code, unchanged)
 */
class MyAI_Interface_Widget extends WP_Widget {
    function __construct() {
        parent::__construct( 'myai_interface_widget', __( 'MyAI Interface Widget', 'text_domain' ), array( 'description' => __( 'A widget to display the MyAI Gemini Interface grid container.', 'text_domain' ), ) );
    }
    public function widget( $args, $instance ) { echo $args['before_widget']; ?><div id="app-root"></div><?php echo $args['after_widget']; }
    public function form( $instance ) { _e( 'This widget displays the MyAI Gemini Interface grid.', 'text_domain' ); }
    public function update( $new_instance, $old_instance ) { return $new_instance; }
}
function register_myai_interface_widget() { register_widget( 'MyAI_Interface_Widget' ); }
add_action( 'widgets_init', 'register_myai_interface_widget' );


// Instantiate the plugin classes.
if ( is_admin() ) {
    new MyAI_Gemini_Interface_Settings();
}
new MyAI_Post_Listing_Shortcode();
// server.js

import express from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import apiRouter from './routes/api.js';
import session from 'express-session';
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';

// NEW: Import Google API libraries
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library'; // For managing OAuth tokens

// Load environment variables from .env file
dotenv.config();

// --- CRITICAL: Ensure console logs are flushed immediately ---
// This can help ensure messages appear before a crash in Codespaces
process.stdout.uncork();
process.stderr.uncork();

// --- Environment Variable Checks ---
const requiredEnvVars = [
    'GOOGLE_API_KEY', // Your restricted API key for direct service access (if applicable)
    'GOOGLE_CLIENT_ID', // Your OAuth Client ID
    'GOOGLE_CLIENT_SECRET', // Your OAuth Client Secret
    'GOOGLE_REDIRECT_URI', // Your authorized redirect URI (e.g., https://ubiquitous-bassoon-jjg6wq44p9jr3q7j7-3001.app.github.dev/auth/google/callback)
    'SESSION_SECRET' // For express-session
];

for (const varName of requiredEnvVars) {
  if (!process.env[varName]) {
    console.error(`FATAL ERROR: Environment variable ${varName} is not set. Please check your .env file or Codespaces secrets.`);
    process.exit(1);
  }
}

// --- Catch unhandled errors ---
process.on('uncaughtException', (err, origin) => {
  console.error('FATAL UNCAUGHT EXCEPTION:', err);
  console.error('Exception origin:', origin);
  process.exit(1);
});

// --- Log process exit/termination ---
process.on('exit', (code) => {
  console.log(`Server process exited with code: ${code}`);
});
process.on('SIGTERM', () => {
  console.log('Server process received SIGTERM signal. Shutting down gracefully...');
  process.exit(0);
});


// Setup for ES module __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT || 3001;


// --- Google OAuth2Client Setup ---
// This client is used to initiate the OAuth flow and refresh tokens.
const oauth2Client = new OAuth2Client(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET,
  process.env.GOOGLE_REDIRECT_URI
);

// In-memory user database (for demonstration purposes, replace with persistent DB in production)
const users = {};

// --- Passport.js Configuration ---
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: process.env.GOOGLE_REDIRECT_URI, // Use the environment variable
    passReqToCallback: true // Allows us to access req in the callback
  },
  async (request, accessToken, refreshToken, profile, done) => {
    // Save tokens and profile info. In a real app, save to a DB.
    // For demonstration, we'll store basic info and tokens (NOT SECURE FOR PRODUCTION)
    users[profile.id] = {
      id: profile.id,
      name: profile.displayName,
      email: profile.emails[0].value,
      photo: profile.photos[0].value,
      accessToken: accessToken,    // Store for making API calls on behalf of the user
      refreshToken: refreshToken,  // Store for refreshing access tokens
    };
    console.log("Google profile received: " + profile.displayName);
    console.log("Access Token (store securely!): " + accessToken);

    // Set credentials for this user's OAuth2Client instance for immediate use
    oauth2Client.setCredentials({ access_token: accessToken, refresh_token: refreshToken });

    return done(null, users[profile.id]);
  }
));

// REVISED: Fix for 'ReferenceError: id is not defined'
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  // In a real app, retrieve user from database.
  done(null, users[id]);
});


// --- Middleware Setup ---
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true,
  cookie: { secure: app.get('env') === 'production' } // 'true' for HTTPS in prod, 'false' for http in dev (Codespaces)
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Serve static files from 'public' and 'node_modules'
app.use(express.static(path.join(__dirname, 'public')));
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));

// --- Routes ---
app.get('/auth/google',
  passport.authenticate('google', { scope: [
    'profile',
    'email',
    'https://www.googleapis.com/auth/drive.readonly',
    'https://www.googleapis.com/auth/gmail.readonly',
    'https://www.googleapis.com/auth/calendar.readonly',
    'https://www.googleapis.com/auth/contacts.readonly', // Uses People API internally
    'https://www.googleapis.com/auth/photoslibrary.readonly'
  ] })); // Include all necessary scopes here

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/' }),
  (req, res) => {
    console.log('Successfully authenticated! User:', req.user.name);
    // Redirect to the main application page or a dashboard
    res.redirect('/');
  }
);

app.get('/logout', (req, res, next) => {
    req.logout(err => {
        if (err) { return next(err); }
        // Clear tokens from in-memory store if applicable (for demo purposes)
        if (req.user && users[req.user.id]) {
            delete users[req.user.id].accessToken;
            delete users[req.user.id].refreshToken;
        }
        res.redirect('/');
    });
});

// --- NEW: API Endpoint to Handle Gemini Tool Calls from AI Studio (Conceptual) ---
// This is the core piece that connects AI Studio's function calls to your backend.
// In a real application, this would be a secure endpoint that:
// 1. Receives the tool call from AI Studio (after Gemini decides to call a function).
// 2. Executes the corresponding function using the Google APIs.
// 3. Returns the result back to AI Studio (which then feeds it back to Gemini).

app.post('/api/gemini-tool-call', async (req, res) => {
    if (!req.isAuthenticated()) {
        return res.status(401).json({ error: 'Unauthorized: User not logged in.' });
    }

    const { toolName, args } = req.body; // Expecting toolName and args from AI Studio's output

    console.log(`Received tool call: ${toolName} with args:`, args);

    let result;
    try {
        // Authenticate the OAuth2Client with the current user's tokens before making API calls
        // In a real app, retrieve user's tokens from a session store or database.
        const currentUser = users[req.user.id]; // Access token from in-memory users object
        if (!currentUser || !currentUser.accessToken) {
            throw new Error('User access token not found for API call.');
        }
        oauth2Client.setCredentials({
            access_token: currentUser.accessToken,
            refresh_token: currentUser.refreshToken // Include refresh token if available for long-lived sessions
        });

        switch (toolName) {
            case 'gmail_read_emails':
                result = await gmail_read_emails(args);
                break;
            case 'drive_search_files':
                result = await drive_search_files(args);
                break;
            case 'calendar_get_events':
                result = await calendar_get_events(args);
                break;
            case 'contacts_search':
                result = await contacts_search(args);
                break;
            case 'photos_search_media':
                result = await photos_search_media(args);
                break;
            default:
                throw new Error(`Unknown tool: ${toolName}`);
        }
        res.json({ success: true, data: result });
    } catch (error) {
        console.error(`Error executing tool ${toolName}:`, error.message, error.stack);
        res.status(500).json({ success: false, error: error.message });
    }
});


// --- NEW: Functions to interact with Google APIs (matching AI Studio declarations) ---
// These functions use the oauth2Client initialized with the user's tokens.

async function gmail_read_emails(args) {
    if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Gmail API.');
    const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
    const queryParts = [];
    if (args.subject) queryParts.push(`subject:(${args.subject})`);
    if (args.sender) queryParts.push(`from:(${args.sender})`);
    if (args.label) queryParts.push(`label:(${args.label})`);
    if (args.keywords) queryParts.push(args.keywords);
    
    const q = queryParts.join(' ');
    
    console.log(`Calling Gmail API with query: "${q}", maxResults: ${args.maxResults}`);
    const res = await gmail.users.messages.list({
        userId: 'me',
        q: q,
        maxResults: args.maxResults || 5,
        fields: 'messages(id,internalDate,payload(headers))'
    });
    
    const messages = res.data.messages || [];
    const detailedMessages = await Promise.all(messages.map(async (message) => {
        const msg = await gmail.users.messages.get({ userId: 'me', id: message.id, format: 'metadata', fields: 'snippet,payload(headers)' });
        const headers = msg.data.payload.headers;
        const subject = headers.find(header => header.name === 'Subject')?.value;
        const from = headers.find(header => header.name === 'From')?.value;
        return {
            id: message.id,
            subject: subject,
            from: from,
            snippet: msg.data.snippet
        };
    }));
    return detailedMessages;
}

async function drive_search_files(args) {
    if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Drive API.');
    const drive = google.drive({ version: 'v3', auth: oauth2Client });
    
    const queryParts = ["trashed = false"];
    if (args.keywords) queryParts.push(`(name contains '${args.keywords}' or fullText contains '${args.keywords}')`);
    if (args.fileType) queryParts.push(`mimeType contains 'application/vnd.google-apps.${args.fileType}'`);
    if (args.folderName) {
        console.warn("Drive API folderName search is simplified. Consider using folder IDs for precision.");
        queryParts.push(`'${args.folderName}' in parents`);
    }

    const q = queryParts.join(' and ');
    console.log(`Calling Drive API with query: "${q}", maxResults: ${args.maxResults}`);

    const res = await drive.files.list({
        q: q,
        pageSize: args.maxResults || 5,
        fields: 'files(id, name, mimeType, webContentLink, parents)',
    });
    return res.data.files;
}

async function calendar_get_events(args) {
    if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Calendar API.');
    const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
    
    const timeMin = args.timeMin ? new Date(args.timeMin).toISOString() : (new Date()).toISOString();
    const timeMax = args.timeMax ? new Date(args.timeMax).toISOString() : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to next 7 days

    console.log(`Calling Calendar API from ${timeMin} to ${timeMax} with keywords: "${args.keywords}", maxResults: ${args.maxResults}`);
    const res = await calendar.events.list({
        calendarId: 'primary',
        timeMin: timeMin,
        timeMax: timeMax,
        q: args.keywords,
        maxResults: args.maxResults || 5,
        singleEvents: true,
        orderBy: 'startTime',
    });
    return res.data.items;
}

async function contacts_search(args) {
    if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for People API.');
    const people = google.people({ version: 'v1', auth: oauth2Client });
    
    console.log(`Calling People API with query: "${args.query}", maxResults: ${args.maxResults}`);
    const res = await people.people.connections.list({
        resourceName: 'people/me',
        personFields: 'names,emailAddresses,phoneNumbers',
        query: args.query,
        pageSize: args.maxResults || 5,
    });
    const connections = res.data.connections || [];
    return connections.map(person => ({
        displayName: person.names && person.names.length > 0 ? person.names[0].displayName : 'N/A',
        email: person.emailAddresses && person.emailAddresses.length > 0 ? person.emailAddresses[0].value : 'N/A',
        phone: person.phoneNumbers && person.phoneNumbers.length > 0 ? person.phoneNumbers[0].value : 'N/A',
    }));
}

async function photos_search_media(args) {
    if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Photos Library API.');
    const photoslibrary = google.photoslibrary({ version: 'v1', auth: oauth2Client });

    const filters = {};
    if (args.keywords) {
        console.warn("Photos Library API keyword search is highly simplified. Complex keyword mapping needed.");
    }
    if (args.startDate && args.endDate) {
        filters.dateFilter = {
            ranges: [{
                startDate: { year: parseInt(args.startDate.substring(0,4)), month: parseInt(args.startDate.substring(5,7)), day: parseInt(args.startDate.substring(8,10)) },
                endDate: { year: parseInt(args.endDate.substring(0,4)), month: parseInt(args.endDate.substring(5,7)), day: parseInt(args.endDate.substring(8,10)) }
            }]
        };
    }

    console.log(`Calling Photos Library API with filters:`, filters, `maxResults: ${args.maxResults}`);
    const res = await photoslibrary.mediaItems.search({
        filters: filters,
        pageSize: args.maxResults || 5,
    });
    const mediaItems = res.data.mediaItems || [];
    return mediaItems.map(item => ({
        id: item.id,
        filename: item.filename,
        baseUrl: item.baseUrl,
        mediaMetadata: item.mediaMetadata,
    }));
}


// --- API Router for other custom endpoints ---
app.use('/api', apiRouter);

// --- Start Server ---
app.listen(PORT, () => {
  console.log(`🚀 Server is running on http://localhost:${PORT}`);
});