Skip to content
Open
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
7 changes: 7 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ src/
Public.vue Edit own profile (name, email, avatar)
Peers.vue Peer management (currently hidden)
Restart.vue Redirect target after shard restart
lib/ Framework-free pure functions, unit-tested from tests/unit/
errors.js errorMessage(): displayable string from an axios error
pricing.js Shard price computation
components/ 13 reusable UI components
Navbar.vue Sticky nav with feedback modal, version update notification, disk warnings
AppIcon.vue App launcher tile with status indicator
Expand All @@ -58,6 +61,10 @@ All API calls go to the shard_core backend. Two base paths:

Dev proxy: `vue.config.js` proxies requests to a remote shard or `localhost:8080`.

Backend errors carry the reason in the response body's `detail` field. Render caught errors
through the `errorMessage` filter (`{{ err | errorMessage }}`) rather than binding the error
object directly — binding it directly renders `AxiosError: Received HTTP status 401`.

### Authentication Flow
1. App loads → calls `/core/public/meta/whoami`
2. If anonymous → redirect to `/welcome` or `/pair`
Expand Down
7 changes: 7 additions & 0 deletions src/lib/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function errorMessage(error) {
const data = error && error.response && error.response.data;
if (data) {
return data.detail || data.error || data.message || data;
}
return (error && error.message) || error;
}
5 changes: 2 additions & 3 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'vue-tour/dist/vue-tour.css'
import './assets/css/main.css'
import QrcodeVue from "qrcode.vue";
import moment from "moment/moment";
import { errorMessage } from './lib/errors';

Vue.use(BootstrapVue)
Vue.use(BootstrapVueIcons)
Expand Down Expand Up @@ -38,9 +39,7 @@ Vue.filter('formatDateHumanize', function (value) {
return duration.humanize(true);
})

Vue.filter('errorMessage', function (error) {
return error.response.data.detail || error.response.data.error || error.response.data.message || error.response.data || error.message || error;
})
Vue.filter('errorMessage', errorMessage)

new Vue({
router,
Expand Down
2 changes: 1 addition & 1 deletion src/views/Pair.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
</b-form>
</div>

<b-alert v-model="show_error" dismissible variant="danger">{{ pairing_error }}</b-alert>
<b-alert v-model="show_error" dismissible variant="danger">{{ pairing_error | errorMessage }}</b-alert>

</b-col>
<b-col></b-col>
Expand Down
2 changes: 1 addition & 1 deletion src/views/Terminals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<b-modal id="new-terminal-modal" title="Pair new terminal" hide-footer @hidden="stopPairing">
<b-spinner v-if="pairing.loading"></b-spinner>
<p v-else-if="pairing.error">
{{ pairing.error }}
{{ pairing.error | errorMessage }}
</p>
<div v-else-if="pairingCodeValidityProgress===0">
<p>Pairing code expired</p>
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/errors.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { errorMessage } from '@/lib/errors';

describe('errorMessage', () => {
test('prefers the detail field of an API error response', () => {
const error = {
message: 'Request failed with status code 401',
response: {data: {detail: 'This pairing code has expired.'}},
};
expect(errorMessage(error)).toBe('This pairing code has expired.');
});

test('falls back to error/message fields of the response body', () => {
expect(errorMessage({response: {data: {error: 'boom'}}})).toBe('boom');
expect(errorMessage({response: {data: {message: 'boom'}}})).toBe('boom');
});

test('uses a plain-string response body as-is', () => {
expect(errorMessage({response: {data: 'Bad Gateway'}})).toBe('Bad Gateway');
});

test('falls back to the axios message when there is no response', () => {
expect(errorMessage({message: 'Network Error'})).toBe('Network Error');
});

test('handles an error with neither response nor message', () => {
expect(errorMessage('kaboom')).toBe('kaboom');
});
});
Loading