diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index d535db0d51..631fa03621 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -764,6 +764,12 @@ Opens Phoenix Pro page ## HELP\_CANCEL\_TRIAL Cancels Phoenix Pro trial +**Kind**: global variable + + +## HELP\_DISABLE\_OFF\_HOURS +Toggles the Pro off-hours offer (label uses server-vended brand name) + **Kind**: global variable diff --git a/package.json b/package.json index 577c665716..4cda12c33b 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,6 @@ "_minorVersionBump": "gulp minorVersionBump", "_majorVersionBump": "gulp majorVersionBump", "serve": "npm install --prefix src-node && node serve-proxy.js . -p 8000 -c-1", - "serveLocalAccount": "npm install --prefix src-node && node serve-proxy.js . -p 8000 -c-1 --localAccount", - "serveStagingAccount": "npm install --prefix src-node && node serve-proxy.js . -p 8000 -c-1 --stagingAccount", "_serveWithWebCacheHelp": "echo !!!Make sure to npm run release:dev/stageing/prod before testing the cache!!!", "serveWithWebCache": "npm run _releaseWebCache && npm run _serveWithWebCacheHelp && http-server ./dist -p 8000 -c-1", "serveExternal": "npm install --prefix src-node && node serve-proxy.js . -p 8000 -a 0.0.0.0 --log-ip -c-1", diff --git a/serve-proxy.js b/serve-proxy.js index 6054454b94..d7a1819902 100644 --- a/serve-proxy.js +++ b/serve-proxy.js @@ -12,9 +12,16 @@ const ACCOUNT_STAGING = 'https://account-stage.phcode.dev'; const ACCOUNT_DEV = 'http://localhost:5000'; const ASSETS_SERVER = 'https://assets.phcode.dev'; -// Account server configuration - switch between local and production -let accountServer = ACCOUNT_PROD; // Production -// Set to local development server if --localAccount flag is provided +// Static proxy routes - the server is fully stateless; the client chooses which +// accounts server to talk to via the dev-only Debug Overrides dialog (accounts +// server dropdown), which selects the proxy path at boot. Longer prefixes are +// listed first so /proxy/accountsDev is not swallowed by /proxy/accounts. +const PROXY_ROUTES = [ + { prefix: '/proxy/accountsStaging', target: ACCOUNT_STAGING }, + { prefix: '/proxy/accountsDev', target: ACCOUNT_DEV }, + { prefix: '/proxy/accounts', target: ACCOUNT_PROD }, + { prefix: '/proxy/assets', target: ASSETS_SERVER } +]; // Default configuration let config = { @@ -29,8 +36,6 @@ let config = { // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); - let hasLocalAccount = false; - let hasStagingAccount = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -52,22 +57,10 @@ function parseArgs() { config.silent = true; } else if (arg === '--log-ip') { config.logIp = true; - } else if (arg === '--localAccount') { - hasLocalAccount = true; - accountServer = ACCOUNT_DEV; - } else if (arg === '--stagingAccount') { - hasStagingAccount = true; - accountServer = ACCOUNT_STAGING; } else if (!arg.startsWith('-')) { config.root = path.resolve(arg); } } - - // Check for mutually exclusive flags - if (hasLocalAccount && hasStagingAccount) { - console.error('Error: --localAccount and --stagingAccount cannot be used together'); - process.exit(1); - } } // Create proxy server @@ -86,37 +79,31 @@ proxy.on('error', (err, req, res) => { } }); -// Modify proxy request headers -proxy.on('proxyReq', (proxyReq, req) => { - // Transform localhost:8000 to appear as phcode.dev domain - const originalReferer = req.headers.referer; - const originalOrigin = req.headers.origin; - - // Set target host based on which proxy route is being used - const targetHost = req._proxyTarget - ? new URL(req._proxyTarget).hostname - : new URL(accountServer).hostname; - proxyReq.setHeader('Host', targetHost); +// Build the headers for the proxied request. Passed via proxy.web options instead +// of mutating inside the proxyReq event: with followRedirects enabled the request +// headers can already be flushed by the time proxyReq fires (ERR_HTTP_HEADERS_SENT). +// Transforms localhost:8000 to appear as the phcode.dev domain. +function buildProxyHeaders(req, target) { + const headers = { + 'Host': new URL(target).hostname, + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-For': req.connection.remoteAddress + }; - // Transform referer from localhost:8000 to phcode.dev + const originalReferer = req.headers.referer; if (originalReferer && originalReferer.includes('localhost:8000')) { - const newReferer = originalReferer.replace(/http:\/\/localhost:8000/g, 'https://phcode.dev'); - proxyReq.setHeader('Referer', newReferer); + headers['Referer'] = originalReferer.replace(/http:\/\/localhost:8000/g, 'https://phcode.dev'); } else if (!originalReferer) { - proxyReq.setHeader('Referer', 'https://phcode.dev/'); + headers['Referer'] = 'https://phcode.dev/'; } - // Transform origin from localhost:8000 to phcode.dev + const originalOrigin = req.headers.origin; if (originalOrigin && originalOrigin.includes('localhost:8000')) { - const newOrigin = originalOrigin.replace(/http:\/\/localhost:8000/g, 'https://phcode.dev'); - proxyReq.setHeader('Origin', newOrigin); + headers['Origin'] = originalOrigin.replace(/http:\/\/localhost:8000/g, 'https://phcode.dev'); } - // Ensure HTTPS scheme - proxyReq.setHeader('X-Forwarded-Proto', 'https'); - proxyReq.setHeader('X-Forwarded-For', req.connection.remoteAddress); - -}); + return headers; +} // Modify proxy response headers proxy.on('proxyRes', (proxyRes, req, res) => { @@ -298,70 +285,28 @@ const server = http.createServer((req, res) => { return; } - // Handle proxy config request - if (parsedUrl.pathname === '/proxy/config') { - const configResponse = { - accountURL: accountServer + '/' - }; - - if (!config.silent) { - console.log(`[CONFIG] ${req.method} ${parsedUrl.pathname} -> ${JSON.stringify(configResponse)}`); - } - - const headers = { - 'Content-Type': 'application/json' - }; - - if (config.cors) { - headers['Access-Control-Allow-Origin'] = '*'; - headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'; - headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control'; - } - - res.writeHead(200, headers); - res.end(JSON.stringify(configResponse)); - return; - } - - // Check if this is a proxy request - if (parsedUrl.pathname.startsWith('/proxy/accounts')) { - // Extract the path after /proxy/accounts - const targetPath = parsedUrl.pathname.replace('/proxy/accounts', ''); - const originalUrl = req.url; - - // Modify the request URL for the proxy - req.url = targetPath + (parsedUrl.search || ''); - req._proxyTarget = accountServer; - - if (!config.silent) { - console.log(`[PROXY] ${req.method} ${originalUrl} -> ${accountServer}${req.url}`); - } - - // Proxy the request - proxy.web(req, res, { - target: accountServer, - changeOrigin: true, - secure: true - }); - return; - } - - if (parsedUrl.pathname.startsWith('/proxy/assets')) { - const targetPath = parsedUrl.pathname.replace('/proxy/assets', ''); - const originalUrl = req.url; - req.url = targetPath + (parsedUrl.search || ''); - req._proxyTarget = ASSETS_SERVER; - - if (!config.silent) { - console.log(`[PROXY] ${req.method} ${originalUrl} -> ${ASSETS_SERVER}${req.url}`); + // Check if this is a proxy request (routes are static - see PROXY_ROUTES) + for (const route of PROXY_ROUTES) { + if (parsedUrl.pathname === route.prefix || parsedUrl.pathname.startsWith(route.prefix + '/')) { + const targetPath = parsedUrl.pathname.replace(route.prefix, ''); + const originalUrl = req.url; + + // Modify the request URL for the proxy + req.url = targetPath + (parsedUrl.search || ''); + req._proxyTarget = route.target; + + if (!config.silent) { + console.log(`[PROXY] ${req.method} ${originalUrl} -> ${route.target}${req.url}`); + } + + proxy.web(req, res, { + target: route.target, + changeOrigin: true, + secure: true, + headers: buildProxyHeaders(req, route.target) + }); + return; } - - proxy.web(req, res, { - target: ASSETS_SERVER, - changeOrigin: true, - secure: true - }); - return; } // Serve static files @@ -405,9 +350,10 @@ server.listen(config.port, config.host, () => { console.log(`Starting up http-server, serving ${config.root}`); console.log(`Available on:`); console.log(` http://${config.host === '0.0.0.0' ? 'localhost' : config.host}:${config.port}`); - console.log(`Proxy routes:`); - console.log(` /proxy/accounts/* -> ${accountServer}/*`); - console.log(` /proxy/assets/* -> ${ASSETS_SERVER}/*`); + console.log(`Proxy routes (pick the accounts server in Debug > Diagnostic Tools > Debug Overrides):`); + for (const route of PROXY_ROUTES) { + console.log(` ${route.prefix}/* -> ${route.target}/*`); + } console.log('Hit CTRL-C to stop the server'); } }); diff --git a/src/command/Commands.js b/src/command/Commands.js index ee412fafb7..b2586a1656 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -419,6 +419,9 @@ define(function (require, exports, module) { /** Cancels Phoenix Pro trial */ exports.HELP_CANCEL_TRIAL = "help.cancelTrial"; + /** Toggles the Pro off-hours offer (label uses server-vended brand name) */ + exports.HELP_DISABLE_OFF_HOURS = "help.disableOffHoursOffer"; + /** Opens Phoenix License page */ exports.HELP_VIEW_LICENSE = "help.viewLicense"; // HelpCommandHandlers.js _handleLinkMenuItem() diff --git a/src/index.html b/src/index.html index 8609768790..a2d1c98fa6 100644 --- a/src/index.html +++ b/src/index.html @@ -533,21 +533,26 @@ } async function _startRequireLoop() { - // If running on localhost, `npm run serve`, `npm run serveLocalAccount` targets puts in a fetch proxy. - // tTe proxy helps work around cookie domain set by phcode.dev as the dev urls are localhost. so to use - // either the actual services endpoint or localhost endpoints in dev, this is needed. - if (!Phoenix.isTestWindow && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) { + // Dev-only accounts-server override, set from Debug > Diagnostic Tools > Debug Overrides. + // Read synchronously from localStorage before any AMD module loads: account_url covers the + // desktop app and every web-tab flow; accounts_proxy_path tells the login services which + // serve-proxy route to use in browser dev (the proxy works around phcode.dev cookie domains + // on localhost). Same dev gate as the dialog itself; test windows always keep baked config. + if (!Phoenix.isTestWindow && window.AppConfig.config.environment === "dev") { try { - const response = await fetch('/proxy/config'); - if (response.ok) { - const config = await response.json(); - if (config.accountURL) { - window.AppConfig.config.account_url = config.accountURL; - console.log('Applied dynamic account URL from proxy:', config.accountURL); - } + const overrides = JSON.parse( + localStorage.getItem("LOCAL_OVERIDES_FOR_PHOIENXI_DEBUG")) || {}; + if (overrides.ACCOUNTS_SERVER_OVERRIDE === "dev") { + window.AppConfig.config.account_url = "http://localhost:5000/"; + window.AppConfig.config.accounts_proxy_path = "/proxy/accountsDev"; + console.log('Debug override: accounts server set to local dev (localhost:5000)'); + } else if (overrides.ACCOUNTS_SERVER_OVERRIDE === "staging") { + window.AppConfig.config.account_url = "https://account-stage.phcode.dev/"; + window.AppConfig.config.accounts_proxy_path = "/proxy/accountsStaging"; + console.log('Debug override: accounts server set to staging'); } } catch (error) { - console.warn('Failed to fetch proxy config, using default account URL:', error); + console.warn('Failed to read accounts server override, using default:', error); } } loadJS('thirdparty/requirejs/require.js', _requireDone, document.body, "main"); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7b9b425bb3..0b307718ac 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -2448,6 +2448,30 @@ define({ "GET_PRO_NOT_NOW": "Not Now", "GET_PHOENIX_PRO": "Get Phoenix Pro", "USER_FREE_PLAN_NAME_DO_NOT_TRANSLATE": "Community Edition", + // Pro off-hours offer ({0} is the server-vended offer brand name, eg. "Pro Off Hours") + "OFF_HOURS_ENDING_TITLE": "{0} is ending soon", + "OFF_HOURS_ENDING_MSG": "Your free {0} session ends soon. Get {2} to keep Pro all day.", + "OFF_HOURS_ENDED_TITLE": "{0} has ended", + "OFF_HOURS_ENDED_MSG": "Your free {0} session has ended. Get {2} to keep Pro features on.", + "OFF_HOURS_LOGIN_TITLE": "{0} is now free", + "OFF_HOURS_LOGIN_MSG": "{0}: {1} is free every day during {2}. Log in to use it.", + "OFF_HOURS_LOGIN_BTN": "Log in", + "OFF_HOURS_DONT_SHOW_AGAIN": "Don't show this again", + "OFF_HOURS_NAV_TOOLTIP": "{0} — Phoenix Pro is free for you during your personal time ({1})", + "OFF_HOURS_TIME_LEFT_HOURS": "{0}h left", + "OFF_HOURS_TIME_LEFT_MINUTES": "{0}m left", + "OFF_HOURS_DURATION_HOURS": "{0}h", + "OFF_HOURS_DURATION_MINUTES": "{0}m", + "OFF_HOURS_POPUP_ACTIVE": "{0}: {1} left", + "OFF_HOURS_POPUP_LOGIN_ACTIVE": "Log in to get {0}: {1} left", + "OFF_HOURS_POPUP_UPCOMING": "{0} starts in {1}", + "OFF_HOURS_EXPLAIN_TITLE": "What is {0}?", + "OFF_HOURS_EXPLAIN_MSG": "{0} is free for everyone during personal off-work hours — {1}, in your local time. Just use the editor during these hours, no subscription needed.", + "OFF_HOURS_EXPLAIN_ACTIVE": "It's free right now — {0} left!", + "OFF_HOURS_EXPLAIN_LOGIN_ACTIVE": "It's free right now — {0} left. Log in to use it!", + "OFF_HOURS_EXPLAIN_UPCOMING": "Your next free session starts in {0}.", + "OFF_HOURS_WAIT_BTN": "I'll Wait", + "OFF_HOURS_KEEP_CODING_BTN": "Keep Coding", // license dialogs "MANAGE_LICENSE_DIALOG_TITLE": "Manage Licenses", "LICENSE_ACCOUNT_HEADING": "Account License", diff --git a/src/phoenix-builder/debug-overrides-dialog.html b/src/phoenix-builder/debug-overrides-dialog.html index 09705079d7..1b431eef17 100644 --- a/src/phoenix-builder/debug-overrides-dialog.html +++ b/src/phoenix-builder/debug-overrides-dialog.html @@ -12,9 +12,22 @@

