- ✨ Follow arms to personalize your feed. Only posts from followed arms will appear in your "Following" tab.
+ ✨ Follow arms to personalize your feed. Only posts from
+ followed arms will appear in your "Following" tab.
{ARMS.map((arm) => (
))}
diff --git a/client/pages/SubdomainPassport.tsx b/client/pages/SubdomainPassport.tsx
index f85f5ca8..45f8eb21 100644
--- a/client/pages/SubdomainPassport.tsx
+++ b/client/pages/SubdomainPassport.tsx
@@ -62,11 +62,11 @@ const SubdomainPassport = () => {
let url = "";
if (subdomainInfo.isCreatorPassport) {
url = `${API_BASE}/api/passport/subdomain/${encodeURIComponent(
- subdomainInfo.subdomain
+ subdomainInfo.subdomain,
)}`;
} else if (subdomainInfo.isProjectPassport) {
url = `${API_BASE}/api/passport/project/${encodeURIComponent(
- subdomainInfo.subdomain
+ subdomainInfo.subdomain,
)}`;
}
@@ -82,7 +82,7 @@ const SubdomainPassport = () => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
- errorData.error || `HTTP ${response.status}: Not found`
+ errorData.error || `HTTP ${response.status}: Not found`,
);
}
diff --git a/discord-bot/events/messageCreate-announcements.js b/discord-bot/events/messageCreate-announcements.js
index a86965d1..446379a2 100644
--- a/discord-bot/events/messageCreate-announcements.js
+++ b/discord-bot/events/messageCreate-announcements.js
@@ -69,8 +69,7 @@ module.exports = {
.insert({
username: "aethex-announcements",
full_name: "AeThex Announcements",
- avatar_url:
- "https://aethex.dev/logo.png",
+ avatar_url: "https://aethex.dev/logo.png",
})
.select("id");
@@ -108,9 +107,9 @@ module.exports = {
if (imageExtensions.some((ext) => attachmentLower.endsWith(ext))) {
mediaType = "image";
- } else if (videoExtensions.some((ext) =>
- attachmentLower.endsWith(ext),
- )) {
+ } else if (
+ videoExtensions.some((ext) => attachmentLower.endsWith(ext))
+ ) {
mediaType = "video";
}
}
@@ -149,11 +148,17 @@ module.exports = {
);
if (insertError) {
- console.error("[Announcements Sync] Failed to create post:", insertError);
+ console.error(
+ "[Announcements Sync] Failed to create post:",
+ insertError,
+ );
try {
await message.react("❌");
} catch (reactionError) {
- console.warn("[Announcements Sync] Could not add reaction:", reactionError);
+ console.warn(
+ "[Announcements Sync] Could not add reaction:",
+ reactionError,
+ );
}
return;
}
@@ -197,7 +202,10 @@ module.exports = {
}),
});
} catch (webhookError) {
- console.warn("[Announcements Sync] Failed to sync to webhook:", webhookError);
+ console.warn(
+ "[Announcements Sync] Failed to sync to webhook:",
+ webhookError,
+ );
}
}
@@ -209,7 +217,10 @@ module.exports = {
try {
await message.react("✅");
} catch (reactionError) {
- console.warn("[Announcements Sync] Could not add success reaction:", reactionError);
+ console.warn(
+ "[Announcements Sync] Could not add success reaction:",
+ reactionError,
+ );
}
} catch (error) {
console.error("[Announcements Sync] Unexpected error:", error);
@@ -217,7 +228,10 @@ module.exports = {
try {
await message.react("⚠️");
} catch (reactionError) {
- console.warn("[Announcements Sync] Could not add warning reaction:", reactionError);
+ console.warn(
+ "[Announcements Sync] Could not add warning reaction:",
+ reactionError,
+ );
}
}
},
diff --git a/discord-bot/events/messageCreate.js b/discord-bot/events/messageCreate.js
index 691b39df..344c3b2e 100644
--- a/discord-bot/events/messageCreate.js
+++ b/discord-bot/events/messageCreate.js
@@ -87,13 +87,17 @@ async function handleAnnouncementSync(message) {
mediaUrl = attachment.url;
const attachmentLower = attachment.name.toLowerCase();
- if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
- attachmentLower.endsWith(ext),
- )) {
+ if (
+ [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
+ attachmentLower.endsWith(ext),
+ )
+ ) {
mediaType = "image";
- } else if ([".mp4", ".webm", ".mov", ".avi"].some((ext) =>
- attachmentLower.endsWith(ext),
- )) {
+ } else if (
+ [".mp4", ".webm", ".mov", ".avi"].some((ext) =>
+ attachmentLower.endsWith(ext),
+ )
+ ) {
mediaType = "video";
}
}
@@ -137,9 +141,7 @@ async function handleAnnouncementSync(message) {
return;
}
- console.log(
- `[Announcements] ✅ Synced to AeThex (${armAffiliation} arm)`,
- );
+ console.log(`[Announcements] ✅ Synced to AeThex (${armAffiliation} arm)`);
await message.react("✅");
} catch (error) {
@@ -160,7 +162,10 @@ module.exports = {
if (!message.content && message.attachments.size === 0) return;
// Check if this is an announcement to sync
- if (ANNOUNCEMENT_CHANNELS.length > 0 && ANNOUNCEMENT_CHANNELS.includes(message.channelId)) {
+ if (
+ ANNOUNCEMENT_CHANNELS.length > 0 &&
+ ANNOUNCEMENT_CHANNELS.includes(message.channelId)
+ ) {
return handleAnnouncementSync(message);
}
@@ -200,7 +205,10 @@ module.exports = {
.single();
if (profileError || !userProfile) {
- console.error("[Feed Sync] Could not fetch user profile:", profileError);
+ console.error(
+ "[Feed Sync] Could not fetch user profile:",
+ profileError,
+ );
return;
}
@@ -215,13 +223,17 @@ module.exports = {
mediaUrl = attachment.url;
const attachmentLower = attachment.name.toLowerCase();
- if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
- attachmentLower.endsWith(ext),
- )) {
+ if (
+ [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) =>
+ attachmentLower.endsWith(ext),
+ )
+ ) {
mediaType = "image";
- } else if ([".mp4", ".webm", ".mov", ".avi"].some((ext) =>
- attachmentLower.endsWith(ext),
- )) {
+ } else if (
+ [".mp4", ".webm", ".mov", ".avi"].some((ext) =>
+ attachmentLower.endsWith(ext),
+ )
+ ) {
mediaType = "video";
}
}
@@ -234,7 +246,8 @@ module.exports = {
const guildNameLower = guild.name.toLowerCase();
if (guildNameLower.includes("gameforge")) armAffiliation = "gameforge";
else if (guildNameLower.includes("corp")) armAffiliation = "corp";
- else if (guildNameLower.includes("foundation")) armAffiliation = "foundation";
+ else if (guildNameLower.includes("foundation"))
+ armAffiliation = "foundation";
else if (guildNameLower.includes("devlink")) armAffiliation = "devlink";
else if (guildNameLower.includes("nexus")) armAffiliation = "nexus";
else if (guildNameLower.includes("staff")) armAffiliation = "staff";
@@ -276,14 +289,15 @@ module.exports = {
return;
}
- console.log(
- `[Feed Sync] ✅ Posted from ${message.author.tag} to AeThex`,
- );
+ console.log(`[Feed Sync] ✅ Posted from ${message.author.tag} to AeThex`);
try {
await message.react("✅");
} catch (reactionError) {
- console.warn("[Feed Sync] Could not add success reaction:", reactionError);
+ console.warn(
+ "[Feed Sync] Could not add success reaction:",
+ reactionError,
+ );
}
try {
diff --git a/docs/FEED-PHASE1-IMPLEMENTATION.md b/docs/FEED-PHASE1-IMPLEMENTATION.md
index 28c5463a..27bfbf97 100644
--- a/docs/FEED-PHASE1-IMPLEMENTATION.md
+++ b/docs/FEED-PHASE1-IMPLEMENTATION.md
@@ -34,7 +34,9 @@ Phase 1 is the **read-only, curated foundation** that proves the Axiom Model wor
## Features Implemented
### 1. **Arm Affiliation Theming** ✅
+
Every post displays a **color-coded badge** and **left border accent** matching the Arm:
+
- **LABS** (Yellow): Innovation & experimentation
- **GAMEFORGE** (Green): Game development
- **CORP** (Blue): Commercial partnerships
@@ -46,17 +48,22 @@ Every post displays a **color-coded badge** and **left border accent** matching
**Why this matters**: The colors are the **visual proof of the Firewall**. At a glance, you know what type of content you're reading.
### 2. **Arm Follow System** ✅
+
Users can now:
+
- Follow specific Arms
- Personalize their feed to show only followed Arms
- Access the "Following" tab to see curated content
-**Database**:
+**Database**:
+
- New `arm_follows` table tracks user -> arm relationships
- RLS policies ensure users can only manage their own follows
### 3. **Arm-Specific Feeds** ✅
+
New routes available:
+
- `/labs` - Labs feed only
- `/gameforge` - GameForge feed only
- `/corp` - Corp feed only
@@ -66,29 +73,35 @@ New routes available:
- `/staff` - Staff feed only
Each has:
+
- Dedicated header with Arm icon & description
- Content filtered to that Arm only
- Same interaction system (like, comment, share)
### 4. **Admin Feed Manager** ✅
+
**Route**: `/admin/feed`
Founders/Admins can now create **system announcements** that seed the feed. Features:
+
- Title & content editor (max 500 & 5000 chars)
- Arm affiliation selector
- Tag management
- One-click publish
**Use cases**:
+
- Announce new partnerships
- Showcase Arm-to-Arm collaborations
- Prove the "Talent Flywheel" in action
- Demonstrate ethical separation
### 5. **Discord Announcements Sync** ✅
+
**One-way**: Discord → AeThex Feed
The Discord bot now listens to configured announcement channels and automatically:
+
1. Posts to the AeThex feed
2. Auto-detects Arm affiliation from channel/guild name
3. Includes media (images, videos)
@@ -96,6 +109,7 @@ The Discord bot now listens to configured announcement channels and automaticall
5. Reacts with ✅ when successful
**Configuration**:
+
```env
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702,your_other_channels
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/...
@@ -110,6 +124,7 @@ DISCORD_FEED_CHANNEL_ID=1425114041021497454
### New Tables
#### `arm_follows`
+
```sql
id BIGSERIAL PRIMARY KEY
user_id UUID REFERENCES auth.users(id)
@@ -121,6 +136,7 @@ UNIQUE(user_id, arm_affiliation)
```
#### `community_posts` (Updated)
+
```sql
-- Already existed, now with validated arm_affiliation
arm_affiliation TEXT NOT NULL CHECK (arm_affiliation IN (...))
@@ -136,24 +152,28 @@ CREATE INDEX idx_community_posts_created_at ON community_posts(created_at DESC)
### Feed Management
#### Get Arm Follows
+
```
GET /api/user/arm-follows?user_id={userId}
Returns: { arms: ["labs", "gameforge", ...] }
```
#### Follow an Arm
+
```
POST /api/user/arm-follows?user_id={userId}
Body: { arm_affiliation: "labs" }
```
#### Unfollow an Arm
+
```
DELETE /api/user/arm-follows?user_id={userId}
Body: { arm_affiliation: "labs" }
```
#### Create Post (Admin)
+
```
POST /api/community/posts
Body: {
@@ -169,6 +189,7 @@ Body: {
### Discord Integration
#### Discord Webhook Sync
+
```
POST /api/discord/feed-sync
Body: {
@@ -189,6 +210,7 @@ Body: {
## File Changes Summary
### New Files Created
+
- `code/client/pages/AdminFeed.tsx` - Admin feed manager UI
- `code/client/components/feed/ArmFeed.tsx` - Reusable Arm feed component
- `code/client/pages/ArmFeeds.tsx` - Individual Arm feed page exports
@@ -199,6 +221,7 @@ Body: {
- `code/discord-bot/.env.example` - Environment variable template
### Modified Files
+
- `code/client/components/social/FeedItemCard.tsx` - Added Arm badges & visual theming
- `code/client/pages/Feed.tsx` - Added arm follow management UI
- `code/discord-bot/bot.js` - Enhanced to load event listeners with correct intents
@@ -208,6 +231,7 @@ Body: {
## Deployment Checklist
### 1. Database Migrations
+
```bash
npx supabase migration up
# OR manually apply:
@@ -215,7 +239,9 @@ npx supabase migration up
```
### 2. Environment Variables
+
Set in your production environment:
+
```env
DISCORD_ANNOUNCEMENT_CHANNELS=1435667453244866702
DISCORD_FEED_WEBHOOK_URL=https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN
@@ -225,7 +251,9 @@ VITE_API_BASE=https://your-api-domain.com
```
### 3. Update App Routing
+
Add these routes to `code/client/App.tsx`:
+
```typescript
{
path: "/admin/feed",
@@ -262,7 +290,9 @@ Add these routes to `code/client/App.tsx`:
```
### 4. Discord Bot Restart
+
Restart the Discord bot for it to:
+
1. Load the new message event listener
2. Subscribe to announcement channels
3. Start syncing posts
@@ -272,17 +302,20 @@ Restart the Discord bot for it to:
## Usage Guide
### For Founders/Admins
+
1. Go to `/admin/feed`
2. Write your announcement
3. Select the appropriate Arm
4. Publish
Example post:
+
> **Title**: GameForge + Foundation Partnership
> **Content**: We're thrilled to announce that GameForge will hire 3 Artists from Foundation via Nexus. This is the Talent Flywheel in action.
> **Arm**: gameforge
### For Users
+
1. Go to `/feed` (main unified feed)
2. Manage which Arms you follow using "Manage Follows"
3. Filter the feed with the Arm buttons
@@ -294,6 +327,7 @@ Example post:
## Phase 2: User-Generated Posts
Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will add:
+
- User post composer in the `/feed` page
- Moderation queue for new user posts
- Reputation scoring
@@ -319,12 +353,14 @@ Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will
## Performance Notes
**Indexes Added**:
+
- `idx_community_posts_arm_affiliation` - Fast Arm filtering
- `idx_community_posts_created_at` - Fast sorting by date
- `idx_arm_follows_user_id` - Fast user follow lookups
- `idx_arm_follows_arm` - Fast arm-based queries
**Caching Recommendations** (Phase 2):
+
- Cache user's followed Arms for 5 minutes
- Cache trending posts per Arm
- Use Redis for real-time engagement counts
@@ -334,6 +370,7 @@ Once Phase 1 is proven (admin posts working, Discord sync working), Phase 2 will
## Contact & Support
For questions on Phase 1 implementation or moving to Phase 2, refer to:
+
- `/api/community/posts` - Main post creation API
- `/api/user/arm-follows` - Arm follow management
- `code/discord-bot/events/messageCreate.js` - Discord sync logic
diff --git a/server/index.ts b/server/index.ts
index 8e033fb2..d2b9c8d9 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -309,7 +309,9 @@ export function createServer() {
// Subdomain detection middleware for aethex.me and aethex.space
app.use((req, res, next) => {
const host = (req.headers.host || "").toLowerCase();
- const forwarded = ((req.headers["x-forwarded-host"] as string) || "").toLowerCase();
+ const forwarded = (
+ (req.headers["x-forwarded-host"] as string) || ""
+ ).toLowerCase();
const hostname = forwarded || host;
// Parse subdomain
@@ -358,7 +360,9 @@ export function createServer() {
// API: Creator passport lookup by subdomain (aethex.me)
app.get("/api/passport/subdomain/:username", async (req, res) => {
try {
- const username = String(req.params.username || "").toLowerCase().trim();
+ const username = String(req.params.username || "")
+ .toLowerCase()
+ .trim();
if (!username) {
return res.status(400).json({ error: "username required" });
}
@@ -366,7 +370,7 @@ export function createServer() {
const { data, error } = await adminSupabase
.from("user_profiles")
.select(
- "id, username, full_name, avatar_url, user_type, bio, created_at, email"
+ "id, username, full_name, avatar_url, user_type, bio, created_at, email",
)
.eq("username", username)
.single();
@@ -405,7 +409,7 @@ export function createServer() {
let query = adminSupabase
.from("projects")
.select(
- "id, title, slug, description, user_id, created_at, updated_at, status, image_url, website"
+ "id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
)
.eq("slug", projectname);
@@ -416,7 +420,7 @@ export function createServer() {
query = adminSupabase
.from("projects")
.select(
- "id, title, slug, description, user_id, created_at, updated_at, status, image_url, website"
+ "id, title, slug, description, user_id, created_at, updated_at, status, image_url, website",
)
.ilike("title", projectname);
@@ -3252,7 +3256,9 @@ export function createServer() {
app.get("/api/social/following", async (req, res) => {
const userId = req.query.userId as string;
if (!userId) {
- return res.status(400).json({ error: "userId query parameter required" });
+ return res
+ .status(400)
+ .json({ error: "userId query parameter required" });
}
try {
const { data, error } = await adminSupabase
@@ -3278,7 +3284,9 @@ export function createServer() {
app.get("/api/social/followers", async (req, res) => {
const userId = req.query.userId as string;
if (!userId) {
- return res.status(400).json({ error: "userId query parameter required" });
+ return res
+ .status(400)
+ .json({ error: "userId query parameter required" });
}
try {
const { data, error } = await adminSupabase