Browse Source

fix: remaining starred-project issues, plus better Error logging and user verbiage

pull/203/head
Trent Larson 2 weeks ago
parent
commit
ee587ac3fc
  1. 9
      src/components/DataExportSection.vue
  2. 2
      src/components/TopMessage.vue
  3. 2
      src/constants/accountView.ts
  4. 29
      src/db/databaseUtil.ts
  5. 41
      src/libs/endorserServer.ts
  6. 7
      src/services/api.ts
  7. 20
      src/utils/PlatformServiceMixin.ts
  8. 35
      src/utils/logger.ts
  9. 6
      src/views/AccountViewView.vue
  10. 2
      src/views/ContactAmountsView.vue
  11. 29
      src/views/HomeView.vue
  12. 24
      src/views/ProjectViewView.vue

9
src/components/DataExportSection.vue

@ -18,7 +18,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
> >
<!-- Notification dot - show while the user has not yet backed up their seed phrase --> <!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome <font-awesome
v-if="!hasBackedUpSeed" v-if="showRedNotificationDot"
icon="circle" icon="circle"
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full" class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
></font-awesome> ></font-awesome>
@ -108,7 +108,7 @@ export default class DataExportSection extends Vue {
* Flag indicating if the user has backed up their seed phrase * Flag indicating if the user has backed up their seed phrase
* Used to control the visibility of the notification dot * Used to control the visibility of the notification dot
*/ */
hasBackedUpSeed = false; showRedNotificationDot = false;
/** /**
* Notification helper for consistent notification patterns * Notification helper for consistent notification patterns
@ -240,11 +240,12 @@ export default class DataExportSection extends Vue {
private async loadSeedBackupStatus(): Promise<void> { private async loadSeedBackupStatus(): Promise<void> {
try { try {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.hasBackedUpSeed = !!settings.hasBackedUpSeed; this.showRedNotificationDot =
!!settings.isRegistered && !settings.hasBackedUpSeed;
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Failed to load seed backup status:", err); logger.error("Failed to load seed backup status:", err);
// Default to false (show notification dot) if we can't load the setting // Default to false (show notification dot) if we can't load the setting
this.hasBackedUpSeed = false; this.showRedNotificationDot = false;
} }
} }
} }

2
src/components/TopMessage.vue

@ -27,7 +27,7 @@ import { logger } from "../utils/logger";
}) })
export default class TopMessage extends Vue { export default class TopMessage extends Vue {
// Enhanced PlatformServiceMixin v4.0 provides: // Enhanced PlatformServiceMixin v4.0 provides:
// - Cached database operations: this.$contacts(), this.$settings(), this.$accountSettings() // - Cached database operations: this.$contacts(), this.$accountSettings()
// - Settings shortcuts: this.$saveSettings() // - Settings shortcuts: this.$saveSettings()
// - Cache management: this.$refreshSettings(), this.$clearAllCaches() // - Cache management: this.$refreshSettings(), this.$clearAllCaches()
// - Ultra-concise database methods: this.$db(), this.$exec(), this.$query() // - Ultra-concise database methods: this.$db(), this.$exec(), this.$query()

2
src/constants/accountView.ts

@ -86,7 +86,7 @@ export const ACCOUNT_VIEW_CONSTANTS = {
CANNOT_UPLOAD_IMAGES: "You cannot upload images.", CANNOT_UPLOAD_IMAGES: "You cannot upload images.",
BAD_SERVER_RESPONSE: "Bad server response.", BAD_SERVER_RESPONSE: "Bad server response.",
ERROR_RETRIEVING_LIMITS: ERROR_RETRIEVING_LIMITS:
"No limits were found, so no actions are allowed. You will need to get registered.", "No limits were found, so no actions are allowed. You need to get registered.",
}, },
// Project assignment errors // Project assignment errors

29
src/db/databaseUtil.ts

@ -9,34 +9,6 @@ import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QueryExecResult } from "@/interfaces/database"; import { QueryExecResult } from "@/interfaces/database";
export async function updateDefaultSettings(
settingsChanges: Settings,
): Promise<boolean> {
delete settingsChanges.accountDid; // just in case
// ensure there is no "id" that would override the key
delete settingsChanges.id;
try {
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = generateUpdateStatement(
settingsChanges,
"settings",
"id = ?",
[MASTER_SETTINGS_KEY],
);
const result = await platformService.dbExec(sql, params);
return result.changes === 1;
} catch (error) {
logger.error("Error updating default settings:", error);
if (error instanceof Error) {
throw error; // Re-throw if it's already an Error with a message
} else {
throw new Error(
`Failed to update settings. We recommend you try again or restart the app.`,
);
}
}
}
export async function insertDidSpecificSettings( export async function insertDidSpecificSettings(
did: string, did: string,
settings: Partial<Settings> = {}, settings: Partial<Settings> = {},
@ -91,6 +63,7 @@ export async function updateDidSpecificSettings(
? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0] ? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0]
: null; : null;
// Note that we want to eliminate this check (and fix the above if it doesn't work).
// Check if any of the target fields were actually changed // Check if any of the target fields were actually changed
let actuallyUpdated = false; let actuallyUpdated = false;
if (currentRecord && updatedRecord) { if (currentRecord && updatedRecord) {

41
src/libs/endorserServer.ts

@ -809,6 +809,8 @@ export async function getStarredProjectsWithChanges(
} }
if (!afterId) { if (!afterId) {
// This doesn't make sense: there should always be some previous one they've seen.
// We'll just return blank.
return { data: [], hitLimit: false }; return { data: [], hitLimit: false };
} }
@ -1756,7 +1758,7 @@ export async function fetchEndorserRateLimits(
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
try { // not wrapped in a 'try' because the error returned is self-explanatory
const response = await axios.get(url, { headers } as AxiosRequestConfig); const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful registration check // Log successful registration check
@ -1769,36 +1771,6 @@ export async function fetchEndorserRateLimits(
}); });
return response; return response;
} catch (error) {
// Enhanced error logging with user registration context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
const errorCode = axiosError.response?.data?.error?.code;
const errorMessage = axiosError.response?.data?.error?.message;
const httpStatus = axiosError.response?.status;
logger.warn("[User Registration] User not registered on server:", {
did: issuerDid,
server: apiServer,
errorCode: errorCode,
errorMessage: errorMessage,
httpStatus: httpStatus,
needsRegistration: true,
timestamp: new Date().toISOString(),
});
// Log the original error for debugging
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
} }
/** /**
@ -1847,14 +1819,17 @@ export async function fetchImageRateLimits(
}; };
}; };
logger.error("[Image Server] Image rate limits check failed:", { logger.warn(
"[Image Server] Image rate limits check failed, which is expected for users not registered on test server (eg. when only registered on local server).",
{
did: issuerDid, did: issuerDid,
server: server, server: server,
errorCode: axiosError.response?.data?.error?.code, errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message, errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status, httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); },
);
return null; return null;
} }
} }

7
src/services/api.ts

@ -19,7 +19,6 @@ import { logger, safeStringify } from "../utils/logger";
* @remarks * @remarks
* Special handling includes: * Special handling includes:
* - Enhanced logging for Capacitor platform * - Enhanced logging for Capacitor platform
* - Rate limit detection and handling
* - Detailed error information logging including: * - Detailed error information logging including:
* - Error message * - Error message
* - HTTP status * - HTTP status
@ -50,11 +49,5 @@ export const handleApiError = (error: AxiosError, endpoint: string) => {
}); });
} }
// Specific handling for rate limits
if (error.response?.status === 400) {
logger.warn(`[Rate Limit] ${endpoint}`);
return null;
}
throw error; throw error;
}; };

20
src/utils/PlatformServiceMixin.ts

@ -353,6 +353,14 @@ export const PlatformServiceMixin = {
? JSON.stringify(settings.searchBoxes) ? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes); : String(settings.searchBoxes);
} }
if (settings.starredPlanHandleIds !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).starredPlanHandleIds = Array.isArray(
settings.starredPlanHandleIds,
)
? JSON.stringify(settings.starredPlanHandleIds)
: String(settings.starredPlanHandleIds);
}
return converted; return converted;
}, },
@ -555,6 +563,12 @@ export const PlatformServiceMixin = {
if (settings.searchBoxes) { if (settings.searchBoxes) {
settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []); settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []);
} }
if (settings.starredPlanHandleIds) {
settings.starredPlanHandleIds = this._parseJsonField(
settings.starredPlanHandleIds,
[],
);
}
return settings; return settings;
} catch (error) { } catch (error) {
@ -621,6 +635,12 @@ export const PlatformServiceMixin = {
[], [],
); );
} }
if (mergedSettings.starredPlanHandleIds) {
mergedSettings.starredPlanHandleIds = this._parseJsonField(
mergedSettings.starredPlanHandleIds,
[],
);
}
return mergedSettings; return mergedSettings;
} catch (error) { } catch (error) {

35
src/utils/logger.ts

@ -24,10 +24,28 @@ export function getMemoryLogs(): string[] {
return [..._memoryLogs]; return [..._memoryLogs];
} }
/**
* Stringify an object with proper handling of circular references and functions
*
* Don't use for arrays; map with this over the array.
*
* @param obj - The object to stringify
* @returns The stringified object, plus 'message' and 'stack' for Error objects
*/
export function safeStringify(obj: unknown) { export function safeStringify(obj: unknown) {
const seen = new WeakSet(); const seen = new WeakSet();
return JSON.stringify(obj, (_key, value) => { // since 'message' & 'stack' are not enumerable for errors, let's add those
let objToStringify = obj;
if (obj instanceof Error) {
objToStringify = {
...obj,
message: obj.message,
stack: obj.stack,
};
}
return JSON.stringify(objToStringify, (_key, value) => {
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null) {
if (seen.has(value)) { if (seen.has(value)) {
return "[Circular]"; return "[Circular]";
@ -178,7 +196,8 @@ export const logger = {
} }
// Database logging // Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "info"); logToDatabase(message + argsString, "info");
}, },
@ -189,7 +208,8 @@ export const logger = {
} }
// Database logging // Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "info"); logToDatabase(message + argsString, "info");
}, },
@ -200,7 +220,8 @@ export const logger = {
} }
// Database logging // Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "warn"); logToDatabase(message + argsString, "warn");
}, },
@ -211,9 +232,9 @@ export const logger = {
} }
// Database logging // Database logging
const messageString = safeStringify(message); const argsString =
const argsString = args.length > 0 ? safeStringify(args) : ""; args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(messageString + argsString, "error"); logToDatabase(message + argsString, "error");
}, },
// New database-focused methods (self-contained) // New database-focused methods (self-contained)

