Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets. Co-authored-by: Cursor <cursoragent@cursor.com>
353 lines
14 KiB
Python
353 lines
14 KiB
Python
"""Direct SFTP sync + native rebuild (when Gitea push fails)."""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
|
|
HOST = "107.173.30.245"
|
|
USER = "root"
|
|
PASS = "Xiao4669805"
|
|
REMOTE = "/opt/nomadweb"
|
|
DOMAIN = "nomadweb.nomadro.com"
|
|
|
|
# Override via env or edit before run
|
|
FILES = [
|
|
"README.md",
|
|
".gitignore",
|
|
"frontend/src/app/changelog/page.tsx",
|
|
"frontend/src/app/globals.css",
|
|
"frontend/src/app/profile/page.tsx",
|
|
"frontend/src/app/plan/page.tsx",
|
|
"frontend/src/app/compare/page.tsx",
|
|
"frontend/src/app/layout.tsx",
|
|
"frontend/src/app/manifest.ts",
|
|
"frontend/src/app/login/page.tsx",
|
|
"frontend/src/app/sitemap.ts",
|
|
"frontend/src/app/tools/page.tsx",
|
|
"frontend/src/app/destinations/[slug]/page.tsx",
|
|
"frontend/next.config.ts",
|
|
"frontend/public/sw.js",
|
|
"frontend/public/icons/icon.svg",
|
|
"frontend/public/icons/icon-192.png",
|
|
"frontend/public/icons/icon-512.png",
|
|
"frontend/public/icons/apple-touch-icon.png",
|
|
"frontend/src/components/DestinationDetailClient.tsx",
|
|
"frontend/src/components/DestinationMatcher.tsx",
|
|
"frontend/src/components/CompareModal.tsx",
|
|
"frontend/src/components/CompareClient.tsx",
|
|
"frontend/src/components/GlobalSearch.tsx",
|
|
"frontend/src/components/HomeClient.tsx",
|
|
"frontend/src/components/KeyboardShortcuts.tsx",
|
|
"frontend/src/components/Navbar.tsx",
|
|
"frontend/src/components/TripPlanner.tsx",
|
|
"frontend/src/components/WorldMap.tsx",
|
|
"frontend/src/components/Destinations.tsx",
|
|
"frontend/src/components/MovePlanClient.tsx",
|
|
"frontend/src/components/MatcherTrigger.tsx",
|
|
"frontend/src/components/ParticleCanvas.tsx",
|
|
"frontend/src/components/Loader.tsx",
|
|
"frontend/src/components/WhenVisible.tsx",
|
|
"frontend/src/components/Hero.tsx",
|
|
"frontend/src/components/ToolsStrip.tsx",
|
|
"frontend/src/components/JourneyStrip.tsx",
|
|
"frontend/src/components/SectionNav.tsx",
|
|
"frontend/src/components/Footer.tsx",
|
|
"frontend/src/components/OnboardingTour.tsx",
|
|
"frontend/src/components/PwaRegister.tsx",
|
|
"frontend/src/components/BlogSection.tsx",
|
|
"frontend/src/components/FAQSection.tsx",
|
|
"frontend/src/components/VisaSection.tsx",
|
|
"frontend/src/lib/tools.ts",
|
|
"frontend/src/lib/types.ts",
|
|
"frontend/src/lib/planMeta.ts",
|
|
"frontend/src/lib/compareScore.ts",
|
|
"frontend/src/lib/tripActions.ts",
|
|
"frontend/src/lib/matcherPrefs.ts",
|
|
"frontend/src/lib/tripStorage.ts",
|
|
"frontend/src/lib/authNext.ts",
|
|
"frontend/src/lib/planSync.ts",
|
|
"frontend/src/lib/planIcs.ts",
|
|
"frontend/src/lib/auth.tsx",
|
|
"frontend/src/lib/api.ts",
|
|
"frontend/src/lib/i18n/types.ts",
|
|
"frontend/src/lib/i18n/dictionaries.ts",
|
|
"frontend/src/lib/i18n/I18nProvider.tsx",
|
|
"frontend/src/lib/i18n/index.ts",
|
|
"frontend/package.json",
|
|
"frontend/package-lock.json",
|
|
"frontend/postcss.config.mjs",
|
|
"frontend/content/book/book.json",
|
|
"frontend/content/book/en/book.json",
|
|
"frontend/content/book/features.json",
|
|
"frontend/content/book/site.json",
|
|
"frontend/content/book/chapters/00-preface.md",
|
|
"frontend/content/book/chapters/01-first-remote.md",
|
|
"frontend/content/book/chapters/02-chiangmai.md",
|
|
"frontend/content/book/chapters/03-gear.md",
|
|
"frontend/content/book/chapters/04-homelab.md",
|
|
"frontend/content/book/chapters/05-network.md",
|
|
"frontend/content/book/chapters/06-epilogue.md",
|
|
"frontend/src/app/book/layout.tsx",
|
|
"frontend/src/app/book/loading.tsx",
|
|
"frontend/src/app/book/page.tsx",
|
|
"frontend/src/app/book/ebook-globals.css",
|
|
"frontend/src/app/book/opengraph-image.tsx",
|
|
"frontend/src/app/book/read/page.tsx",
|
|
"frontend/src/app/book/read/[slug]/page.tsx",
|
|
"frontend/src/components/ebook/AdminConsole.tsx",
|
|
"frontend/src/components/ebook/AdSlot.tsx",
|
|
"frontend/src/components/ebook/AnalyticsProvider.tsx",
|
|
"frontend/src/components/ebook/AnimatedCounter.tsx",
|
|
"frontend/src/components/ebook/BookEnd.tsx",
|
|
"frontend/src/components/ebook/BookmarkButton.tsx",
|
|
"frontend/src/components/ebook/BookSearch.tsx",
|
|
"frontend/src/components/ebook/BuyEbook.tsx",
|
|
"frontend/src/components/ebook/ChapterHero.tsx",
|
|
"frontend/src/components/ebook/ChapterMenu.tsx",
|
|
"frontend/src/components/ebook/ChapterNav.tsx",
|
|
"frontend/src/components/ebook/ChapterReader.tsx",
|
|
"frontend/src/components/ebook/ChapterSidebar.tsx",
|
|
"frontend/src/components/ebook/ContinueReading.tsx",
|
|
"frontend/src/components/ebook/ContinuousReader.tsx",
|
|
"frontend/src/components/ebook/CoverChapterGrid.tsx",
|
|
"frontend/src/components/ebook/CoverPage.tsx",
|
|
"frontend/src/components/ebook/CryptoSupport.tsx",
|
|
"frontend/src/components/ebook/FeedbackPanel.tsx",
|
|
"frontend/src/components/ebook/FocusReader.tsx",
|
|
"frontend/src/components/ebook/FontSizeControl.tsx",
|
|
"frontend/src/components/ebook/GrowthKit.tsx",
|
|
"frontend/src/components/ebook/HighlightPainter.tsx",
|
|
"frontend/src/components/ebook/InChapterSearch.tsx",
|
|
"frontend/src/components/ebook/InsightCarousel.tsx",
|
|
"frontend/src/components/ebook/KeyboardNav.tsx",
|
|
"frontend/src/components/ebook/LocaleProvider.tsx",
|
|
"frontend/src/components/ebook/LocaleSwitcher.tsx",
|
|
"frontend/src/components/ebook/MarkdownContent.tsx",
|
|
"frontend/src/components/ebook/MobileChapterBar.tsx",
|
|
"frontend/src/components/ebook/ProgressBar.tsx",
|
|
"frontend/src/components/ebook/ProgressRing.tsx",
|
|
"frontend/src/components/ebook/Providers.tsx",
|
|
"frontend/src/components/ebook/ReaderLayout.tsx",
|
|
"frontend/src/components/ebook/ReaderSettings.tsx",
|
|
"frontend/src/components/ebook/ReaderShare.tsx",
|
|
"frontend/src/components/ebook/ReaderShortcuts.tsx",
|
|
"frontend/src/components/ebook/ReadingModeToggle.tsx",
|
|
"frontend/src/components/ebook/ReadingTracker.tsx",
|
|
"frontend/src/components/ebook/ScrollReaderShell.tsx",
|
|
"frontend/src/components/ebook/ScrollReadingTracker.tsx",
|
|
"frontend/src/components/ebook/ScrollReveal.tsx",
|
|
"frontend/src/components/ebook/ScrollToTop.tsx",
|
|
"frontend/src/components/ebook/SelectionShare.tsx",
|
|
"frontend/src/components/ebook/ShareButton.tsx",
|
|
"frontend/src/components/ebook/ShareQuoteCard.tsx",
|
|
"frontend/src/components/ebook/ShareToX.tsx",
|
|
"frontend/src/components/ebook/SkillRadar.tsx",
|
|
"frontend/src/components/ebook/SupportThanksContent.tsx",
|
|
"frontend/src/components/ebook/ThemeToggle.tsx",
|
|
"frontend/src/lib/ebook/affiliate.ts",
|
|
"frontend/src/lib/ebook/analytics.ts",
|
|
"frontend/src/lib/ebook/book-types.ts",
|
|
"frontend/src/lib/ebook/book.server.ts",
|
|
"frontend/src/lib/ebook/contentPath.ts",
|
|
"frontend/src/lib/ebook/bookmarks.ts",
|
|
"frontend/src/lib/ebook/creem.ts",
|
|
"frontend/src/lib/ebook/features.server.ts",
|
|
"frontend/src/lib/ebook/features.ts",
|
|
"frontend/src/lib/ebook/headings.ts",
|
|
"frontend/src/lib/ebook/highlights.ts",
|
|
"frontend/src/lib/ebook/i18n/index.ts",
|
|
"frontend/src/lib/ebook/i18n/messages/en.ts",
|
|
"frontend/src/lib/ebook/i18n/messages/zh.ts",
|
|
"frontend/src/lib/ebook/i18n/server.ts",
|
|
"frontend/src/lib/ebook/i18n/types.ts",
|
|
"frontend/src/lib/ebook/metadata.ts",
|
|
"frontend/src/lib/ebook/nowpayments.ts",
|
|
"frontend/src/lib/ebook/paths.ts",
|
|
"frontend/src/lib/ebook/reader-prefs.ts",
|
|
"frontend/src/lib/ebook/reading-progress.ts",
|
|
"frontend/src/lib/ebook/search.ts",
|
|
"frontend/src/lib/ebook/share-text.ts",
|
|
"frontend/src/lib/ebook/share.ts",
|
|
"frontend/src/lib/ebook/site-config.server.ts",
|
|
"frontend/src/lib/ebook/site-env.ts",
|
|
"frontend/src/lib/ebook/site-types.ts",
|
|
"frontend/src/lib/ebook/site.ts",
|
|
"frontend/src/lib/ebook/support-tiers.ts",
|
|
"frontend/src/lib/ebook/time.ts",
|
|
"frontend/src/lib/ebook/utm.ts",
|
|
"backend/app/services/auth.py",
|
|
"backend/app/routers/auth.py",
|
|
"backend/app/schemas.py",
|
|
"backend/app/main.py",
|
|
"backend/app/routers/api.py",
|
|
"backend/app/routers/payment.py",
|
|
"backend/app/routers/social.py",
|
|
"backend/app/routers/community.py",
|
|
"backend/app/data/community_data.py",
|
|
"backend/app/data/digital_content.py",
|
|
"backend/app/data/social_profiles.py",
|
|
"backend/app/services/meetup_live.py",
|
|
"backend/app/services/payment_service.py",
|
|
"backend/app/services/recommendations.py",
|
|
"backend/app/services/social_store.py",
|
|
"backend/app/services/community_store.py",
|
|
"backend/app/services/payment_providers.py",
|
|
"frontend/src/app/next-stop/page.tsx",
|
|
"frontend/src/app/meetups/page.tsx",
|
|
"frontend/src/app/meetups/[id]/live/page.tsx",
|
|
"frontend/src/app/community/page.tsx",
|
|
"frontend/src/app/community/new/page.tsx",
|
|
"frontend/src/app/community/[id]/page.tsx",
|
|
"frontend/src/app/gigs/page.tsx",
|
|
"frontend/src/app/pricing/page.tsx",
|
|
"frontend/src/app/notifications/page.tsx",
|
|
"frontend/src/app/members/[id]/page.tsx",
|
|
"frontend/src/app/meetups/host/page.tsx",
|
|
"frontend/src/app/dating/likes/page.tsx",
|
|
"frontend/src/app/tools/[id]/page.tsx",
|
|
"frontend/src/app/dating/page.tsx",
|
|
"frontend/src/app/chat/page.tsx",
|
|
"frontend/src/app/chat/[id]/page.tsx",
|
|
"frontend/src/app/join/page.tsx",
|
|
"frontend/src/app/join/paid/page.tsx",
|
|
"frontend/src/app/digital/page.tsx",
|
|
"frontend/src/app/digital/course/page.tsx",
|
|
"frontend/src/app/digital/course/[m]/[l]/page.tsx",
|
|
"frontend/src/app/digital/jobs/page.tsx",
|
|
"frontend/src/components/NextStopClient.tsx",
|
|
"frontend/src/components/MeetupsClient.tsx",
|
|
"frontend/src/components/MeetupLiveClient.tsx",
|
|
"frontend/src/components/CommunityHubClient.tsx",
|
|
"frontend/src/components/CommunityNewClient.tsx",
|
|
"frontend/src/components/DiscussionDetailClient.tsx",
|
|
"frontend/src/components/MeetupsHostClient.tsx",
|
|
"frontend/src/components/GigsClient.tsx",
|
|
"frontend/src/components/PricingClient.tsx",
|
|
"frontend/src/components/NotificationsClient.tsx",
|
|
"frontend/src/components/MemberProfileClient.tsx",
|
|
"frontend/src/components/DatingLikesClient.tsx",
|
|
"frontend/src/components/SiteShell.tsx",
|
|
"frontend/src/components/ToolRunnerClient.tsx",
|
|
"frontend/src/components/DatingClient.tsx",
|
|
"frontend/src/components/ChatClient.tsx",
|
|
"frontend/src/components/ChatThreadClient.tsx",
|
|
"frontend/src/components/JoinClient.tsx",
|
|
"frontend/src/components/JoinPaidClient.tsx",
|
|
"frontend/src/components/DigitalHomeClient.tsx",
|
|
"frontend/src/components/DigitalCourseClient.tsx",
|
|
"frontend/src/components/DigitalLessonClient.tsx",
|
|
"frontend/src/components/DigitalJobsClient.tsx",
|
|
"frontend/src/lib/toolsRegistry.tsx",
|
|
]
|
|
|
|
BUILD = f"""
|
|
set -euo pipefail
|
|
exec 9>/var/lock/nomadro-deploy.lock
|
|
if ! flock -n 9; then
|
|
echo LOCK_BUSY
|
|
exit 75
|
|
fi
|
|
REMOTE={REMOTE}
|
|
DOMAIN={DOMAIN}
|
|
export NEXT_TELEMETRY_DISABLED=1
|
|
export NEXT_PUBLIC_API_URL=https://${{DOMAIN}}/api/v1
|
|
export NEXT_PUBLIC_SITE_URL=https://${{DOMAIN}}
|
|
export NODE_ENV=production
|
|
|
|
echo RESTART_API
|
|
systemctl restart nomadro-api
|
|
|
|
echo BUILD_START
|
|
cd "$REMOTE/frontend"
|
|
rm -rf .next
|
|
NPM_CONFIG_PRODUCTION=false npm install
|
|
npm run build
|
|
mkdir -p .next/standalone/.next
|
|
rm -rf .next/standalone/public .next/standalone/.next/static
|
|
cp -a public .next/standalone/public
|
|
cp -a .next/static .next/standalone/.next/static
|
|
rm -rf .next/standalone/content
|
|
mkdir -p .next/standalone/content
|
|
cp -a content .next/standalone/content
|
|
# also keep content next to process cwd variants
|
|
rm -rf .next/standalone/frontend 2>/dev/null || true
|
|
systemctl restart nomadro-web
|
|
ok=0
|
|
for i in $(seq 1 40); do
|
|
web=$(curl -s -o /dev/null -w '%{{http_code}}' http://127.0.0.1:3055/ || true)
|
|
api=$(curl -s http://127.0.0.1:8055/api/v1/health || true)
|
|
echo "try=$i web=$web api=$api"
|
|
if echo "$api" | grep -q '"status":"ok"' && [ "$web" = "200" ]; then
|
|
ok=1
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
curl -skI "https://${{DOMAIN}}/plan" | head -6
|
|
curl -sk "https://${{DOMAIN}}/api/v1/health"
|
|
echo DIRECT_SYNC=1
|
|
[ "$ok" = 1 ]
|
|
"""
|
|
|
|
|
|
def ensure_dir(sftp: paramiko.SFTPClient, path: str) -> None:
|
|
parts = path.strip("/").split("/")
|
|
cur = ""
|
|
for p in parts:
|
|
cur += "/" + p
|
|
try:
|
|
sftp.stat(cur)
|
|
except FileNotFoundError:
|
|
try:
|
|
sftp.mkdir(cur)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def main() -> int:
|
|
root = Path(__file__).resolve().parents[1]
|
|
t0 = time.time()
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
print("Connecting...")
|
|
client.connect(HOST, username=USER, password=PASS, timeout=30)
|
|
sftp = client.open_sftp()
|
|
for rel in FILES:
|
|
local = root / rel
|
|
if not local.exists():
|
|
print(f"SKIP missing {rel}")
|
|
continue
|
|
remote = f"{REMOTE}/{rel}"
|
|
print(f"UPLOAD {rel}")
|
|
ensure_dir(sftp, str(Path(remote).parent).replace("\\", "/"))
|
|
sftp.put(str(local), remote)
|
|
sftp.close()
|
|
print("Uploaded. Building...")
|
|
|
|
_, stdout, stderr = client.exec_command(
|
|
f"cat > /tmp/nomadro-direct.sh <<'EOF'\n{BUILD}\nEOF\nbash /tmp/nomadro-direct.sh",
|
|
timeout=900,
|
|
)
|
|
while True:
|
|
line = stdout.readline()
|
|
if not line:
|
|
break
|
|
print(line, end="")
|
|
err = stderr.read().decode("utf-8", errors="replace")
|
|
code = stdout.channel.recv_exit_status()
|
|
if err.strip():
|
|
print("LOG:", err[-4000:])
|
|
print("exit:", code)
|
|
client.close()
|
|
print(f"Done in {int(time.time() - t0)}s")
|
|
return code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|