Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/API-Reference/command/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,12 @@ Opens Phoenix Pro page
## HELP\_CANCEL\_TRIAL
Cancels Phoenix Pro trial

**Kind**: global variable
<a name="HELP_DISABLE_OFF_HOURS"></a>

## HELP\_DISABLE\_OFF\_HOURS
Toggles the Pro off-hours offer (label uses server-vended brand name)

**Kind**: global variable
<a name="HELP_VIEW_LICENSE"></a>

Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
158 changes: 52 additions & 106 deletions serve-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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];
Expand All @@ -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
Expand All @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
}
});
Expand Down
3 changes: 3 additions & 0 deletions src/command/Commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
29 changes: 17 additions & 12 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
24 changes: 24 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. <a href=\"{1}\">Get {2}</a> to keep Pro all day.",
"OFF_HOURS_ENDED_TITLE": "{0} has ended",
"OFF_HOURS_ENDED_MSG": "Your free {0} session has ended. <a href=\"{1}\">Get {2}</a> 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 — <b>{1}</b>, 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",
Expand Down
15 changes: 14 additions & 1 deletion src/phoenix-builder/debug-overrides-dialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,22 @@ <h1 class="dialog-title">Debug Overrides</h1>
title="Load http://localhost:5555/onbaording_v5/ instead of the production https://ai-panel-onboarding.phcode.dev/onbaording_v5/. Reload Phoenix after toggling."></i>
</label>
</div>
<div style="margin-bottom: 6px;">
<label style="display: flex; align-items: center; gap: 8px;">
<span>Accounts server</span>
<select class="accounts-server-override" style="margin: 0;">
<option value="">Production (account.phcode.dev)</option>
<option value="staging" {{#accountsStaging}}selected{{/accountsStaging}}>Staging (account-stage.phcode.dev)</option>
<option value="dev" {{#accountsDev}}selected{{/accountsDev}}>Local dev (localhost:5000)</option>
</select>
<i class="fa-solid fa-circle-info"
style="opacity: 0.5; cursor: help; font-size: 12px;"
title="Which accounts server the app talks to. Local dev expects loginService `npm run serve` on localhost:5000. Notes: desktop auto-auth only works against production (manual OTP on staging/dev); staging login in the browser may need its secure session cookie copied manually."></i>
</label>
</div>
</div>
<div class="modal-footer">
<button class="dialog-button btn" data-button-id="cancel">Cancel</button>
<button class="dialog-button btn primary" data-button-id="ok">Save</button>
<button class="dialog-button btn primary debug-overrides-save-btn" data-button-id="ok">Save</button>
</div>
</div>
32 changes: 30 additions & 2 deletions src/phoenix-builder/debug-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
});
}

Expand Down
Loading
Loading