Browse Source

fix(settings): resolve server switching not applying immediately

- Fix database query logic in PlatformServiceMixin.$getSettings() to properly
  distinguish between master settings (ID) and account settings (DID)
- Add comprehensive logging for settings debugging with request IDs and
  component tracking
- Fix ProfileService initialization order in AccountViewView to use correct
  partnerApiServer after settings load
- Add URL flow testing interface in TestView for debugging server switching
- Enhance settings consistency validation and error handling

Resolves issue where profile server changes were saved but not applied due to
incorrect database query logic and settings priority handling.

Files changed: PlatformServiceMixin.ts, AccountViewView.vue, TestView.vue,
TopMessage.vue, main.ts, router/index.ts

Testing: Added comprehensive URL flow testing interface for validation
pull/170/head
Matthew Raymer 2 weeks ago
parent
commit
fd30343ec4
  1. 33
      src/components/TopMessage.vue
  2. 9
      src/main.ts
  3. 16
      src/router/index.ts
  4. 29
      src/views/AccountViewView.vue
  5. 455
      src/views/TestView.vue

33
src/components/TopMessage.vue

@ -18,6 +18,7 @@ import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import { logger } from "../utils/logger";
@Component({
mixins: [PlatformServiceMixin],
@ -29,6 +30,7 @@ export default class TopMessage extends Vue {
// - Cache management: this.$refreshSettings(), this.$clearAllCaches()
// - Ultra-concise database methods: this.$db(), this.$exec(), this.$query()
// - All methods use smart caching with TTL for massive performance gains
// - FIXED: Now properly respects database settings without forcing API server overrides
$notify!: (notification: NotificationIface, timeout?: number) => void;
@ -42,26 +44,49 @@ export default class TopMessage extends Vue {
this.notify = createNotifyHelpers(this.$notify);
try {
// Ultra-concise cached settings loading - replaces 50+ lines of logic!
const settings = await this.$accountSettings(undefined, {
activeDid: undefined,
apiServer: AppString.PROD_ENDORSER_API_SERVER,
// Load settings without overriding database values - fixes settings inconsistency
logger.info("[TopMessage] 📥 Loading settings without overrides...");
const settings = await this.$accountSettings();
logger.info("[TopMessage] 📊 Settings loaded:", {
activeDid: settings.activeDid,
apiServer: settings.apiServer,
warnIfTestServer: settings.warnIfTestServer,
warnIfProdServer: settings.warnIfProdServer,
component: "TopMessage",
timestamp: new Date().toISOString(),
});
// Only show warnings if the user has explicitly enabled them
if (
settings.warnIfTestServer &&
settings.apiServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You're not using prod, user " + didPrefix;
logger.info("[TopMessage] ⚠️ Test server warning displayed:", {
apiServer: settings.apiServer,
didPrefix: didPrefix,
});
} else if (
settings.warnIfProdServer &&
settings.apiServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You are using prod, user " + didPrefix;
logger.info("[TopMessage] ⚠️ Production server warning displayed:", {
apiServer: settings.apiServer,
didPrefix: didPrefix,
});
} else {
logger.debug(
"[TopMessage] ℹ️ No warnings displayed - conditions not met",
);
}
} catch (err: unknown) {
logger.error("[TopMessage] ❌ Error loading settings:", err);
this.notify.error(JSON.stringify(err), TIMEOUTS.MODAL);
}
}

9
src/main.ts

@ -13,6 +13,15 @@ const platform = process.env.VITE_PLATFORM || "web";
logger.info(`[Main] 🚀 Loading TimeSafari for platform: ${platform}`);
// Log all relevant environment variables for boot-time debugging
logger.info("[Main] 🌍 Boot-time environment configuration:", {
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
timestamp: new Date().toISOString(),
});
// Dynamically import the appropriate main entry point
if (platform === "capacitor") {
logger.info(`[Main] 📱 Loading Capacitor-specific entry point`);

16
src/router/index.ts

@ -337,6 +337,22 @@ router.beforeEach(async (to, _from, next) => {
});
try {
// Log boot-time configuration on first navigation
if (!_from) {
logger.info(
"[Router] 🚀 First navigation detected - logging boot-time configuration:",
{
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer:
process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
targetRoute: to.path,
timestamp: new Date().toISOString(),
},
);
}
// Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [
"/start",

29
src/views/AccountViewView.vue

@ -58,7 +58,10 @@
v-if="!isRegistered"
:passkeys-enabled="PASSKEYS_ENABLED"
:given-name="givenName"
message="Before you can publicly announce a new project or time commitment, a friend needs to register you."
:message="
`Before you can publicly announce a new project or time commitment, ` +
`a friend needs to register you.`
"
/>
<!-- Notifications -->
@ -925,7 +928,10 @@ export default class AccountViewView extends Vue {
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
if (L.Icon.Default) {
// Type-safe way to handle Leaflet icon prototype
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
const iconDefault = L.Icon.Default.prototype as unknown as Record<
string,
unknown
>;
if ("_getIconUrl" in iconDefault) {
delete iconDefault._getIconUrl;
}
@ -947,14 +953,25 @@ export default class AccountViewView extends Vue {
* @throws Will display specific messages to the user based on different errors.
*/
async mounted(): Promise<void> {
this.profileService = createProfileService(
this.axios,
this.partnerApiServer,
);
try {
await this.initializeState();
await this.processIdentity();
// FIXED: Create ProfileService AFTER settings are loaded to get correct partnerApiServer
this.profileService = createProfileService(
this.axios,
this.partnerApiServer,
);
logger.info(
"[AccountViewView] ✅ ProfileService created with correct partnerApiServer:",
{
partnerApiServer: this.partnerApiServer,
component: "AccountViewView",
timestamp: new Date().toISOString(),
},
);
if (this.isRegistered) {
try {
const profile = await this.profileService.loadProfile(this.activeDid);

455
src/views/TestView.vue

@ -91,13 +91,95 @@
name: 'shared-photo',
query: { fileName },
}"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
data-testId="fileUploadButton"
>
Go to Shared Page
</router-link>
</div>
<!-- URL Flow Testing Section -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">URL Flow Testing</h2>
<p class="text-sm text-gray-600 mb-3">
Test claim and partner server URL flow from initialization to change
propagation.
</p>
<div class="space-y-4">
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">Current URL State</h3>
<div class="space-y-2 text-sm">
<div>
<strong>API Server:</strong>
<span class="font-mono">{{ apiServer || "Not Set" }}</span>
</div>
<div>
<strong>Partner API Server:</strong>
<span class="font-mono">{{ partnerApiServer || "Not Set" }}</span>
</div>
<div>
<strong>Active DID:</strong>
<span class="font-mono">{{ activeDid || "Not Set" }}</span>
</div>
<div>
<strong>Platform:</strong>
<span class="font-mono">{{ getCurrentPlatform() }}</span>
</div>
</div>
</div>
<div class="space-y-2">
<button
:class="primaryButtonClasses"
:disabled="isUrlTestRunning"
@click="testUrlFlow()"
>
{{ isUrlTestRunning ? "Testing..." : "Test URL Flow" }}
</button>
<button :class="secondaryButtonClasses" @click="changeApiServer()">
Change API Server (Test Prod)
</button>
<button
:class="secondaryButtonClasses"
@click="changePartnerApiServer()"
>
Change Partner API Server (Test Prod)
</button>
<button :class="warningButtonClasses" @click="resetToDefaults()">
Reset to Defaults
</button>
<button :class="secondaryButtonClasses" @click="refreshSettings()">
Refresh Settings
</button>
<button
:class="secondaryButtonClasses"
@click="logEnvironmentState()"
>
Log Environment State
</button>
</div>
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">URL Flow Test Results</h3>
<div class="max-h-64 overflow-y-auto space-y-2">
<div
v-for="(result, index) in urlTestResults"
:key="index"
class="p-2 border border-gray-200 rounded text-xs font-mono bg-white"
>
{{ result }}
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results.
@ -326,6 +408,11 @@ export default class Help extends Vue {
showEntityGridTest = false;
showPlatformServiceTest = false;
// for URL flow testing
isUrlTestRunning = false;
urlTestResults: string[] = [];
partnerApiServer: string | undefined;
/**
* Computed properties for template streamlining
* Eliminates repeated classes and logic in template
@ -534,24 +621,93 @@ export default class Help extends Vue {
}
/**
* Component initialization
*
* Loads user settings and account information for testing interface
* Uses PlatformServiceMixin for database access
*/
async mounted() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName;
const account = await retrieveAccountMetadata(this.activeDid);
if (this.activeDid) {
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
} else {
alert("No account found for DID " + this.activeDid);
logger.info(
"[TestView] 🚀 Component mounting - starting URL flow tracking",
);
// Boot-time logging for initial configuration
logger.info("[TestView] 🌍 Boot-time configuration detected:", {
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
timestamp: new Date().toISOString(),
});
try {
// Track settings loading
logger.info("[TestView] 📥 Loading account settings...");
const settings = await this.$accountSettings();
logger.info("[TestView] 📊 Settings loaded:", {
activeDid: settings.activeDid,
apiServer: settings.apiServer,
partnerApiServer: settings.partnerApiServer,
isRegistered: settings.isRegistered,
firstName: settings.firstName,
});
// Update component state
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
this.userName = settings.firstName;
logger.info("[TestView] ✅ Component state updated:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
// Load account metadata
if (this.activeDid) {
logger.info(
"[TestView] 🔍 Loading account metadata for DID:",
this.activeDid,
);
const account = await retrieveAccountMetadata(this.activeDid);
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
logger.info("[TestView] ✅ Account metadata loaded:", {
did: account.did,
hasPasskey: !!account.passkeyCredIdHex,
passkeyId: account.passkeyCredIdHex,
});
} else {
logger.warn(
"[TestView] ⚠️ No account found for DID:",
this.activeDid,
);
alert("No account found for DID " + this.activeDid);
}
}
logger.info("[TestView] 🎯 Component initialization complete:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
hasPasskey: !!this.credIdHex,
platform: this.getCurrentPlatform(),
});
} catch (error) {
logger.error(
"[TestView] ❌ Error during component initialization:",
error,
);
this.$notify(
{
group: "error",
type: "error",
title: "Initialization Error",
text: `Failed to initialize component: ${error instanceof Error ? error.message : String(error)}`,
},
5000,
);
}
}
@ -824,5 +980,276 @@ export default class Help extends Vue {
);
}
}
/**
* Tests the URL flow from initialization to change propagation.
* This simulates the flow where a user's DID is set, and then the
* claim and partner server URLs are updated.
*/
public async testUrlFlow() {
this.isUrlTestRunning = true;
this.urlTestResults = [];
try {
logger.info("[TestView] 🔬 Starting comprehensive URL flow test");
this.addUrlTestResult("🚀 Starting URL flow test...");
// Test 1: Current state
this.addUrlTestResult(`📊 Current State:`);
this.addUrlTestResult(` - API Server: ${this.apiServer || "Not Set"}`);
this.addUrlTestResult(
` - Partner API Server: ${this.partnerApiServer || "Not Set"}`,
);
this.addUrlTestResult(` - Active DID: ${this.activeDid || "Not Set"}`);
this.addUrlTestResult(` - Platform: ${this.getCurrentPlatform()}`);
// Test 2: Load fresh settings
this.addUrlTestResult(`\n📥 Testing Settings Loading:`);
const startTime = Date.now();
const settings = await this.$accountSettings();
const loadTime = Date.now() - startTime;
this.addUrlTestResult(` - Settings loaded in ${loadTime}ms`);
this.addUrlTestResult(
` - API Server from settings: ${settings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Partner API Server from settings: ${settings.partnerApiServer || "Not Set"}`,
);
// Test 3: Database query
this.addUrlTestResult(`\n💾 Testing Database Query:`);
const dbStartTime = Date.now();
const dbResult = await this.$dbQuery(
"SELECT apiServer, partnerApiServer, activeDid FROM settings WHERE id = ? OR accountDid = ?",
[1, this.activeDid || ""],
);
const dbTime = Date.now() - dbStartTime;
if (dbResult?.values) {
this.addUrlTestResult(` - Database query completed in ${dbTime}ms`);
this.addUrlTestResult(
` - Raw DB values: ${JSON.stringify(dbResult.values)}`,
);
} else {
this.addUrlTestResult(
` - Database query failed or returned no results`,
);
}
// Test 4: Environment variables
this.addUrlTestResult(`\n🌍 Testing Environment Variables:`);
this.addUrlTestResult(
` - VITE_PLATFORM: ${import.meta.env.VITE_PLATFORM || "Not Set"}`,
);
this.addUrlTestResult(
` - VITE_DEFAULT_ENDORSER_API_SERVER: ${import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER || "Not Set"}`,
);
this.addUrlTestResult(
` - VITE_DEFAULT_PARTNER_API_SERVER: ${import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER || "Not Set"}`,
);
// Test 5: Constants
this.addUrlTestResult(`\n📋 Testing App Constants:`);
this.addUrlTestResult(
` - PROD_ENDORSER_API_SERVER: ${AppString.PROD_ENDORSER_API_SERVER}`,
);
this.addUrlTestResult(
` - PROD_PARTNER_API_SERVER: ${AppString.PROD_PARTNER_API_SERVER}`,
);
// Test 6: Change detection
this.addUrlTestResult(`\n🔄 Testing Change Detection:`);
const originalApiServer = this.apiServer;
const originalPartnerServer = this.partnerApiServer;
// Simulate a change
this.addUrlTestResult(` - Original API Server: ${originalApiServer}`);
this.addUrlTestResult(
` - Original Partner Server: ${originalPartnerServer}`,
);
// Test 7: Settings update
this.addUrlTestResult(`\n💾 Testing Settings Update:`);
const testChanges = {
apiServer:
originalApiServer === "https://api.endorser.ch"
? "https://test-api.endorser.ch"
: "https://api.endorser.ch",
};
this.addUrlTestResult(
` - Attempting to change API Server to: ${testChanges.apiServer}`,
);
const updateResult = await this.$saveSettings(testChanges);
this.addUrlTestResult(
` - Update result: ${updateResult ? "Success" : "Failed"}`,
);
// Test 8: Verify change propagation
this.addUrlTestResult(`\n✅ Testing Change Propagation:`);
const newSettings = await this.$accountSettings();
this.addUrlTestResult(
` - New API Server from settings: ${newSettings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Component state API Server: ${this.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Change propagated: ${newSettings.apiServer === this.apiServer ? "Yes" : "No"}`,
);
// Test 9: Revert changes
this.addUrlTestResult(`\n🔄 Reverting Changes:`);
const revertResult = await this.$saveSettings({
apiServer: originalApiServer,
});
this.addUrlTestResult(
` - Revert result: ${revertResult ? "Success" : "Failed"}`,
);
// Test 10: Final verification
this.addUrlTestResult(`\n🎯 Final Verification:`);
const finalSettings = await this.$accountSettings();
this.addUrlTestResult(
` - Final API Server: ${finalSettings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Matches original: ${finalSettings.apiServer === originalApiServer ? "Yes" : "No"}`,
);
this.addUrlTestResult(`\n✅ URL flow test completed successfully!`);
logger.info("[TestView] ✅ URL flow test completed successfully");
} catch (error) {
const errorMsg = `❌ URL flow test failed: ${error instanceof Error ? error.message : String(error)}`;
this.addUrlTestResult(errorMsg);
logger.error("[TestView] ❌ URL flow test failed:", error);
} finally {
this.isUrlTestRunning = false;
}
}
/**
* Adds a result to the URL test results array.
*/
private addUrlTestResult(message: string) {
this.urlTestResults.push(message);
}
/**
* Changes the API server to the production URL.
*/
public changeApiServer() {
const currentServer = this.apiServer;
const newServer =
currentServer === "https://api.endorser.ch"
? "https://test-api.endorser.ch"
: "https://api.endorser.ch";
logger.info("[TestView] 🔄 Changing API server:", {
from: currentServer,
to: newServer,
});
this.apiServer = newServer;
this.addUrlTestResult(
`API Server changed from ${currentServer} to ${newServer}`,
);
}
/**
* Changes the partner API server to the production URL.
*/
public changePartnerApiServer() {
const currentServer = this.partnerApiServer;
const newServer =
currentServer === "https://partner-api.endorser.ch"
? "https://test-partner-api.endorser.ch"
: "https://partner-api.endorser.ch";
logger.info("[TestView] 🔄 Changing partner API server:", {
from: currentServer,
to: newServer,
});
this.partnerApiServer = newServer;
this.addUrlTestResult(
`Partner API Server changed from ${currentServer} to ${newServer}`,
);
}
/**
* Resets all URL-related settings to their initial values.
*/
public resetToDefaults() {
this.apiServer = AppString.TEST_ENDORSER_API_SERVER;
this.partnerApiServer = AppString.TEST_PARTNER_API_SERVER;
this.activeDid = "";
this.addUrlTestResult("URL Flow Test Results Reset to Defaults.");
}
/**
* Refreshes settings from the database to verify changes.
*/
public async refreshSettings() {
try {
logger.info("[TestView] 🔄 Refreshing settings from database");
const settings = await this.$accountSettings();
// Update component state
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
logger.info("[TestView] ✅ Settings refreshed:", {
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
this.addUrlTestResult(
`Settings refreshed - API Server: ${this.apiServer}, Partner API Server: ${this.partnerApiServer}`,
);
} catch (error) {
logger.error("[TestView] ❌ Error refreshing settings:", error);
this.addUrlTestResult(
`Error refreshing settings: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Logs the current environment state to the console.
*/
public logEnvironmentState() {
logger.info("[TestView] 🌐 Current Environment State:", {
VITE_PLATFORM: import.meta.env.VITE_PLATFORM,
VITE_DEFAULT_ENDORSER_API_SERVER: import.meta.env
.VITE_DEFAULT_ENDORSER_API_SERVER,
VITE_DEFAULT_PARTNER_API_SERVER: import.meta.env
.VITE_DEFAULT_PARTNER_API_SERVER,
NODE_ENV: process.env.NODE_ENV,
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
this.$notify({
group: "info",
type: "info",
title: "Environment State Logged",
text: "Current environment state logged to console.",
});
}
/**
* Gets the current platform based on the API server.
*/
public getCurrentPlatform(): string {
if (this.apiServer?.includes(AppString.PROD_ENDORSER_API_SERVER)) {
return "Production";
} else if (this.apiServer?.includes(AppString.TEST_ENDORSER_API_SERVER)) {
return "Test";
} else {
return "Unknown";
}
}
}
</script>

Loading…
Cancel
Save