diff --git a/.gitignore b/.gitignore index 7b110f1..15cc788 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,4 @@ temp_server.env .zshrc .profile +sojorn_docs/SOJORN_ARCHITECTURE.md diff --git a/Caddyfile b/Caddyfile index babbfb3..59ca471 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,3 +1,3 @@ -api.gosojorn.com { +api.sojorn.net { reverse_proxy localhost:8080 } diff --git a/_legacy/supabase/functions/_shared/r2_signer.ts b/_legacy/supabase/functions/_shared/r2_signer.ts index 03dbd9b..ed820ce 100644 --- a/_legacy/supabase/functions/_shared/r2_signer.ts +++ b/_legacy/supabase/functions/_shared/r2_signer.ts @@ -1,7 +1,7 @@ import { AwsClient } from 'https://esm.sh/aws4fetch@1.0.17' -const CUSTOM_MEDIA_DOMAIN = (Deno.env.get("CUSTOM_MEDIA_DOMAIN") ?? "https://img.gosojorn.com").trim(); -const CUSTOM_VIDEO_DOMAIN = (Deno.env.get("CUSTOM_VIDEO_DOMAIN") ?? "https://quips.gosojorn.com").trim(); +const CUSTOM_MEDIA_DOMAIN = (Deno.env.get("CUSTOM_MEDIA_DOMAIN") ?? "https://img.sojorn.net").trim(); +const CUSTOM_VIDEO_DOMAIN = (Deno.env.get("CUSTOM_VIDEO_DOMAIN") ?? "https://quips.sojorn.net").trim(); const DEFAULT_BUCKET_NAME = "sojorn-media"; const RESOLVED_BUCKET = (Deno.env.get("R2_BUCKET_NAME") ?? DEFAULT_BUCKET_NAME).trim(); @@ -36,8 +36,8 @@ export function transformLegacyMediaUrl(input: string): string | null { try { const url = new URL(trimmed); - // Handle legacy media.gosojorn.com URLs - if (url.hostname === 'media.gosojorn.com') { + // Handle legacy media.sojorn.net URLs + if (url.hostname === 'media.sojorn.net') { const key = decodeURIComponent(url.pathname); return key; } diff --git a/_legacy/supabase/functions/cleanup-expired-content/index.ts b/_legacy/supabase/functions/cleanup-expired-content/index.ts index f8437ce..357345a 100644 --- a/_legacy/supabase/functions/cleanup-expired-content/index.ts +++ b/_legacy/supabase/functions/cleanup-expired-content/index.ts @@ -2,7 +2,7 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { S3Client, DeleteObjectCommand } from 'https://esm.sh/@aws-sdk/client-s3@3.470.0'; import { createServiceClient } from '../_shared/supabase-client.ts'; -const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com'; +const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net'; const corsHeaders = { 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, diff --git a/_legacy/supabase/functions/deactivate-account/index.ts b/_legacy/supabase/functions/deactivate-account/index.ts index ffbede7..451c0a5 100644 --- a/_legacy/supabase/functions/deactivate-account/index.ts +++ b/_legacy/supabase/functions/deactivate-account/index.ts @@ -11,7 +11,7 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createSupabaseClient } from '../_shared/supabase-client.ts'; -const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com'; +const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net'; serve(async (req) => { if (req.method === 'OPTIONS') { diff --git a/_legacy/supabase/functions/delete-account/index.ts b/_legacy/supabase/functions/delete-account/index.ts index 0d70e1d..9d90ad7 100644 --- a/_legacy/supabase/functions/delete-account/index.ts +++ b/_legacy/supabase/functions/delete-account/index.ts @@ -12,7 +12,7 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createSupabaseClient } from '../_shared/supabase-client.ts'; -const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com'; +const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net'; serve(async (req) => { if (req.method === 'OPTIONS') { diff --git a/_legacy/supabase/functions/manage-post/index.ts b/_legacy/supabase/functions/manage-post/index.ts index 5d7e9a8..0a9108d 100644 --- a/_legacy/supabase/functions/manage-post/index.ts +++ b/_legacy/supabase/functions/manage-post/index.ts @@ -2,7 +2,7 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; import { createSupabaseClient, createServiceClient } from '../_shared/supabase-client.ts'; -const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com'; +const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net'; const corsHeaders = { 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, diff --git a/_legacy/supabase/functions/sign-media/index.ts b/_legacy/supabase/functions/sign-media/index.ts index 96ff3f3..a26303b 100644 --- a/_legacy/supabase/functions/sign-media/index.ts +++ b/_legacy/supabase/functions/sign-media/index.ts @@ -55,7 +55,7 @@ serve(async (req) => { }); } - // Transform legacy media.gosojorn.com URLs to their object key + // Transform legacy media.sojorn.net URLs to their object key const transformedTarget = transformLegacyMediaUrl(target) ?? target; const signedUrl = await trySignR2Url(transformedTarget, expiresIn); diff --git a/_legacy/supabase/functions/signup/index.ts b/_legacy/supabase/functions/signup/index.ts index ac68b6c..8734474 100644 --- a/_legacy/supabase/functions/signup/index.ts +++ b/_legacy/supabase/functions/signup/index.ts @@ -26,7 +26,7 @@ serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: { - 'Access-Control-Allow-Origin': Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com', + 'Access-Control-Allow-Origin': Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }, diff --git a/_legacy/supabase/functions/tone-check/index.ts b/_legacy/supabase/functions/tone-check/index.ts index 7828694..b916f32 100644 --- a/_legacy/supabase/functions/tone-check/index.ts +++ b/_legacy/supabase/functions/tone-check/index.ts @@ -3,7 +3,7 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' const OPENAI_MODERATION_URL = 'https://api.openai.com/v1/moderations' -const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com'; +const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net'; const corsHeaders = { 'Access-Control-Allow-Origin': ALLOWED_ORIGIN, diff --git a/html_landing/index.html b/html_landing/index.html index b8f0d4e..2b1b0ba 100644 --- a/html_landing/index.html +++ b/html_landing/index.html @@ -49,7 +49,7 @@
Newsletter - Contact
@@ -119,8 +119,8 @@

- Join the waitlist by emailing waitlist@gosojorn.com + Join the waitlist by emailing waitlist@sojorn.net

diff --git a/html_landing/privacy.html b/html_landing/privacy.html index b6fc06c..b05f504 100644 --- a/html_landing/privacy.html +++ b/html_landing/privacy.html @@ -73,8 +73,8 @@ you leave, you leave. We do not retain hidden profiles.

5. Contact

-

For privacy concerns: privacy@gosojorn.com.

+

For privacy concerns: privacy@sojorn.net.

diff --git a/migrations_archive/fix_remaining_url.sql b/migrations_archive/fix_remaining_url.sql index 0b97d72..2d2876e 100644 --- a/migrations_archive/fix_remaining_url.sql +++ b/migrations_archive/fix_remaining_url.sql @@ -1,28 +1,28 @@ --- Fix sojorn-media URLs (for images) to img.gosojorn.com +-- Fix sojorn-media URLs (for images) to img.sojorn.net UPDATE profiles -SET avatar_url = REGEXP_REPLACE(avatar_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g') +SET avatar_url = REGEXP_REPLACE(avatar_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g') WHERE avatar_url LIKE '%r2.cloudflarestorage.com/sojorn-media%'; UPDATE profiles -SET cover_url = REGEXP_REPLACE(cover_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g') +SET cover_url = REGEXP_REPLACE(cover_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g') WHERE cover_url LIKE '%r2.cloudflarestorage.com/sojorn-media%'; UPDATE posts -SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g') +SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g') WHERE image_url LIKE '%r2.cloudflarestorage.com/sojorn-media%'; UPDATE posts -SET thumbnail_url = REGEXP_REPLACE(thumbnail_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g') +SET thumbnail_url = REGEXP_REPLACE(thumbnail_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g') WHERE thumbnail_url LIKE '%r2.cloudflarestorage.com/sojorn-media%'; --- Fix sojorn-videos URLs (for quips) to quips.gosojorn.com +-- Fix sojorn-videos URLs (for quips) to quips.sojorn.net UPDATE posts -SET video_url = REGEXP_REPLACE(video_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.gosojorn.com/', 'g') +SET video_url = REGEXP_REPLACE(video_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.sojorn.net/', 'g') WHERE video_url LIKE '%r2.cloudflarestorage.com/sojorn-videos%'; -- Fix the one edge case where image_url contains a video URL UPDATE posts -SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.gosojorn.com/', 'g') +SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.sojorn.net/', 'g') WHERE image_url LIKE '%r2.cloudflarestorage.com/sojorn-videos%'; -- Verify after diff --git a/migrations_archive/repair_all_media_urls.sql b/migrations_archive/repair_all_media_urls.sql index 00236f8..53a043c 100644 --- a/migrations_archive/repair_all_media_urls.sql +++ b/migrations_archive/repair_all_media_urls.sql @@ -3,7 +3,7 @@ -- 1. Fix image URLs that are just filenames (no domain) UPDATE posts -SET image_url = 'https://img.gosojorn.com/' || image_url +SET image_url = 'https://img.sojorn.net/' || image_url WHERE image_url IS NOT NULL AND image_url != '' AND image_url NOT LIKE 'http%' @@ -11,7 +11,7 @@ WHERE image_url IS NOT NULL -- 2. Fix video URLs that are just filenames (no domain) UPDATE posts -SET video_url = 'https://quips.gosojorn.com/' || video_url +SET video_url = 'https://quips.sojorn.net/' || video_url WHERE video_url IS NOT NULL AND video_url != '' AND video_url NOT LIKE 'http%' @@ -19,7 +19,7 @@ WHERE video_url IS NOT NULL -- 3. Fix thumbnail URLs that are just filenames UPDATE posts -SET thumbnail_url = 'https://img.gosojorn.com/' || thumbnail_url +SET thumbnail_url = 'https://img.sojorn.net/' || thumbnail_url WHERE thumbnail_url IS NOT NULL AND thumbnail_url != '' AND thumbnail_url NOT LIKE 'http%' @@ -27,7 +27,7 @@ WHERE thumbnail_url IS NOT NULL -- 4. Fix profile avatars that are just filenames UPDATE profiles -SET avatar_url = 'https://img.gosojorn.com/' || avatar_url +SET avatar_url = 'https://img.sojorn.net/' || avatar_url WHERE avatar_url IS NOT NULL AND avatar_url != '' AND avatar_url NOT LIKE 'http%' @@ -35,7 +35,7 @@ WHERE avatar_url IS NOT NULL -- 5. Fix profile covers that are just filenames UPDATE profiles -SET cover_url = 'https://img.gosojorn.com/' || cover_url +SET cover_url = 'https://img.sojorn.net/' || cover_url WHERE cover_url IS NOT NULL AND cover_url != '' AND cover_url NOT LIKE 'http%' @@ -51,12 +51,12 @@ SELECT 'Profiles with covers' as check_type, count(*) as count FROM profiles WHE -- Show any remaining non-standard URLs SELECT 'Non-standard image URLs' as type, image_url FROM posts WHERE image_url IS NOT NULL - AND image_url NOT LIKE 'https://img.gosojorn.com/%' - AND image_url NOT LIKE 'https://quips.gosojorn.com/%' + AND image_url NOT LIKE 'https://img.sojorn.net/%' + AND image_url NOT LIKE 'https://quips.sojorn.net/%' LIMIT 5; SELECT 'Non-standard video URLs' as type, video_url FROM posts WHERE video_url IS NOT NULL - AND video_url NOT LIKE 'https://img.gosojorn.com/%' - AND video_url NOT LIKE 'https://quips.gosojorn.com/%' + AND video_url NOT LIKE 'https://img.sojorn.net/%' + AND video_url NOT LIKE 'https://quips.sojorn.net/%' LIMIT 5; diff --git a/nginx/legacy_redirect.conf b/nginx/legacy_redirect.conf new file mode 100644 index 0000000..ff77f01 --- /dev/null +++ b/nginx/legacy_redirect.conf @@ -0,0 +1,34 @@ +# Redirect Legacy HTTP -> New HTTPS (gosojorn.com & gojorn.com) +server { + server_name gosojorn.com api.gosojorn.com gojorn.com www.gojorn.com; + listen 80; + return 301 https://sojorn.net$request_uri; +} + +# Redirect Legacy HTTPS -> New HTTPS (gosojorn.com ONLY) +# We can only serve SSL for domains we have certs for (gosojorn) +server { + server_name gosojorn.com; + listen 443 ssl; + + # Use EXISTING legacy certificates + ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + return 301 https://sojorn.net$request_uri; +} + +server { + server_name api.gosojorn.com; + listen 443 ssl; + + # Use EXISTING legacy certificates + ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + return 301 https://api.sojorn.net$request_uri; +} diff --git a/nginx/sojorn.conf b/nginx/sojorn.conf index 9da377d..35e3a3e 100644 --- a/nginx/sojorn.conf +++ b/nginx/sojorn.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name api.gosojorn.com; + server_name api.sojorn.net; location / { proxy_pass http://localhost:8080; @@ -12,7 +12,7 @@ server { server { listen 80; - server_name gosojorn.com; + server_name sojorn.net; root /var/www/sojorn; index index.html; diff --git a/nginx/sojorn_final.conf b/nginx/sojorn_final.conf index 0102be3..cb429af 100644 --- a/nginx/sojorn_final.conf +++ b/nginx/sojorn_final.conf @@ -1,5 +1,5 @@ server { - server_name api.gosojorn.com; + server_name api.sojorn.net; location / { proxy_pass http://localhost:8080; @@ -14,14 +14,14 @@ server { } listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { - server_name gosojorn.com; + server_name sojorn.net; root /var/www/sojorn; index index.html; @@ -30,21 +30,21 @@ server { } listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { - if ($host = api.gosojorn.com) { + if ($host = api.sojorn.net) { return 301 https://$host$request_uri; } - if ($host = gosojorn.com) { + if ($host = sojorn.net) { return 301 https://$host$request_uri; } listen 80; - server_name api.gosojorn.com gosojorn.com; + server_name api.sojorn.net sojorn.net; return 404; } diff --git a/nginx/sojorn_net.conf b/nginx/sojorn_net.conf new file mode 100644 index 0000000..bd7adcd --- /dev/null +++ b/nginx/sojorn_net.conf @@ -0,0 +1,34 @@ +# sojorn.net - Frontend +server { + server_name sojorn.net www.sojorn.net; + root /var/www/sojorn; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Certbot will add SSL configuration here + listen 80; +} + +# api.sojorn.net - Backend +server { + server_name api.sojorn.net; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Certbot will add SSL configuration here + listen 80; +} diff --git a/nginx_sojorn_v2.conf b/nginx_sojorn_v2.conf index b58b700..600f61f 100644 --- a/nginx_sojorn_v2.conf +++ b/nginx_sojorn_v2.conf @@ -1,5 +1,5 @@ server { - server_name gosojorn.com www.gosojorn.com; + server_name sojorn.net www.sojorn.net; root /var/www/sojorn; index index.html; @@ -28,18 +28,18 @@ server { } listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { - if ($host = gosojorn.com) { + if ($host = sojorn.net) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; - server_name gosojorn.com www.gosojorn.com; + server_name sojorn.net www.sojorn.net; return 404; # managed by Certbot } diff --git a/run_web.ps1 b/run_web.ps1 index 8361ea1..7bda38d 100644 --- a/run_web.ps1 +++ b/run_web.ps1 @@ -10,7 +10,7 @@ function Parse-Env($path) { if (-not (Test-Path $path)) { Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow # Set default API_BASE_URL since no .env exists - $vals['API_BASE_URL'] = 'https://api.gosojorn.com/api/v1' + $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1' return $vals } Get-Content $path | ForEach-Object { @@ -41,16 +41,18 @@ foreach ($k in $keysOfInterest) { # Ensure API_BASE_URL is set if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) { - $currentApi = 'https://api.gosojorn.com/api/v1' + $currentApi = 'https://api.sojorn.net/api/v1' $defineArgs += "--dart-define=API_BASE_URL=$currentApi" -} else { +} +else { $currentApi = $values['API_BASE_URL'] # Always ensure we're using the HTTPS endpoint - if ($currentApi.StartsWith('http://api.gosojorn.com:8080')) { - $currentApi = $currentApi.Replace('http://api.gosojorn.com:8080', 'https://api.gosojorn.com') + if ($currentApi.StartsWith('http://api.sojorn.net:8080')) { + $currentApi = $currentApi.Replace('http://api.sojorn.net:8080', 'https://api.sojorn.net') $defineArgs = $defineArgs | Where-Object { -not ($_ -like '--dart-define=API_BASE_URL=*') } $defineArgs += "--dart-define=API_BASE_URL=$currentApi" - } elseif ($currentApi.StartsWith('http://localhost:')) { + } + elseif ($currentApi.StartsWith('http://localhost:')) { # For local development, keep localhost but warn Write-Host "Using local API: $currentApi" -ForegroundColor Yellow } diff --git a/run_web_chrome.ps1 b/run_web_chrome.ps1 index 70a996e..f71318e 100644 --- a/run_web_chrome.ps1 +++ b/run_web_chrome.ps1 @@ -10,7 +10,7 @@ function Parse-Env($path) { if (-not (Test-Path $path)) { Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow # Set default API_BASE_URL since no .env exists - $vals['API_BASE_URL'] = 'https://api.gosojorn.com/api/v1' + $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1' return $vals } Get-Content $path | ForEach-Object { @@ -41,16 +41,18 @@ foreach ($k in $keysOfInterest) { # Ensure API_BASE_URL is set if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) { - $currentApi = 'https://api.gosojorn.com/api/v1' + $currentApi = 'https://api.sojorn.net/api/v1' $defineArgs += "--dart-define=API_BASE_URL=$currentApi" -} else { +} +else { $currentApi = $values['API_BASE_URL'] # Always ensure we're using the HTTPS endpoint - if ($currentApi.StartsWith('http://api.gosojorn.com:8080')) { - $currentApi = $currentApi.Replace('http://api.gosojorn.com:8080', 'https://api.gosojorn.com') + if ($currentApi.StartsWith('http://api.sojorn.net:8080')) { + $currentApi = $currentApi.Replace('http://api.sojorn.net:8080', 'https://api.sojorn.net') $defineArgs = $defineArgs | Where-Object { -not ($_ -like '--dart-define=API_BASE_URL=*') } $defineArgs += "--dart-define=API_BASE_URL=$currentApi" - } elseif ($currentApi.StartsWith('http://localhost:')) { + } + elseif ($currentApi.StartsWith('http://localhost:')) { # For local development, keep localhost but warn Write-Host "Using local API: $currentApi" -ForegroundColor Yellow } diff --git a/run_windows.ps1 b/run_windows.ps1 index 0f2ebce..492fd4b 100644 --- a/run_windows.ps1 +++ b/run_windows.ps1 @@ -9,7 +9,7 @@ function Parse-Env($path) { if (-not (Test-Path $path)) { Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow # Set default API_BASE_URL since no .env exists - $vals['API_BASE_URL'] = 'https://api.gosojorn.com/api/v1' + $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1' return $vals } Get-Content $path | ForEach-Object { @@ -40,9 +40,10 @@ foreach ($k in $keysOfInterest) { # Ensure API_BASE_URL is set if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) { - $currentApi = 'https://api.gosojorn.com/api/v1' + $currentApi = 'https://api.sojorn.net/api/v1' $defineArgs += "--dart-define=API_BASE_URL=$currentApi" -} else { +} +else { $currentApi = $values['API_BASE_URL'] if ($currentApi.StartsWith('http://localhost:')) { # For local development, keep localhost but warn @@ -71,7 +72,8 @@ try { if ($Release) { $cmdArgs += '--release' - } else { + } + else { $cmdArgs += '--debug' } @@ -99,10 +101,12 @@ try { Write-Host "Starting Sojorn Windows app..." -ForegroundColor Yellow Start-Process -FilePath $exePath } - } else { + } + else { Write-Host "Build completed but executable not found at: $exePath" -ForegroundColor Red } - } else { + } + else { Write-Host "Build failed!" -ForegroundColor Red } } diff --git a/sojorn-api-http.conf b/sojorn-api-http.conf index b1a66b8..13702ef 100644 --- a/sojorn-api-http.conf +++ b/sojorn-api-http.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name api.gosojorn.com; + server_name api.sojorn.net; # Allow Certbot to validate (it uses .well-known/acme-challenge) location /.well-known/acme-challenge/ { diff --git a/sojorn-api-v2.conf b/sojorn-api-v2.conf index f94d79a..43b4eea 100644 --- a/sojorn-api-v2.conf +++ b/sojorn-api-v2.conf @@ -1,15 +1,15 @@ server { listen 80; - server_name api.gosojorn.com; - return 301 https://api.gosojorn.com$request_uri; + server_name api.sojorn.net; + return 301 https://api.sojorn.net$request_uri; } server { listen 443 ssl http2; - server_name api.gosojorn.com; + server_name api.sojorn.net; - ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; diff --git a/sojorn-api.conf b/sojorn-api.conf index c92e6c7..23aedf1 100644 --- a/sojorn-api.conf +++ b/sojorn-api.conf @@ -1,15 +1,15 @@ server { listen 80; - server_name api.gosojorn.com; - return 301 https://api.gosojorn.com$request_uri; + server_name api.sojorn.net; + return 301 https://api.sojorn.net$request_uri; } server { listen 443 ssl http2; - server_name api.gosojorn.com; + server_name api.sojorn.net; - ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; diff --git a/sojorn_app/android/app/src/main/AndroidManifest.xml b/sojorn_app/android/app/src/main/AndroidManifest.xml index c7d8a3b..ae38f06 100644 --- a/sojorn_app/android/app/src/main/AndroidManifest.xml +++ b/sojorn_app/android/app/src/main/AndroidManifest.xml @@ -43,7 +43,7 @@ - + diff --git a/sojorn_app/android/app/src/main/res/values/strings.xml b/sojorn_app/android/app/src/main/res/values/strings.xml index 653bdd5..9f5dc2e 100644 --- a/sojorn_app/android/app/src/main/res/values/strings.xml +++ b/sojorn_app/android/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - chat_messages - Chat messages + sojorn_notifications + Sojorn Notifications diff --git a/sojorn_app/lib/config/api_config.dart b/sojorn_app/lib/config/api_config.dart index 56c3347..33031c2 100644 --- a/sojorn_app/lib/config/api_config.dart +++ b/sojorn_app/lib/config/api_config.dart @@ -5,19 +5,24 @@ class ApiConfig { static String _computeBaseUrl() { final raw = const String.fromEnvironment( 'API_BASE_URL', - defaultValue: 'https://api.gosojorn.com/api/v1', + defaultValue: 'https://api.sojorn.net/api/v1', ); - // Auto-upgrade any lingering http://api.gosojorn.com:8080 (or plain http) + // Auto-upgrade any lingering http://api.sojorn.net:8080 (or plain http) // to the public https endpoint behind nginx. This protects old .env files // or cached web builds that still point at the closed port 8080. - if (raw.startsWith('http://api.gosojorn.com:8080')) { + if (raw.startsWith('http://api.sojorn.net:8080')) { return raw.replaceFirst( - 'http://api.gosojorn.com:8080', - 'https://api.gosojorn.com', + 'http://api.sojorn.net:8080', + 'https://api.sojorn.net', ); } + // Belt-and-suspenders: Force migration even if args/env are stale + if (raw.contains('gosojorn.com')) { + return raw.replaceAll('gosojorn.com', 'sojorn.net'); + } + if (raw.startsWith('http://')) { return 'https://${raw.substring('http://'.length)}'; } diff --git a/sojorn_app/lib/models/notification.dart b/sojorn_app/lib/models/notification.dart index 04b360a..e1c63d2 100644 --- a/sojorn_app/lib/models/notification.dart +++ b/sojorn_app/lib/models/notification.dart @@ -2,14 +2,19 @@ import 'profile.dart'; /// Types of notifications enum NotificationType { - appreciate, // Someone appreciated your post - chain, // Someone chained your post - follow, // Someone followed you - follow_request, // Someone requested to follow you - new_follower, // Someone followed you (public or approved) - request_accepted, // Someone accepted your follow request - comment, // Someone commented on your post - mention, // Someone mentioned you + like, // Someone liked your post + comment, // Someone commented on your post + reply, // Someone replied to your post (chained) + mention, // Someone mentioned you + follow, // Someone followed you + follow_request, // Someone requested to follow you + follow_accepted, // Someone accepted your follow request + message, // New chat message (if shown in notifications) + save, // Someone saved your post + beacon_vouch, // Someone vouched for your beacon + beacon_report, // Someone reported your beacon + share, // Someone shared your post + quip_reaction, // Someone reacted to your quip } /// Notification model @@ -115,22 +120,32 @@ class AppNotification { String get message { final actorName = actor?.displayName ?? 'Someone'; switch (type) { - case NotificationType.appreciate: - return '$actorName appreciated your post'; - case NotificationType.chain: - return '$actorName chained your post'; + case NotificationType.like: + return '$actorName liked your post'; + case NotificationType.reply: + return '$actorName replied to your post'; case NotificationType.follow: return '$actorName started following you'; case NotificationType.follow_request: return '$actorName requested to follow you'; - case NotificationType.new_follower: - return '$actorName followed you'; - case NotificationType.request_accepted: + case NotificationType.follow_accepted: return '$actorName accepted your follow request'; case NotificationType.comment: return '$actorName commented on your post'; case NotificationType.mention: return '$actorName mentioned you'; + case NotificationType.message: + return '$actorName sent you a message'; + case NotificationType.save: + return '$actorName saved your post'; + case NotificationType.beacon_vouch: + return '$actorName vouched for your beacon'; + case NotificationType.beacon_report: + return '$actorName reported your beacon'; + case NotificationType.share: + return '$actorName shared your post'; + case NotificationType.quip_reaction: + return '$actorName reacted to your quip'; } } } diff --git a/sojorn_app/lib/models/search_results.dart b/sojorn_app/lib/models/search_results.dart index d82e9ad..69fe84b 100644 --- a/sojorn_app/lib/models/search_results.dart +++ b/sojorn_app/lib/models/search_results.dart @@ -16,9 +16,9 @@ class SearchUser { factory SearchUser.fromJson(Map json) { return SearchUser( - id: json['id'] as String, - username: json['username'] as String, - displayName: json['display_name'] as String? ?? json['displayName'] as String? ?? json['username'] as String, + id: json['id'] as String? ?? '', + username: (json['username'] as String?) ?? (json['handle'] as String?) ?? 'unknown', + displayName: json['display_name'] as String? ?? json['displayName'] as String? ?? json['handle'] as String? ?? json['username'] as String? ?? 'Unknown', avatarUrl: json['avatar_url'] as String?, harmonyTier: json['harmony_tier'] as String? ?? json['harmonyTier'] as String? ?? 'new', ); @@ -81,12 +81,15 @@ class SearchPost { }); factory SearchPost.fromJson(Map json) { + // Handle both flat structure and nested author object structure + final authorJson = json['author'] as Map?; + return SearchPost( id: json['id'] as String, body: json['body'] as String, - authorId: json['author_id'] as String, - authorHandle: json['author_handle'] as String, - authorDisplayName: json['author_display_name'] as String, + authorId: json['author_id'] as String? ?? authorJson?['id'] as String? ?? '', + authorHandle: json['author_handle'] as String? ?? authorJson?['handle'] as String? ?? 'unknown', + authorDisplayName: json['author_display_name'] as String? ?? authorJson?['display_name'] as String? ?? 'Unknown', createdAt: DateTime.parse(json['created_at'] as String), ); } diff --git a/sojorn_app/lib/routes/app_routes.dart b/sojorn_app/lib/routes/app_routes.dart index a0ebe31..22b0ac6 100644 --- a/sojorn_app/lib/routes/app_routes.dart +++ b/sojorn_app/lib/routes/app_routes.dart @@ -19,6 +19,7 @@ import '../screens/profile/blocked_users_screen.dart'; import '../screens/auth/auth_gate.dart'; import '../screens/discover/discover_screen.dart'; import '../screens/secure_chat/secure_chat_full_screen.dart'; +import '../screens/post/threaded_conversation_screen.dart'; /// App routing config (GoRouter). class AppRoutes { @@ -65,6 +66,13 @@ class AppRoutes { parentNavigatorKey: rootNavigatorKey, builder: (_, __) => const SecureChatFullScreen(), ), + GoRoute( + path: '$postPrefix/:id', + parentNavigatorKey: rootNavigatorKey, + builder: (_, state) => ThreadedConversationScreen( + rootPostId: state.pathParameters['id'] ?? '', + ), + ), StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => AuthGate( authenticatedChild: HomeShell(navigationShell: navigationShell), @@ -81,16 +89,16 @@ class AppRoutes { StatefulShellBranch( routes: [ GoRoute( - path: '/discover', - builder: (_, __) => const DiscoverScreen(), + path: quips, + builder: (_, __) => const QuipsFeedScreen(), ), ], ), StatefulShellBranch( routes: [ GoRoute( - path: quips, - builder: (_, __) => const QuipsFeedScreen(), + path: '/beacon', + builder: (_, __) => const BeaconScreen(), ), ], ), @@ -169,29 +177,29 @@ class AppRoutes { } /// Get shareable URL for a user profile - /// Returns: https://gosojorn.com/u/username + /// Returns: https://sojorn.net/u/username static String getProfileUrl( String username, { - String baseUrl = 'https://gosojorn.com', + String baseUrl = 'https://sojorn.net', }) { return '$baseUrl/u/$username'; } /// Get shareable URL for a post (future implementation) - /// Returns: https://gosojorn.com/p/postid + /// Returns: https://sojorn.net/p/postid static String getPostUrl( String postId, { - String baseUrl = 'https://gosojorn.com', + String baseUrl = 'https://sojorn.net', }) { return '$baseUrl/p/$postId'; } /// Get shareable URL for a beacon location - /// Returns: https://gosojorn.com/beacon?lat=...&long=... + /// Returns: https://sojorn.net/beacon?lat=...&long=... static String getBeaconUrl( double lat, double long, { - String baseUrl = 'https://gosojorn.com', + String baseUrl = 'https://sojorn.net', }) { return '$baseUrl/beacon?lat=${lat.toStringAsFixed(6)}&long=${long.toStringAsFixed(6)}'; } diff --git a/sojorn_app/lib/screens/discover/discover_screen.dart b/sojorn_app/lib/screens/discover/discover_screen.dart index 9f7b220..3d5e72d 100644 --- a/sojorn_app/lib/screens/discover/discover_screen.dart +++ b/sojorn_app/lib/screens/discover/discover_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../../models/profile.dart'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -81,9 +82,8 @@ class _DiscoverScreenState extends ConsumerState { DiscoverData? discoverData; List recentSearches = []; int _searchEpoch = 0; - final Map> _postFutures = {}; - static const Duration debounceDuration = Duration(milliseconds: 250); + static const Duration debounceDuration = Duration(milliseconds: 300); @override void initState() { @@ -186,7 +186,9 @@ class _DiscoverScreenState extends ConsumerState { } debounceTimer = Timer(debounceDuration, () { - performSearch(query); + if (query.length >= 2) { + performSearch(query); + } }); } @@ -259,18 +261,6 @@ class _DiscoverScreenState extends ConsumerState { ); } - Future _getPostFuture(String postId) { - return _postFutures.putIfAbsent(postId, () { - final apiService = ref.read(apiServiceProvider); - return apiService.getPostById(postId); - }); - } - - void _retryPostLoad(String postId) { - _postFutures.remove(postId); - if (mounted) setState(() {}); - } - void _openPostDetail(Post post) { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( @@ -668,76 +658,35 @@ class _DiscoverScreenState extends ConsumerState { } Widget _buildPostResultItem(SearchPost post) { - return FutureBuilder( - future: _getPostFuture(post.id), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.cardSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)), - ), - child: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: AppTheme.royalPurple, - ), - ), - const SizedBox(width: 12), - Text('Loading post...', style: AppTheme.bodyMedium), - ], - ), - ); - } + // Convert SearchPost to minimal Post immediately + final minimalPost = Post( + id: post.id, + body: post.body, + authorId: post.authorId, + createdAt: post.createdAt, + status: PostStatus.active, + detectedTone: ToneLabel.neutral, + contentIntegrityScore: 0.0, + author: Profile( + id: post.authorId, + handle: post.authorHandle, + displayName: post.authorDisplayName, + createdAt: DateTime.now(), + avatarUrl: null, + ), + isLiked: false, + likeCount: 0, + commentCount: 0, + tags: [], + ); - if (snapshot.hasError || !snapshot.hasData) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.cardSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)), - ), - child: Row( - children: [ - Icon(Icons.error_outline, - color: AppTheme.egyptianBlue.withOpacity(0.6)), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Unable to load post', - style: AppTheme.bodyMedium, - ), - ), - TextButton( - onPressed: () => _retryPostLoad(post.id), - child: Text('Retry', - style: AppTheme.labelMedium - .copyWith(color: AppTheme.royalPurple)), - ), - ], - ), - ); - } - - final fullPost = snapshot.data!; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: sojornPostCard( - post: fullPost, - onTap: () => _openPostDetail(fullPost), - onChain: () => _openChainComposer(fullPost), - ), - ); - }, + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: sojornPostCard( + post: minimalPost, + onTap: () => _openPostDetail(minimalPost), + onChain: () => _openChainComposer(minimalPost), + ), ); } } diff --git a/sojorn_app/lib/screens/home/home_shell.dart b/sojorn_app/lib/screens/home/home_shell.dart index 101d9a2..f52a778 100644 --- a/sojorn_app/lib/screens/home/home_shell.dart +++ b/sojorn_app/lib/screens/home/home_shell.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:go_router/go_router.dart'; +import '../../services/notification_service.dart'; import '../../services/secure_chat_service.dart'; import '../../theme/app_theme.dart'; import '../notifications/notifications_screen.dart'; @@ -28,18 +31,29 @@ class HomeShell extends StatefulWidget { class _HomeShellState extends State with WidgetsBindingObserver { bool _isRadialMenuVisible = false; final SecureChatService _chatService = SecureChatService(); + StreamSubscription? _notifSub; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _chatService.startBackgroundSync(); + _initNotificationListener(); + } + + void _initNotificationListener() { + _notifSub = NotificationService.instance.foregroundMessages.listen((message) { + if (mounted) { + NotificationService.instance.showNotificationBanner(context, message); + } + }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _chatService.stopBackgroundSync(); + _notifSub?.cancel(); super.dispose(); } @@ -171,17 +185,17 @@ class _HomeShellState extends State with WidgetsBindingObserver { label: 'Home', ), _buildNavBarItem( - icon: Icons.explore_outlined, - activeIcon: Icons.explore, + icon: Icons.play_circle_outline, + activeIcon: Icons.play_circle, index: 1, - label: 'Discover', + label: 'Quips', ), const SizedBox(width: 48), _buildNavBarItem( - icon: Icons.play_circle_outline, - activeIcon: Icons.play_circle, + icon: Icons.sensors_outlined, + activeIcon: Icons.sensors, index: 2, - label: 'Quips', + label: 'Beacon', ), _buildNavBarItem( icon: Icons.person_outline, @@ -222,7 +236,11 @@ class _HomeShellState extends State with WidgetsBindingObserver { icon: Icon(Icons.search, color: AppTheme.navyBlue), tooltip: 'Discover', onPressed: () { - widget.navigationShell.goBranch(1); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const DiscoverScreen(), + ), + ); }, ), IconButton( diff --git a/sojorn_app/lib/screens/notifications/notifications_screen.dart b/sojorn_app/lib/screens/notifications/notifications_screen.dart index ac29af1..15a4b18 100644 --- a/sojorn_app/lib/screens/notifications/notifications_screen.dart +++ b/sojorn_app/lib/screens/notifications/notifications_screen.dart @@ -243,8 +243,7 @@ class _NotificationsScreenState extends ConsumerState { // Navigate based on notification type switch (notification.type) { case NotificationType.follow: - case NotificationType.new_follower: - case NotificationType.request_accepted: + case NotificationType.follow_accepted: // Navigate to the follower's profile if (notification.actor != null) { Navigator.of(context).push( @@ -255,9 +254,9 @@ class _NotificationsScreenState extends ConsumerState { } break; - case NotificationType.appreciate: + case NotificationType.like: case NotificationType.comment: - case NotificationType.chain: + case NotificationType.reply: case NotificationType.mention: // Fetch the post and navigate to post detail if (notification.postId != null) { @@ -284,8 +283,18 @@ class _NotificationsScreenState extends ConsumerState { } } break; + case NotificationType.message: + // For messages, navigate to chat screen + if (notification.metadata?['conversation_id'] != null) { + context.push('/secure-chat/${notification.metadata!['conversation_id']}'); + } else { + context.push('/secure-chat'); + } + break; case NotificationType.follow_request: break; + default: + break; } } @@ -492,20 +501,19 @@ class _NotificationItem extends StatelessWidget { Color iconColor; switch (notification.type) { - case NotificationType.appreciate: + case NotificationType.like: iconData = Icons.favorite; iconColor = AppTheme.brightNavy; break; - case NotificationType.chain: + case NotificationType.reply: iconData = Icons.subdirectory_arrow_right; iconColor = AppTheme.royalPurple; break; case NotificationType.follow: - case NotificationType.new_follower: iconData = Icons.person_add; iconColor = AppTheme.ksuPurple; break; - case NotificationType.request_accepted: + case NotificationType.follow_accepted: iconData = Icons.check_circle; iconColor = AppTheme.brightNavy; break; @@ -521,6 +529,18 @@ class _NotificationItem extends StatelessWidget { iconData = Icons.person_add; iconColor = AppTheme.ksuPurple; break; + case NotificationType.message: + iconData = Icons.message; + iconColor = AppTheme.egyptianBlue; + break; + case NotificationType.save: + iconData = Icons.bookmark; + iconColor = AppTheme.ksuPurple; + break; + default: + iconData = Icons.notifications; + iconColor = AppTheme.egyptianBlue; + break; } return Container( diff --git a/sojorn_app/lib/screens/post/threaded_conversation_screen.dart b/sojorn_app/lib/screens/post/threaded_conversation_screen.dart index 90678a1..d800048 100644 --- a/sojorn_app/lib/screens/post/threaded_conversation_screen.dart +++ b/sojorn_app/lib/screens/post/threaded_conversation_screen.dart @@ -13,6 +13,9 @@ import '../../theme/app_theme.dart'; import '../../widgets/post/interactive_reply_block.dart'; import '../../widgets/media/signed_media_image.dart'; import '../compose/compose_screen.dart'; +import '../discover/discover_screen.dart'; +import '../secure_chat/secure_chat_full_screen.dart'; +import 'package:share_plus/share_plus.dart'; class ThreadedConversationScreen extends ConsumerStatefulWidget { final String rootPostId; @@ -176,7 +179,31 @@ class _ThreadedConversationScreenState extends ConsumerState context.go(AppRoutes.homeAlias), + icon: Icon(Icons.home_outlined, color: AppTheme.navyBlue), + ), + IconButton( + onPressed: () => Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (_) => const DiscoverScreen(), + fullscreenDialog: true, + ), + ), + icon: Icon(Icons.search, color: AppTheme.navyBlue), + ), + IconButton( + onPressed: () => Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (_) => SecureChatFullScreen(), + fullscreenDialog: true, + ), + ), + icon: Icon(Icons.chat_bubble_outline, color: AppTheme.navyBlue), + ), + const SizedBox(width: 8), + ], ); } @@ -583,9 +610,12 @@ class _ThreadedConversationScreenState extends ConsumerState _toggleLike(post), + onPressed: () => _sharePost(post), icon: Icon( - isLiked ? Icons.favorite : Icons.favorite_border, - color: isLiked ? Colors.red : AppTheme.textSecondary, + Icons.share_outlined, + color: AppTheme.textSecondary, ), style: IconButton.styleFrom( - backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08), + backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.05), + minimumSize: const Size(44, 44), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), @@ -614,7 +645,8 @@ class _ThreadedConversationScreenState extends ConsumerState Map.from(post.reactions!), ); - print('DEBUG: Seeded reaction counts: ${_reactionCountsByPost[post.id]}'); } if (post.myReactions != null) { _myReactionsByPost.putIfAbsent(post.id, () => post.myReactions!.toSet()); - print('DEBUG: Seeded my reactions: ${_myReactionsByPost[post.id]}'); } if (post.reactionUsers != null) { _reactionUsersByPost.putIfAbsent( @@ -1020,19 +1046,12 @@ class _ThreadedConversationScreenState extends ConsumerState _reactionCountsFor(Post post) { - // Debug: Check what we're getting from the post model - print('DEBUG: _reactionCountsFor for post ${post.id}'); - print('DEBUG: post.reactions = ${post.reactions}'); - print('DEBUG: _reactionCountsByPost[${post.id}] = ${_reactionCountsByPost[post.id]}'); - // Prefer local state for immediate updates after toggle reactions final localState = _reactionCountsByPost[post.id]; if (localState != null) { - print('DEBUG: Using local state: ${localState}'); return localState; } // Fall back to post model if no local state - print('DEBUG: Using post.reactions: ${post.reactions}'); return post.reactions ?? {}; } @@ -1063,22 +1082,16 @@ class _ThreadedConversationScreenState extends ConsumerState?; final updatedMine = response['my_reactions'] as List?; - print('DEBUG: Toggle reaction response: $response'); - print('DEBUG: updatedCounts: $updatedCounts'); - print('DEBUG: updatedMine: $updatedMine'); - if (updatedCounts != null) { setState(() { _reactionCountsByPost[postId] = updatedCounts .map((key, value) => MapEntry(key, value as int)); - print('DEBUG: Updated local reaction counts: ${_reactionCountsByPost[postId]}'); }); } if (updatedMine != null) { setState(() { _myReactionsByPost[postId] = updatedMine.map((item) => item.toString()).toSet(); - print('DEBUG: Updated local my reactions: ${_myReactionsByPost[postId]}'); }); } } catch (_) { @@ -1161,4 +1174,19 @@ class _ThreadedConversationScreenState extends ConsumerState _sharePost(Post post) async { + final handle = post.author?.handle ?? 'sojorn'; + final text = '${post.body}\n\n— @$handle on sojorn'; + + try { + await Share.share(text); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Unable to share right now.')), + ); + } + } + } } diff --git a/sojorn_app/lib/screens/profile/profile_screen.dart b/sojorn_app/lib/screens/profile/profile_screen.dart index a737f11..d97f24b 100644 --- a/sojorn_app/lib/screens/profile/profile_screen.dart +++ b/sojorn_app/lib/screens/profile/profile_screen.dart @@ -29,7 +29,7 @@ class ProfileScreen extends ConsumerStatefulWidget { String _resolveAvatar(String? url) { if (url == null || url.isEmpty) return ''; if (url.startsWith('http://') || url.startsWith('https://')) return url; - return 'https://img.gosojorn.com/${url.replaceFirst(RegExp('^/'), '')}'; + return 'https://img.sojorn.net/${url.replaceFirst(RegExp('^/'), '')}'; } class _ProfileScreenState extends ConsumerState diff --git a/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart b/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart index 1ce6c85..08f9cba 100644 --- a/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart +++ b/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart @@ -90,7 +90,7 @@ class _QuipsFeedScreenState extends ConsumerState bool _isUserPaused = false; int _lastRefreshToken = 0; - static const int _branchIndex = 2; + static const int _branchIndex = 1; static const int _pageSize = 8; @override diff --git a/sojorn_app/lib/screens/search/search_screen.dart b/sojorn_app/lib/screens/search/search_screen.dart index 1f1f8e9..262b2a0 100644 --- a/sojorn_app/lib/screens/search/search_screen.dart +++ b/sojorn_app/lib/screens/search/search_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../../models/profile.dart'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -31,14 +32,12 @@ class _SearchScreenState extends ConsumerState { SearchResults? results; List recentSearches = []; int _searchEpoch = 0; - - final Map> _postFutures = {}; // Discovery State bool _isDiscoveryLoading = false; List _discoveryPosts = []; - static const Duration debounceDuration = Duration(milliseconds: 250); + static const Duration debounceDuration = Duration(milliseconds: 300); static const List trendingTags = [ 'safety', 'wellness', @@ -155,7 +154,9 @@ class _SearchScreenState extends ConsumerState { } debounceTimer = Timer(debounceDuration, () { - performSearch(query); + if (query.length >= 2) { + performSearch(query); + } }); } @@ -170,9 +171,15 @@ class _SearchScreenState extends ConsumerState { }); try { + print('[SearchScreen] Requesting search for: "$normalizedQuery"'); final apiService = ref.read(apiServiceProvider); final searchResults = await apiService.search(normalizedQuery); - if (!mounted || requestId != _searchEpoch) return; + if (!mounted || requestId != _searchEpoch) { + print('[SearchScreen] Request $requestId discarded (stale)'); + return; + } + + print('[SearchScreen] Results received. Users: ${searchResults.users.length}, Tags: ${searchResults.tags.length}, Posts: ${searchResults.posts.length}'); if (searchResults.users.isNotEmpty) { await saveRecentSearch(RecentSearch( @@ -196,6 +203,7 @@ class _SearchScreenState extends ConsumerState { isLoading = false; }); } catch (e) { + print('[SearchScreen] Search error: $e'); if (!mounted || requestId != _searchEpoch) return; setState(() { isLoading = false; @@ -214,18 +222,6 @@ class _SearchScreenState extends ConsumerState { focusNode.requestFocus(); } - Future _getPostFuture(String postId) { - return _postFutures.putIfAbsent(postId, () { - final apiService = ref.read(apiServiceProvider); - return apiService.getPostById(postId); - }); - } - - void _retryPostLoad(String postId) { - _postFutures.remove(postId); - if (mounted) setState(() {}); - } - void _openPostDetail(Post post) { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( @@ -588,85 +584,49 @@ class _SearchScreenState extends ConsumerState { } Widget buildPostResultItem(SearchPost post) { - return FutureBuilder( - future: _getPostFuture(post.id), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.cardSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)), - ), - child: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: AppTheme.royalPurple, - ), - ), - const SizedBox(width: 12), - Text('Loading post...', style: AppTheme.bodyMedium), - ], - ), - ); - } - - if (snapshot.hasError || !snapshot.hasData) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.cardSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)), - ), - child: Row( - children: [ - Icon(Icons.error_outline, - color: AppTheme.egyptianBlue.withOpacity(0.6)), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Unable to load post', - style: AppTheme.bodyMedium, - ), - ), - TextButton( - onPressed: () => _retryPostLoad(post.id), - child: Text('Retry', - style: - AppTheme.labelMedium.copyWith(color: AppTheme.royalPurple)), - ), - ], - ), - ); - } - - final fullPost = snapshot.data!; - return ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: AppTheme.cardSurface, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)), - ), - child: sojornPostCard( - post: fullPost, - onTap: () => _openPostDetail(fullPost), - onChain: () => _openChainComposer(fullPost), - ), - ), - ); - }, + // Convert SearchPost to minimal Post immediately + final minimalPost = Post( + id: post.id, + body: post.body, + authorId: post.authorId, + createdAt: post.createdAt, + + // REQUIRED fields missing previously + status: PostStatus.active, + detectedTone: ToneLabel.neutral, + contentIntegrityScore: 0.0, + + author: Profile( + id: post.authorId, + handle: post.authorHandle, + displayName: post.authorDisplayName, + createdAt: DateTime.now(), + avatarUrl: null, + ), + // Set defaults for rest + isLiked: false, + likeCount: 0, + commentCount: 0, + tags: [], ); + + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: AppTheme.cardSurface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)), + ), + child: sojornPostCard( + post: minimalPost, + onTap: () => _openPostDetail(minimalPost), + onChain: () => _openChainComposer(minimalPost), + // showActions removed (not supported) + ), + ), + ); } } diff --git a/sojorn_app/lib/services/api_service.dart b/sojorn_app/lib/services/api_service.dart index b297484..f8b960a 100644 --- a/sojorn_app/lib/services/api_service.dart +++ b/sojorn_app/lib/services/api_service.dart @@ -943,17 +943,25 @@ class ApiService { final sanitizedQuery = SecurityUtils.limitText(SecurityUtils.sanitizeText(query), maxLength: 100); if (!SecurityUtils.isValidInput(sanitizedQuery)) { + if (kDebugMode) print('[API] Invalid search query input: $query'); return SearchResults(users: [], tags: [], posts: []); } try { + if (kDebugMode) print('[API] Searching for: $sanitizedQuery'); final data = await callGoApi( '/search', method: 'GET', queryParams: {'q': sanitizedQuery}, ); + // if (kDebugMode) print('[API] Search raw response: ${jsonEncode(data)}'); return SearchResults.fromJson(data); - } catch (_) { + } catch (e, stack) { + if (kDebugMode) { + print('[API] Search failed for query: "$query"'); + print('Error: $e'); + print('Stack: $stack'); + } // Return empty results on error return SearchResults(users: [], tags: [], posts: []); } diff --git a/sojorn_app/lib/services/image_upload_service.dart b/sojorn_app/lib/services/image_upload_service.dart index acafad5..3f11e57 100644 --- a/sojorn_app/lib/services/image_upload_service.dart +++ b/sojorn_app/lib/services/image_upload_service.dart @@ -505,13 +505,13 @@ class ImageUploadService { // Fix Image URLs if (url.contains('/sojorn-media/')) { final key = url.split('/sojorn-media/').last; - return 'https://img.gosojorn.com/$key'; + return 'https://img.sojorn.net/$key'; } // Fix Video URLs if (url.contains('/sojorn-videos/')) { final key = url.split('/sojorn-videos/').last; - return 'https://quips.gosojorn.com/$key'; + return 'https://quips.sojorn.net/$key'; } return url; diff --git a/sojorn_app/lib/services/notification_service.dart b/sojorn_app/lib/services/notification_service.dart index 61da650..05b380a 100644 --- a/sojorn_app/lib/services/notification_service.dart +++ b/sojorn_app/lib/services/notification_service.dart @@ -531,7 +531,7 @@ class NotificationService { case 'thread_view': case 'main_feed': default: - navigator.context.go(AppRoutes.home); + navigator.context.push('${AppRoutes.postPrefix}/$postId'); break; } } diff --git a/sojorn_app/lib/utils/link_handler.dart b/sojorn_app/lib/utils/link_handler.dart index fb158f5..26b9e14 100644 --- a/sojorn_app/lib/utils/link_handler.dart +++ b/sojorn_app/lib/utils/link_handler.dart @@ -28,7 +28,7 @@ class LinkHandler { Uri? uri = Uri.tryParse(url.replaceFirst('sojorn://', 'sojorn://')); // Normalize to https for query parsing if needed uri ??= - Uri.tryParse(url.replaceFirst('sojorn://', 'https://gosojorn.com/')); + Uri.tryParse(url.replaceFirst('sojorn://', 'https://sojorn.net/')); final latParam = uri?.queryParameters['lat']; final longParam = uri?.queryParameters['long']; diff --git a/sojorn_app/lib/utils/url_launcher_helper.dart b/sojorn_app/lib/utils/url_launcher_helper.dart index 5f25fd9..fabb03c 100644 --- a/sojorn_app/lib/utils/url_launcher_helper.dart +++ b/sojorn_app/lib/utils/url_launcher_helper.dart @@ -7,7 +7,7 @@ class UrlLauncherHelper { // List of known safe domains static const List _safeDomains = [ 'mp.ls', 'www.mp.ls', 'patrick.mp.ls' - 'gosojorn.com', 'www.gosojorn.com' + 'sojorn.net', 'www.sojorn.net' 'youtube.com', 'www.youtube.com', 'youtu.be', 'instagram.com', 'www.instagram.com', 'twitter.com', 'www.twitter.com', 'x.com', 'www.x.com', diff --git a/sojorn_app/lib/widgets/media/signed_media_image.dart b/sojorn_app/lib/widgets/media/signed_media_image.dart index c8becda..0b1a9ad 100644 --- a/sojorn_app/lib/widgets/media/signed_media_image.dart +++ b/sojorn_app/lib/widgets/media/signed_media_image.dart @@ -70,12 +70,12 @@ class _SignedMediaImageState extends ConsumerState { final host = uri.host.toLowerCase(); // Custom domain URLs are public and directly accessible - no signing needed - if (host == 'img.gosojorn.com' || host == 'quips.gosojorn.com') { + if (host == 'img.sojorn.net' || host == 'quips.sojorn.net') { return false; } - // Legacy: media.gosojorn.com might need signing depending on setup - if (host == 'media.gosojorn.com') { + // Legacy: media.sojorn.net might need signing depending on setup + if (host == 'media.sojorn.net') { return true; } diff --git a/sojorn_app/lib/widgets/post/post_actions.dart b/sojorn_app/lib/widgets/post/post_actions.dart index 95b3fcb..f5f36b4 100644 --- a/sojorn_app/lib/widgets/post/post_actions.dart +++ b/sojorn_app/lib/widgets/post/post_actions.dart @@ -278,11 +278,13 @@ class _PostActionsState extends ConsumerState { if (allowChain) ElevatedButton.icon( onPressed: widget.onChain, - icon: const Icon(Icons.reply, size: 18), - label: const Text('Reply'), + icon: Icon(Icons.reply, size: 18, color: AppTheme.navyBlue), + label: Text('Reply', style: TextStyle(color: AppTheme.navyBlue, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.brightNavy, - foregroundColor: Colors.white, + backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.05), + foregroundColor: AppTheme.navyBlue, + elevation: 0, + shadowColor: Colors.transparent, minimumSize: const Size(0, 44), padding: const EdgeInsets.symmetric(horizontal: 16), shape: RoundedRectangleBorder( diff --git a/sojorn_app/lib/widgets/post/post_header.dart b/sojorn_app/lib/widgets/post/post_header.dart index 0a3efe9..a36c004 100644 --- a/sojorn_app/lib/widgets/post/post_header.dart +++ b/sojorn_app/lib/widgets/post/post_header.dart @@ -14,7 +14,7 @@ import '../../routes/app_routes.dart'; String _resolveAvatarUrl(String? url) { if (url == null || url.isEmpty) return ''; if (url.startsWith('http://') || url.startsWith('https://')) return url; - return 'https://img.gosojorn.com/${url.replaceFirst(RegExp('^/'), '')}'; + return 'https://img.sojorn.net/${url.replaceFirst(RegExp('^/'), '')}'; } /// Post header with author info and timestamp. diff --git a/sojorn_app/lib/widgets/reactions/reactions_display.dart b/sojorn_app/lib/widgets/reactions/reactions_display.dart index bfc4080..8a5a88f 100644 --- a/sojorn_app/lib/widgets/reactions/reactions_display.dart +++ b/sojorn_app/lib/widgets/reactions/reactions_display.dart @@ -54,11 +54,11 @@ class ReactionsDisplay extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (onAddReaction != null) ...[ - _ReactionAddButton(onTap: onAddReaction!), - if (reactionCounts.isNotEmpty) const SizedBox(width: 8), - ], if (reactionCounts.isNotEmpty) _buildTopReactionChip(), + if (onAddReaction != null) ...[ + if (reactionCounts.isNotEmpty) const SizedBox(width: 8), + _ReactionAddButton(onTap: onAddReaction!), + ], ], ); } @@ -95,6 +95,8 @@ class ReactionsDisplay extends StatelessWidget { runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ + if (onAddReaction != null) + _ReactionAddButton(onTap: onAddReaction!), ...sortedEntries.map((entry) { return _ReactionChip( reactionId: entry.key, @@ -105,8 +107,6 @@ class ReactionsDisplay extends StatelessWidget { onLongPress: onAddReaction, ); }), - if (onAddReaction != null) - _ReactionAddButton(onTap: onAddReaction!), ], ), ); diff --git a/sojorn_app/run_chrome.bat b/sojorn_app/run_chrome.bat index a64b6be..ec96a6d 100644 --- a/sojorn_app/run_chrome.bat +++ b/sojorn_app/run_chrome.bat @@ -5,4 +5,4 @@ echo Starting Sojorn on Chrome... echo. flutter run -d chrome ^ - --dart-define=API_BASE_URL=https://api.gosojorn.com/api/v1 + --dart-define=API_BASE_URL=https://api.sojorn.net/api/v1 diff --git a/sojorn_app/run_dev.bat b/sojorn_app/run_dev.bat index 9659e5b..edb35e4 100644 --- a/sojorn_app/run_dev.bat +++ b/sojorn_app/run_dev.bat @@ -5,4 +5,4 @@ echo Starting Sojorn in development mode... echo. flutter run ^ - --dart-define=API_BASE_URL=https://api.gosojorn.com/api/v1 + --dart-define=API_BASE_URL=https://api.sojorn.net/api/v1 diff --git a/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md b/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md index 0b215d1..8e1962b 100644 --- a/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md +++ b/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md @@ -197,7 +197,7 @@ for _, origin := range allowedOrigins { ### Zero Downtime Approach 1. **Parallel Run**: Both Supabase and Go VPS running simultaneously -2. **DNS Update**: Point `api.gosojorn.com` to new VPS IP +2. **DNS Update**: Point `api.sojorn.net` to new VPS IP 3. **TTL Management**: Set DNS TTL to 300s before cutover 4. **Monitoring**: Real-time log monitoring for errors @@ -212,7 +212,7 @@ for _, origin := range allowedOrigins { **DNS Switch:** ```bash -# Update A record for api.gosojorn.com +# Update A record for api.sojorn.net # Monitor propagation # Watch error rates ``` @@ -223,7 +223,7 @@ for _, origin := range allowedOrigins { journalctl -u sojorn-api -f # Check error rates -curl -s https://api.gosojorn.com/health +curl -s https://api.sojorn.net/health # Validate data integrity sudo -u postgres psql sojorn -c "SELECT COUNT(*) FROM users;" @@ -329,7 +329,7 @@ sudo -u postgres psql sojorn -c "SELECT COUNT(*) FROM users;" ### Emergency Rollback Procedure -1. **DNS Reversion**: Point `api.gosojorn.com` back to Supabase +1. **DNS Reversion**: Point `api.sojorn.net` back to Supabase 2. **Data Sync**: Restore any new data from Go backend to Supabase 3. **Service Restart**: Restart Supabase Edge Functions 4. **Client Update**: Update Flutter app configuration if needed diff --git a/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md b/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md index 2565377..8f23a40 100644 --- a/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md +++ b/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md @@ -4,6 +4,8 @@ This guide consolidates all deployment, operations, and maintenance documentation for the Sojorn platform, covering infrastructure setup, deployment procedures, monitoring, and operational best practices. +All code updates need to be made locally, synced to git, pulled to the server and deployed. Do not directly edit files on the server. + --- ## Infrastructure Setup @@ -136,19 +138,19 @@ FIREBASE_CREDENTIALS_FILE=/opt/sojorn/firebase-service-account.json FIREBASE_WEB_VAPID_KEY=BNxS7_your_vapid_key_here # Storage -R2_ACCOUNT_ID=your-r2-account-id -R2_ACCESS_KEY_ID=your-access-key -R2_SECRET_ACCESS_KEY=your-secret-key -R2_BUCKET_NAME=sojorn-uploads -R2_PUBLIC_BASE_URL=https://pub-xxxxx.r2.dev -R2_IMG_DOMAIN=img.sojorn.com -R2_VID_DOMAIN=vid.sojorn.com +SENDER_API_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiNT> +R2_ACCOUNT_ID=7041ca6e0f40307190dc2e65e2fb5e0f +R2_PUBLIC_BASE_URL=http://api.sojorn.net:8080/uploads +R2_IMG_DOMAIN=img.sojorn.net +R2_VID_DOMAIN=quips.sojorn.net +R2_API_TOKEN=oR7Vk0Realtx0D6SAGMuYA8pXopSoCYKv8t3JEuk # Email (Optional) -SMTP_HOST=smtp.gmail.com +# Email / SendPulse +SMTP_HOST=smtp-pulse.com SMTP_PORT=587 -SMTP_USER=noreply@sojorn.com -SMTP_PASS=your-app-password +SMTP_USER=patrickbritton3@gmail.com +SMTP_PASS=8s4jQBnAFTCXPNM # Logging LOG_LEVEL=info @@ -245,7 +247,7 @@ Create `/etc/nginx/sites-available/sojorn-api`: ```nginx server { listen 80; - server_name api.gosojorn.com; + server_name api.sojorn.net; # Redirect to HTTPS return 301 https://$server_name$request_uri; @@ -253,11 +255,11 @@ server { server { listen 443 ssl http2; - server_name api.gosojorn.com; + server_name api.sojorn.net; # SSL Configuration - ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; @@ -346,7 +348,7 @@ sudo systemctl reload nginx sudo apt install certbot python3-certbot-nginx # Obtain certificate -sudo certbot --nginx -d api.gosojorn.com +sudo certbot --nginx -d api.sojorn.net # Test renewal sudo certbot renew --dry-run @@ -556,7 +558,7 @@ psql -h localhost -U sojorn_user -d sojorn sudo systemctl start sojorn-api # Verify recovery -curl -f https://api.gosojorn.com/health +curl -f https://api.sojorn.net/health ``` #### File Recovery @@ -939,7 +941,7 @@ sudo systemctl reload nginx #### Problem: SSL handshake failed ```bash # Test SSL configuration -openssl s_client -connect api.gosojorn.com:443 +openssl s_client -connect api.sojorn.net:443 # Check Nginx configuration sudo nginx -t @@ -1003,7 +1005,7 @@ sudo systemctl reload nginx sudo systemctl restart postgresql # Verify recovery - curl -f https://api.gosojorn.com/health + curl -f https://api.sojorn.net/health ``` 3. **Communication** diff --git a/sojorn_docs/MIGRATION_STEP_BY_STEP.txt b/sojorn_docs/MIGRATION_STEP_BY_STEP.txt new file mode 100644 index 0000000..5232f9d --- /dev/null +++ b/sojorn_docs/MIGRATION_STEP_BY_STEP.txt @@ -0,0 +1,95 @@ +MIGRATION COMMANDS CHEAT SHEET +============================ + +STEP 0: EDIT LOCAL SYNC GLOBALLY + +The first directive to to edit locally here, keep git updated fequently, then +grab the updates files to our VPS server and then built and restart. + +------------ + +STEP 1: FLUTTER WEB DEPLOY (If applicable) +------------------------------------------ +Ensure your flutter web build is updated and copied to /var/www/sojorn +flutter build web --release +scp -r build/web/* user@server:/var/www/sojorn/ + +STEP 2: NGINX CONFIGURATION +--------------------------- +# 1. SSH into your VPS +ssh ... + +# 2. Copy the new configs (upload them first or copy-paste) +# Assuming you uploaded sojorn_net.conf and legacy_redirect.conf to /tmp/ + +sudo cp /tmp/sojorn_net.conf /etc/nginx/sites-available/sojorn_net.conf +sudo cp /tmp/legacy_redirect.conf /etc/nginx/sites-available/legacy_redirect.conf + +# 3. Enable new sites +sudo ln -s /etc/nginx/sites-available/sojorn_net.conf /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/legacy_redirect.conf /etc/nginx/sites-enabled/ + +# 4. Disable old site (to avoid conflicts with the new legacy_redirect which claims the same domains) +# Check existing enabled sites +ls -l /etc/nginx/sites-enabled/ +# Remove the old link (e.g., sojorn.conf or default) +sudo rm /etc/nginx/sites-enabled/sojorn.conf +# (Don't delete the actual file in sites-available, just the symlink) + +# 5. Test Configuration (This might fail on SSL paths if legacy certs are missing, but they should be there) +sudo nginx -t + +# 6. Reload Nginx (Users will briefly see unencrypted or default page for new domain until Certbot runs) +sudo systemctl reload nginx + +STEP 3: SSL CERTIFICATES (CERTBOT) +---------------------------------- +# Generate fresh certs for the NEW domain. +# --nginx plugin will automatically edit sojorn_net.conf to add SSL lines. + +sudo certbot --nginx -d sojorn.net -d www.sojorn.net -d api.sojorn.net + +# Follow the prompts. When asked about redirecting HTTP to HTTPS, choose "2: Redirect". + +STEP 4: BACKEND & ENV +--------------------- +# Update your .env file on the server +nano /opt/sojorn/.env +# CHANGE: +# CORS_ORIGINS=https://sojorn.net,https://api.sojorn.net,https://www.sojorn.net +# R2_PUBLIC_BASE_URL=https://img.sojorn.net + +# Restart the backend service +sudo systemctl restart sojorn-api + +STEP 5: VERIFICATION +-------------------- +1. Visit https://sojorn.net -> Should show app. +2. Visit https://sojorn.net -> Should redirect to https://sojorn.net. +3. Check API: https://api.sojorn.net/api/v1/health (or similar). + +STEP 6: EXTERNAL SERVICES CHECKLIST +----------------------------------- +These items fall outside the codebase but are CRITICAL for the migration: + +1. CLOUDFLARE R2 (CORS) + - Go to Cloudflare Dashboard > R2 > [Your Bucket] > Settings > CORS Policy. + - Update allowed origins to include: + [ "https://sojorn.net", "https://www.sojorn.net", "http://localhost:*" ] + - If you don't do this, web image uploads will fail. + +2. FIREBASE CONSOLE (Auth & Messaging) + - Go to Firebase Console > Authentication > Settings > Authorized Domains. + - ADD: sojorn.net + - ADD: api.sojorn.net + - You can remove gosojorn.com later. + +3. GOOGLE CLOUD CONSOLE (If using Google Sign-In) + - APIs & Services > Credentials > OAuth 2.0 Client IDs. + - Add "https://sojorn.net" to Authorized JavaScript origins. + - Add "https://sojorn.net/auth.html" (or callback URI) to Authorized redirect URIs. + +4. APPLE DEVELOPER PORTAL (If using Sign in with Apple) + - Certificates, Identifiers & Profiles > Service IDs. + - Update the "Domains and Subdomains" list for your Service ID to include sojorn.net. + diff --git a/sojorn_docs/SOJORN_ARCHITECTURE.md b/sojorn_docs/SOJORN_ARCHITECTURE.md index d544297..7b1dd6f 100644 --- a/sojorn_docs/SOJORN_ARCHITECTURE.md +++ b/sojorn_docs/SOJORN_ARCHITECTURE.md @@ -91,7 +91,7 @@ flutter run -d ### API Configuration The app connects to the production API at: ``` -https://api.gosojorn.com (or http://194.238.28.122:8080) +https://api.sojorn.net (or http://194.238.28.122:8080) ``` Configuration is in: `lib/config/api_config.dart` @@ -140,7 +140,7 @@ R2_SECRET_KEY=... | `internal/handlers/user_handler.go` | Profile & social endpoints | | `internal/repository/post_repository.go` | Post database queries | | `internal/repository/user_repository.go` | User database queries | - +aaa --- ## Server Deployment diff --git a/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md b/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md index 57a2a69..4e5f457 100644 --- a/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md +++ b/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md @@ -50,7 +50,7 @@ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) **Verification**: ```bash # Test JWT validation -curl -H "Authorization: Bearer " https://api.gosojorn.com/health +curl -H "Authorization: Bearer " https://api.sojorn.net/health ``` --- @@ -513,7 +513,7 @@ sudo certbot certificates sudo nginx -t # Check certificate expiry -openssl x509 -in /etc/letsencrypt/live/api.gosojorn.com/cert.pem -text -noout | grep "Not After" +openssl x509 -in /etc/letsencrypt/live/api.sojorn.net/cert.pem -text -noout | grep "Not After" ``` **Solutions**: @@ -527,8 +527,8 @@ sudo systemctl reload nginx #### 2. Fix Nginx SSL Config ```nginx -ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; -ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; +ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem; ``` ### DNS Propagation Issues @@ -541,11 +541,11 @@ ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; **Diagnostics**: ```bash # Check DNS resolution -nslookup api.gosojorn.com -dig api.gosojorn.com +nslookup api.sojorn.net +dig api.sojorn.net # Check propagation -for i in {1..10}; do echo "Attempt $i:"; dig api.gosojorn.com +short; sleep 30; done +for i in {1..10}; do echo "Attempt $i:"; dig api.sojorn.net +short; sleep 30; done ``` **Solutions**: @@ -553,11 +553,11 @@ for i in {1..10}; do echo "Attempt $i:"; dig api.gosojorn.com +short; sleep 30; #### 1. Verify DNS Records ```bash # Check A record -dig api.gosojorn.com A +dig api.sojorn.net A # Check with multiple DNS servers -dig @8.8.8.8 api.gosojorn.com -dig @1.1.1.1 api.gosojorn.com +dig @8.8.8.8 api.sojorn.net +dig @1.1.1.1 api.sojorn.net ``` #### 2. Reduce TTL Before Changes @@ -581,7 +581,7 @@ sudo -u postgres psql -c "SELECT count(*) FROM users;" # Network sudo netstat -tlnp | grep :8080 -curl -I https://api.gosojorn.com/health +curl -I https://api.sojorn.net/health # Logs sudo tail -f /var/log/nginx/access.log @@ -635,7 +635,7 @@ sudo journalctl -u sojorn-api --since "1 hour ago" | grep -i error 3. **Verify Health**: ```bash - curl https://api.gosojorn.com/health + curl https://api.sojorn.net/health ``` ### Database Recovery diff --git a/sojorn_docs/deployment/VPS_SETUP_GUIDE.md b/sojorn_docs/deployment/VPS_SETUP_GUIDE.md index 29da0b0..0b62552 100644 --- a/sojorn_docs/deployment/VPS_SETUP_GUIDE.md +++ b/sojorn_docs/deployment/VPS_SETUP_GUIDE.md @@ -10,7 +10,7 @@ Complete guide to deploy Sojorn Flutter Web app to your VPS with Nginx. - VPS with Ubuntu 20.04/22.04 (or Debian-based distro) - Root or sudo access -- Domain name (gosojorn.com) pointed to your VPS IP +- Domain name (sojorn.net) pointed to your VPS IP - SSH access to your VPS --- @@ -87,7 +87,7 @@ apt install certbot python3-certbot-nginx -y **Important:** Make sure your domain DNS is already pointing to your VPS IP before running this. ```bash -certbot --nginx -d gosojorn.com -d www.gosojorn.com +certbot --nginx -d sojorn.net -d www.sojorn.net ``` Follow the prompts: @@ -130,21 +130,21 @@ server { listen 443 ssl http2; listen [::]:443 ssl http2; - server_name www.gosojorn.com; + server_name www.sojorn.net; - ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - return 301 https://gosojorn.com$request_uri; + return 301 https://sojorn.net$request_uri; } # Main server block server { listen 80; listen [::]:80; - server_name gosojorn.com; + server_name sojorn.net; # Redirect HTTP to HTTPS return 301 https://$server_name$request_uri; @@ -153,11 +153,11 @@ server { server { listen 443 ssl http2; listen [::]:443 ssl http2; - server_name gosojorn.com; + server_name sojorn.net; # SSL Configuration - ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem; + ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; @@ -327,8 +327,8 @@ chmod -R 755 /var/www/sojorn ## Part 8: Test Your Deployment -1. Visit `https://gosojorn.com` - you should see your app -2. Test deep linking: `https://gosojorn.com/username` should route to a profile +1. Visit `https://sojorn.net` - you should see your app +2. Test deep linking: `https://sojorn.net/username` should route to a profile 3. Check SSL: Look for the padlock icon in the browser --- @@ -508,10 +508,10 @@ certbot renew You now have: ✅ Nginx web server installed and configured ✅ SSL certificate for HTTPS -✅ Flutter Web app served at https://gosojorn.com +✅ Flutter Web app served at https://sojorn.net ✅ Deep linking support for URLs like /username ✅ Gzip compression for better performance ✅ Proper security headers ✅ Caching for static assets -Your app is now live and accessible at https://gosojorn.com! 🎉 +Your app is now live and accessible at https://sojorn.net! 🎉 diff --git a/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md b/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md index 0212a34..da14a6b 100644 --- a/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md +++ b/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md @@ -43,7 +43,7 @@ This document outlines the step-by-step process for cutover from Supabase to the ## Phase 4: Cutover (Zero Downtime Strategy) 1. **Parallel Run**: Keep both Supabase and Go VPS running. -2. **DNS Update**: Point your API subdomain (e.g., `api.gosojorn.com`) to the new VPS IP. +2. **DNS Update**: Point your API subdomain (e.g., `api.sojorn.net`) to the new VPS IP. 3. **TTL Check**: Ensure DNS TTL is low (e.g., 300s) before starting. 4. **Monitor**: Watch logs for 4xx/5xx errors. ```bash diff --git a/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md b/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md index e00df85..b246b8a 100644 --- a/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md +++ b/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md @@ -9,7 +9,7 @@ The infrastructure for GoSojorn is **now fully functional and production-ready** 1. **CORS Resolved:** Fixed "Failed to fetch" errors by implementing dynamic origin matching (required for `AllowCredentials`). 2. **Schema Complete:** Manually applied missing Signal Protocol migrations (`000002_e2ee_chat.up.sql`). 3. **Data Success:** Expanded seeder now provides ~300 posts and ~70 users, satisfying load-test requirements. -4. **Proxy Verified:** Nginx is correctly routing `api.gosojorn.com` to the Go service. +4. **Proxy Verified:** Nginx is correctly routing `api.sojorn.net` to the Go service. ## Phase 1: Infrastructure & Environment Integrity - **Service Health:** ✅ @@ -51,4 +51,4 @@ The infrastructure for GoSojorn is **now fully functional and production-ready** - **Status:** Stress test threshold MET. ## Final Verdict -The migration from Supabase to GoSojorn is **SUCCESSFUL**. The system is stable, the data is migrated/seeded, and the primary blocker (CORS) is removed. The Supabase instance can be safely paused after final client redirection to `api.gosojorn.com`. +The migration from Supabase to GoSojorn is **SUCCESSFUL**. The system is stable, the data is migrated/seeded, and the primary blocker (CORS) is removed. The Supabase instance can be safely paused after final client redirection to `api.sojorn.net`. diff --git a/sojorn_docs/reference/NEXT_STEPS.md b/sojorn_docs/reference/NEXT_STEPS.md index 952233f..fb84f36 100644 --- a/sojorn_docs/reference/NEXT_STEPS.md +++ b/sojorn_docs/reference/NEXT_STEPS.md @@ -6,8 +6,8 @@ All backend configuration is done. Images should now work properly! ### What Was Configured: -1. ✅ **Custom Domain Connected**: `media.gosojorn.com` → R2 bucket `sojorn-media` -2. ✅ **Environment Variable Set**: `R2_PUBLIC_URL=https://media.gosojorn.com` +1. ✅ **Custom Domain Connected**: `media.sojorn.net` → R2 bucket `sojorn-media` +2. ✅ **Environment Variable Set**: `R2_PUBLIC_URL=https://media.sojorn.net` 3. ✅ **Edge Function Deployed**: Updated `upload-image` function using custom domain 4. ✅ **DNS Verified**: Domain resolving to Cloudflare CDN 5. ✅ **API Queries Fixed**: All post queries include `image_url` field @@ -28,7 +28,7 @@ In the app: **Expected behavior**: - Image uploads successfully - Post appears in feed with image visible -- Image URL format: `https://media.gosojorn.com/{uuid}.jpg` +- Image URL format: `https://media.sojorn.net/{uuid}.jpg` **If it works**: Images will now display everywhere (feed, profiles, chains) ✅ @@ -43,7 +43,7 @@ ORDER BY created_at DESC LIMIT 5; ``` -Expected format: `https://media.gosojorn.com/[uuid].[ext]` +Expected format: `https://media.sojorn.net/[uuid].[ext]` ## Troubleshooting @@ -65,7 +65,7 @@ Look for: After uploading an image, copy its URL from the database and test: ```bash -curl -I https://media.gosojorn.com/[filename-from-db] +curl -I https://media.sojorn.net/[filename-from-db] ``` Should return `HTTP/1.1 200 OK` or `HTTP/2 200` @@ -129,7 +129,7 @@ Complete guides available: | API Queries | ✅ Include `image_url` field | | Flutter Model | ✅ Post model parses `image_url` | | Widget Display | ✅ PostItem widget shows images | -| Custom Domain | ✅ `media.gosojorn.com` connected | +| Custom Domain | ✅ `media.sojorn.net` connected | **Ready to test!** 🚀 diff --git a/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md b/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md index fa7b57d..501abb8 100644 --- a/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md +++ b/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md @@ -253,7 +253,7 @@ Available keys: [id, body, created_at, tone_label, allow_chain, **Other feed response (has image_url):** ``` -DEBUG Post.fromJson: Found image_url in JSON: https://media.gosojorn.com/88a7cc72-... +DEBUG Post.fromJson: Found image_url in JSON: https://media.sojorn.net/88a7cc72-... Available keys: [id, body, author_id, category_id, tone_label, cis_score, status, created_at, edited_at, deleted_at, allow_chain, chain_parent_id, image_url, chain_parent, metrics, author] @@ -271,9 +271,9 @@ I/flutter: PostMedia: post.imageUrl = null ### After Fix ``` -I/flutter: DEBUG Post.fromJson: Found image_url in JSON: https://media.gosojorn.com/88a7cc72-... -I/flutter: PostMedia: post.imageUrl = https://media.gosojorn.com/88a7cc72-... -I/flutter: PostMedia: SHOWING IMAGE for https://media.gosojorn.com/88a7cc72-... +I/flutter: DEBUG Post.fromJson: Found image_url in JSON: https://media.sojorn.net/88a7cc72-... +I/flutter: PostMedia: post.imageUrl = https://media.sojorn.net/88a7cc72-... +I/flutter: PostMedia: SHOWING IMAGE for https://media.sojorn.net/88a7cc72-... I/flutter: PostMedia: Image loading... 8899 / 275401 I/flutter: PostMedia: Image LOADED successfully ``` @@ -291,7 +291,7 @@ npx supabase functions deploy feed-sojorn feed-personal --no-verify-jwt ### Image Upload Flow (Already Working) 1. User selects image in `ComposeScreen` 2. Image uploaded via `ImageUploadService.uploadImage()` to Cloudflare R2 -3. Returns public URL: `https://media.gosojorn.com/{uuid}.jpg` +3. Returns public URL: `https://media.sojorn.net/{uuid}.jpg` 4. URL sent to `publish-post` edge function 5. Saved to `posts.image_url` column diff --git a/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md b/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md index 38fc1ec..c266d8a 100644 --- a/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md +++ b/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md @@ -4,9 +4,9 @@ - **R2 Bucket**: `sojorn-media` - **Account ID**: `7041ca6e0f40307190dc2e65e2fb5e0f` -- **Custom Domain**: `media.gosojorn.com` +- **Custom Domain**: `media.sojorn.net` - **Upload URL**: `https://7041ca6e0f40307190dc2e65e2fb5e0f.r2.cloudflarestorage.com/sojorn-media` -- **Public URL**: `https://media.gosojorn.com` +- **Public URL**: `https://media.sojorn.net` ## Quick Test @@ -15,11 +15,11 @@ Go to: https://dash.cloudflare.com → R2 → `sojorn-media` bucket → Settings Under "Custom Domains", you should see: -- ✅ `media.gosojorn.com` with status "Active" +- ✅ `media.sojorn.net` with status "Active" If not connected: 1. Click "Connect Domain" -2. Enter: `media.gosojorn.com` +2. Enter: `media.sojorn.net` 3. Wait 1-2 minutes for activation ### 2. Test Upload in App @@ -47,14 +47,14 @@ ORDER BY created_at DESC LIMIT 1; ``` -**Expected URL format**: `https://media.gosojorn.com/[uuid].[ext]` +**Expected URL format**: `https://media.sojorn.net/[uuid].[ext]` ### 4. Test URL Directly Copy the image_url from database and test in browser or curl: ```bash -curl -I https://media.gosojorn.com/[filename-from-database] +curl -I https://media.sojorn.net/[filename-from-database] ``` **Expected response**: `HTTP/2 200 OK` @@ -91,7 +91,7 @@ Required secrets: | Issue | Cause | Solution | |-------|-------|----------| | Upload fails with 401 | Invalid R2 credentials | Check R2_ACCESS_KEY and R2_SECRET_KEY | -| Upload succeeds but image 404 | Domain not connected | Connect media.gosojorn.com to bucket | +| Upload succeeds but image 404 | Domain not connected | Connect media.sojorn.net to bucket | | "Missing R2_PUBLIC_URL" | Secret not set/propagated | Wait 2 minutes, redeploy function | | Image loads slowly | Not cached | Normal for first load, subsequent loads cached | @@ -100,7 +100,7 @@ Required secrets: ✅ **Upload Flow**: 1. User selects image → App processes/filters 2. App uploads to edge function → Edge function uploads to R2 -3. Edge function returns: `https://media.gosojorn.com/[uuid].jpg` +3. Edge function returns: `https://media.sojorn.net/[uuid].jpg` 4. App saves post with image_url to database 5. Feed queries posts with image_url 6. PostItem widget displays image