Debug Overrides

title="Load http://localhost:5555/onbaording_v5/ instead of the production https://ai-panel-onboarding.phcode.dev/onbaording_v5/. Reload Phoenix after toggling."> +
+ +
diff --git a/src/phoenix-builder/debug-overrides.js b/src/phoenix-builder/debug-overrides.js index d4c98a5a4a..961a15bf8b 100644 --- a/src/phoenix-builder/debug-overrides.js +++ b/src/phoenix-builder/debug-overrides.js @@ -43,6 +43,7 @@ define(function (require, exports, module) { } const CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), Dialogs = require("widgets/Dialogs"), Mustache = require("thirdparty/mustache/mustache"), OverridesTpl = require("text!./debug-overrides-dialog.html"); @@ -71,22 +72,49 @@ define(function (require, exports, module) { function _handleDebugOverrides() { const overrides = _readOverrides(); - let aiPanelLocalOverride = !!overrides.AI_PANEL_LOCAL_OVERRIDE; + const persistedAiOverride = !!overrides.AI_PANEL_LOCAL_OVERRIDE; + const persistedAccountsOverride = overrides.ACCOUNTS_SERVER_OVERRIDE || ""; + let aiPanelLocalOverride = persistedAiOverride; + let accountsServerOverride = persistedAccountsOverride; + let needsReload = false; const html = Mustache.render(OverridesTpl, { - aiPanelLocalOverride: aiPanelLocalOverride + aiPanelLocalOverride: aiPanelLocalOverride, + accountsStaging: accountsServerOverride === "staging", + accountsDev: accountsServerOverride === "dev" }); Dialogs.showModalDialogUsingTemplate(html).done(function (id) { if (id !== Dialogs.DIALOG_BTN_OK) { return; } const next = _readOverrides(); next.AI_PANEL_LOCAL_OVERRIDE = aiPanelLocalOverride; + if (accountsServerOverride) { + next.ACCOUNTS_SERVER_OVERRIDE = accountsServerOverride; + } else { + delete next.ACCOUNTS_SERVER_OVERRIDE; // production default keeps the blob clean + } _writeOverrides(next); + if (needsReload) { + CommandManager.execute(Commands.APP_RELOAD); + } }); const $dialog = $(".phoenix-debug-overrides.instance"); + + // all current overrides are read at boot, so a save only needs a reload + // when a value actually changed from what is persisted + function _updateSaveButton() { + needsReload = (aiPanelLocalOverride !== persistedAiOverride) || + (accountsServerOverride !== persistedAccountsOverride); + $dialog.find(".debug-overrides-save-btn").text(needsReload ? "Save & Reload" : "Save"); + } $dialog.find(".ai-panel-local-override").on("change", function () { aiPanelLocalOverride = $(this).is(":checked"); + _updateSaveButton(); + }); + $dialog.find(".accounts-server-override").on("change", function () { + accountsServerOverride = $(this).val() || ""; + _updateSaveButton(); }); } diff --git a/tracking-repos.json b/tracking-repos.json index 89421c443f..68ead37bc3 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "1ded0ad017bd0fe0e69503b0a068198da2fdc11a" + "commitID": "78b16c2b0db7ce924ac55e5e3cbd5489c3758004" } }