6
src/views/AccountViewView.vue

@ -1453,9 +1453,6 @@ export default class AccountViewView extends Vue {
if (imageResp && imageResp.status === 200) { if (imageResp && imageResp.status === 200) {
this.imageLimits = imageResp.data; this.imageLimits = imageResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
} }
const endorserResp = await fetchEndorserRateLimits( const endorserResp = await fetchEndorserRateLimits(
@ -1466,9 +1463,6 @@ export default class AccountViewView extends Vue {
if (endorserResp.status === 200) { if (endorserResp.status === 200) {
this.endorserLimits = endorserResp.data; this.endorserLimits = endorserResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
} }
} catch (error) { } catch (error) {
this.limitsMessage = this.limitsMessage =

2
src/views/ContactAmountsView.vue

@ -223,7 +223,7 @@ export default class ContactAmountssView extends Vue {
const contact = await this.$getContact(contactDid); const contact = await this.$getContact(contactDid);
this.contact = contact; this.contact = contact;
const settings = await this.$settings(); const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth) // Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

29
src/views/HomeView.vue

@ -421,6 +421,18 @@ export default class HomeView extends Vue {
numNewOffersToUser: number = 0; // number of new offers-to-user numNewOffersToUser: number = 0; // number of new offers-to-user
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
numNewStarredProjectChanges: number = 0; // number of new starred project changes
starredPlanHandleIds: Array<string> = []; // list of starred project IDs
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
isImageViewerOpen = false;
showProjectsDialog = false;
/** /**
* CRITICAL VUE REACTIVITY BUG WORKAROUND * CRITICAL VUE REACTIVITY BUG WORKAROUND
* *
@ -458,18 +470,6 @@ export default class HomeView extends Vue {
// return shouldShow; // return shouldShow;
// } // }
numNewStarredProjectChanges: number = 0; // number of new starred project changes
starredPlanHandleIds: Array<string> = []; // list of starred project IDs
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
isImageViewerOpen = false;
showProjectsDialog = false;
/** /**
* Initializes notification helpers * Initializes notification helpers
*/ */
@ -734,7 +734,7 @@ export default class HomeView extends Vue {
* Used for displaying contact info in feed and actions * Used for displaying contact info in feed and actions
* *
* @internal * @internal
* Called by mounted() and initializeIdentity() * Called by initializeIdentity()
*/ */
private async loadContacts() { private async loadContacts() {
this.allContacts = await this.$contacts(); this.allContacts = await this.$contacts();
@ -748,7 +748,6 @@ export default class HomeView extends Vue {
* Triggers updateAllFeed() to populate activity feed * Triggers updateAllFeed() to populate activity feed
* *
* @internal * @internal
* Called by mounted()
*/ */
private async loadFeedData() { private async loadFeedData() {
await this.updateAllFeed(); await this.updateAllFeed();
@ -762,7 +761,6 @@ export default class HomeView extends Vue {
* - Rate limit status for both * - Rate limit status for both
* *
* @internal * @internal
* Called by mounted() and initializeIdentity()
* @requires Active DID * @requires Active DID
*/ */
private async loadNewOffers() { private async loadNewOffers() {
@ -873,7 +871,6 @@ export default class HomeView extends Vue {
* - Rate limit status for starred project changes * - Rate limit status for starred project changes
* *
* @internal * @internal
* Called by mounted() and initializeIdentity()
* @requires Active DID * @requires Active DID
*/ */
private async loadNewStarredProjectChanges() { private async loadNewStarredProjectChanges() {

24
src/views/ProjectViewView.vue

@ -170,7 +170,7 @@
</div> </div>
</div> </div>
<a class="cursor-pointer" @click="onClickLoadClaim(projectId)"> <a class="cursor-pointer" @click="onClickLoadClaim(jwtId)">
<font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" /> <font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a> </a>
</div> </div>
@ -303,10 +303,7 @@
{{ offer.objectDescription }} {{ offer.objectDescription }}
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<a <a class="cursor-pointer" @click="onClickLoadClaim(offer.jwtId)">
class="cursor-pointer"
@click="onClickLoadClaim(offer.jwtId as string)"
>
<font-awesome <font-awesome
icon="file-lines" icon="file-lines"
class="pl-2 pt-1 text-blue-500" class="pl-2 pt-1 text-blue-500"
@ -606,7 +603,6 @@ import { AxiosError } from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render"; import VueMarkdown from "vue-markdown-render";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { import {
GenericVerifiableCredential, GenericVerifiableCredential,
@ -746,6 +742,8 @@ export default class ProjectViewView extends Vue {
} | null = null; } | null = null;
/** DIDs that can see issuer information */ /** DIDs that can see issuer information */
issuerVisibleToDids: Array<string> = []; issuerVisibleToDids: Array<string> = [];
/** Project JWT ID */
jwtId = "";
/** Project location data */ /** Project location data */
latitude = 0; latitude = 0;
loadingTotals = false; loadingTotals = false;
@ -910,6 +908,7 @@ export default class ProjectViewView extends Vue {
this.allContacts, this.allContacts,
); );
this.issuerVisibleToDids = resp.data.issuerVisibleToDids || []; this.issuerVisibleToDids = resp.data.issuerVisibleToDids || [];
this.jwtId = resp.data.id;
this.name = resp.data.claim?.name || "(no name)"; this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || ""; this.description = resp.data.claim?.description || "";
this.truncatedDesc = this.description.slice(0, this.truncateLength); this.truncatedDesc = this.description.slice(0, this.truncateLength);
@ -1509,11 +1508,10 @@ export default class ProjectViewView extends Vue {
if (!this.projectId) return; if (!this.projectId) return;
try { try {
const settings = await this.$accountSettings();
const starredIds = settings.starredPlanHandleIds || [];
if (!this.isStarred) { if (!this.isStarred) {
// Add to starred projects // Add to starred projects
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
const starredIds = settings.starredPlanHandleIds || [];
if (!starredIds.includes(this.projectId)) { if (!starredIds.includes(this.projectId)) {
const newStarredIds = [...starredIds, this.projectId]; const newStarredIds = [...starredIds, this.projectId];
const newIdsParam = JSON.stringify(newStarredIds); const newIdsParam = JSON.stringify(newStarredIds);
@ -1526,20 +1524,16 @@ export default class ProjectViewView extends Vue {
this.isStarred = true; this.isStarred = true;
} else { } else {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( logger.error("Got a bad result from SQL update to star a project.");
"Still getting a bad result from SQL update to star a project.",
);
} }
} }
if (!settings.lastAckedStarredPlanChangesJwtId) { if (!settings.lastAckedStarredPlanChangesJwtId) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedStarredPlanChangesJwtId: settings.lastViewedClaimId, lastAckedStarredPlanChangesJwtId: this.jwtId,
}); });
} }
} else { } else {
// Remove from starred projects // Remove from starred projects
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
const starredIds = settings.starredPlanHandleIds || [];
const updatedIds = starredIds.filter((id) => id !== this.projectId); const updatedIds = starredIds.filter((id) => id !== this.projectId);
const newIdsParam = JSON.stringify(updatedIds); const newIdsParam = JSON.stringify(updatedIds);

Loading…
Cancel
Save