Renamed error store to notification store, centralized spinner modal

Increased thread pool capacities in `AsyncConfig.java` and adjusted database schema for larger text fields. Enhanced HS code validation logic and added logging to `EUTaxationApiService` for improved traceability. Cleaned up unused error modal code and aligned styles for calculation processing spinners.
This commit is contained in:
Jan 2025-11-16 23:02:11 +01:00
parent 5adadb671e
commit cd66b5bba5
30 changed files with 173 additions and 132 deletions

View file

@ -1,5 +1,5 @@
import logger from "@/logger.js";
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import {config} from "@/config";
const getCsrfToken = () => {
@ -141,7 +141,7 @@ function handleErrorResponse(data, requestingStore, request) {
const error = new Error('Internal backend error');
error.errorObj = errorObj;
const errorStore = useErrorStore();
const errorStore = useNotificationStore();
if (request.expectedException === null || (Array.isArray(request.expectResponse) && !request.expectedException.includes(data.error.title)) || (typeof request.expectedException === 'string' && data.error.title !== request.expectedException)) {
logger.error(errorObj, request.expectedException);
@ -170,7 +170,7 @@ const executeRequest = async (requestingStore, request) => {
}
logger.error(error, e);
const errorStore = useErrorStore();
const errorStore = useNotificationStore();
void errorStore.addError(error, {store: requestingStore, request: request});
throw e;
@ -199,7 +199,7 @@ const executeRequest = async (requestingStore, request) => {
}
logger.error(error);
const errorStore = useErrorStore();
const errorStore = useNotificationStore();
void errorStore.addError(error, {store: requestingStore, request: request});
throw e;
}
@ -217,7 +217,7 @@ const executeRequest = async (requestingStore, request) => {
trace: null
}
logger.error(error);
const errorStore = useErrorStore();
const errorStore = useNotificationStore();
void errorStore.addError(error, {store: requestingStore, request: request});
throw new Error('Internal backend error');

View file

@ -1,54 +1,47 @@
<template>
<teleport to="body">
<toast ref="toast"></toast>
<!-- <transition-->
<!-- name="error-container"-->
<!-- tag="div"-->
<!-- class="error-notification-container">-->
<!-- <div class="error-notification" v-if="error">-->
<!-- <div>-->
<!-- <ph-warning size="24"></ph-warning>-->
<!-- </div>-->
<!-- <div class="error-message">-->
<!-- <div class="error-message-title">-->
<!-- {{ title }}-->
<!-- </div>-->
<!-- <div class="error-message-content">-->
<!-- {{ message }}-->
<!-- </div>-->
<!-- <div class="error-view-trace" v-if="trace" @click="activateTrace">-->
<!-- View trace-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="icon-error-notification">-->
<!-- <ph-x size="24" @click="close"></ph-x>-->
<!-- </div>-->
<!-- <modal :z-index="9001" :state="showTrace"><trace-view :error="error" @close="deactivateTrace"></trace-view></modal>-->
<!-- </div>-->
<!-- </transition>-->
<modal :z-index="9000" :state="hasSpinner">
<div class="spinner-box space-around">
<spinner></spinner>
<span>{{ spinnerMsg }}</span>
</div>
</modal>
</teleport>
</template>
<script>
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import {mapStores} from "pinia";
import Toast from "@/components/UI/Toast.vue";
import logger from "@/logger.js";
import Box from "@/components/UI/Box.vue";
import Spinner from "@/components/UI/Spinner.vue";
import Modal from "@/components/UI/Modal.vue";
export default {
name: "TheNotificationSystem",
components: {Toast},
components: {Modal, Spinner, Box, Toast},
data() {
return {}
},
computed: {
...mapStores(useErrorStore),
...mapStores(useNotificationStore),
hasSpinner() {
return this.notificationStore.hasSpinner;
},
spinnerMsg() {
return this.notificationStore.spinnerMsg;
},
notifications() {
return this.errorStore.notifications;
return this.notificationStore.notifications;
},
hasNotifications() {
return this.errorStore.notifications?.length !== 0;
return this.notificationStore.notifications?.length !== 0;
}
},
watch: {
@ -60,7 +53,7 @@ export default {
methods: {
fireToasts() {
while (this.hasNotifications) {
const msg = this.errorStore.popNotification();
const msg = this.notificationStore.popNotification();
logger.log("fire msg", msg);
@ -170,4 +163,18 @@ export default {
transition: transform 0.3s ease;
}
.spinner-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3.6rem;
flex: 1 1 auto;
font-size: 1.6rem;
}
.space-around {
margin: 3rem;
}
</style>

View file

@ -182,9 +182,11 @@ export default {
hsCodeChanged(event) {
let sanitized = event.target.value
.replace(/\D/g, '')
.substring(0, 10);
.substring(0, 10)
if(sanitized.length !== 10)
if(sanitized.length !== 0)
sanitized = sanitized.padEnd(10, '0');
else
sanitized = null;
this.$emit("update:hsCode", sanitized);

View file

@ -88,7 +88,7 @@ import {buildDate} from "@/common.js";
import BasicButton from "@/components/UI/BasicButton.vue";
import logger from "@/logger.js";
import {mapStores} from "pinia";
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
export default {
name: "ErrorModalOverview",
@ -108,7 +108,7 @@ export default {
try {
await navigator.clipboard.writeText(JSON.stringify(this.error));
this.errorStore.addNotification({
this.notificationStore.addNotification({
icon: 'Clipboard',
message: "The error has been copied to clipboard",
title: "Successful copied to clipboard",
@ -120,7 +120,7 @@ export default {
} catch (err) {
logger.error('Fehler beim Kopieren:', err);
this.errorStore.addNotification({
this.notificationStore.addNotification({
icon: 'Clipboard',
message: "Cannot copy to clipboard",
title: "Not copied to clipboard",
@ -132,7 +132,7 @@ export default {
}
},
computed: {
...mapStores(useErrorStore),
...mapStores(useNotificationStore),
badgeIcon() {
if (this.error.type === "FRONTEND") {
return "desktop";

View file

@ -134,7 +134,7 @@ export default {
},
formattedBody() {
if (!this.body) return '[]';
return JSON.stringify(this.body, null, 2);
return JSON.stringify(JSON.parse(this.body), null, 2);
},
formattedHeader() {
if (!this.header) return '';

View file

@ -1,6 +1,6 @@
import router from './router.js';
//import store from './store/index.js';
import { setupErrorBuffer } from './store/error.js'
import { setupErrorBuffer } from './store/notification.js'
import {createApp} from 'vue'
import {createPinia} from 'pinia';
import {startSessionRefresh} from "@/backend.js";

View file

@ -87,13 +87,14 @@ import {useAssistantStore} from "@/store/assistant.js";
import CreateNewNode from "@/components/layout/node/CreateNewNode.vue";
import Checkbox from "@/components/UI/Checkbox.vue";
import {UrlSafeBase64} from "@/common.js";
import {useNotificationStore} from "@/store/notification.js";
export default {
name: "CalculationAssistant",
components: {Checkbox, CreateNewNode, Modal, SupplierItem, MaterialItem, BasicButton, AutosuggestSearchbar},
computed: {
...mapStores(useNodeStore, useAssistantStore),
...mapStores(useNodeStore, useAssistantStore, useNotificationStore),
showPartNumberModal() {
return this.partNumberModalState;
}
@ -102,7 +103,8 @@ export default {
return {
newSupplierModalState: false,
partNumberModalState: true,
partNumberField: ''
partNumberField: '',
createMsg: 'Creating calculations ...'
}
},
methods: {
@ -111,6 +113,8 @@ export default {
},
async createPremises() {
this.notificationStore.setSpinner(this.createMsg);
const ids = await this.assistantStore.createPremises();
if (ids.length === 1) {
@ -118,6 +122,9 @@ export default {
} else {
this.$router.push({name: "bulk", params: {ids: new UrlSafeBase64().encodeIds(ids)}});
}
this.notificationStore.clearSpinner();
this.assistantStore.reset();
},
selectedSupplier(supplier) {

View file

@ -53,14 +53,6 @@
</transition-group>
</transition>
<modal :z-index="3000" :state="showProcessingModal">
<div class="edit-calculation-spinner-container space-around">
<spinner></spinner>
<span>{{ shownProcessingMessage }}</span>
</div>
</modal>
<mass-edit-dialog v-if="showData" :show="showMultiselectAction" @action="multiselectAction"
:select-count="selectCount"></mass-edit-dialog>
@ -124,6 +116,7 @@ import DestinationListView from "@/components/layout/edit/DestinationListView.vu
import Toast from "@/components/UI/Toast.vue";
import logger from "@/logger.js";
import {useCustomsStore} from "@/store/customs.js";
import {useNotificationStore} from "@/store/notification.js";
const COMPONENT_TYPES = {
@ -147,7 +140,7 @@ export default {
BasicButton
},
computed: {
...mapStores(usePremiseEditStore, useCustomsStore),
...mapStores(usePremiseEditStore, useCustomsStore, useNotificationStore),
hasSelection() {
if (this.premiseEditStore.isLoading || this.premiseEditStore.selectedLoading) {
return false;
@ -191,14 +184,20 @@ export default {
return this.modalType ? this.componentsData[this.modalType] : null;
},
showProcessingModal() {
return this.premiseEditStore.showProcessingModal || this.showCalculationModal || this.customsStore.loadingTariff;
return this.premiseEditStore.showProcessingModal || this.showCalculationModal;
},
shownProcessingMessage() {
if(this.customsStore.loadingTariff)
return "Looking up tariff rate ..."
return this.processingMessage;
}
},
watch: {
showProcessingModal(newState, _) {
if(newState) {
this.notificationStore.setSpinner(this.shownProcessingMessage);
}
else {
this.notificationStore.clearSpinner();
}
}
},
created() {
@ -521,7 +520,7 @@ export default {
flex: 1 1 30rem
}
.edit-calculation-spinner {
.spinner-box {
font-size: 1.6rem;
width: 24rem;
height: 12rem;

View file

@ -19,12 +19,12 @@
</div>
<div v-if="premiseSingleEditStore.showLoadingSpinner" class="edit-calculation-spinner-container">
<box class="edit-calculation-spinner">
<box class="spinner-box">
<spinner></spinner>
</box>
</div>
<div v-else-if="premiseSingleEditStore.isEmpty" class="edit-calculation-spinner-container">
<box class="edit-calculation-spinner">No calculation found.</box>
<box class="spinner-box">No calculation found.</box>
</div>
<div v-else>
@ -85,13 +85,6 @@
<h3 class="sub-header">Destinations & routes</h3>
<destination-list-view></destination-list-view>
<modal :z-index="3000" :state="showProcessingModal">
<div class="edit-calculation-spinner-container space-around">
<spinner></spinner>
<span>{{ shownProcessingMessage }}</span>
</div>
</modal>
</div>
</div>
</template>
@ -106,24 +99,24 @@ import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
import {mapStores} from "pinia";
import Spinner from "@/components/UI/Spinner.vue";
import NotificationBar from "@/components/UI/NotificationBar.vue";
import Modal from "@/components/UI/Modal.vue";
import TraceView from "@/components/layout/TraceView.vue";
import IconButton from "@/components/UI/IconButton.vue";
import {UrlSafeBase64} from "@/common.js";
import {usePremiseSingleEditStore} from "@/store/premiseSingleEdit.js";
import logger from "@/logger.js";
import {useNotificationStore} from "@/store/notification.js";
import Spinner from "@/components/UI/Spinner.vue";
export default {
name: "SingleEdit",
components: {
Spinner,
IconButton,
TraceView,
Modal,
NotificationBar,
Spinner,
DestinationListView,
PriceEdit,
PackagingEdit,
@ -137,11 +130,10 @@ export default {
traceModal: false,
bulkEditQuery: null,
id: null,
showCalculationModal: false,
}
},
computed: {
...mapStores(usePremiseSingleEditStore),
...mapStores(usePremiseSingleEditStore, useNotificationStore),
premise() {
return this.premiseSingleEditStore.premise;
},
@ -149,7 +141,7 @@ export default {
return this.bulkEditQuery !== null;
},
showProcessingModal() {
return this.premiseSingleEditStore.showProcessingModal || this.showCalculationModal;
return this.premiseSingleEditStore.showProcessingModal;
},
shownProcessingMessage() {
if (this.premiseSingleEditStore.routing)
@ -158,6 +150,16 @@ export default {
return "Please wait. Calculating ...";
}
},
watch: {
showProcessingModal(newState, _) {
if(newState) {
this.notificationStore.setSpinner(this.shownProcessingMessage);
}
else {
this.notificationStore.clearSpinner();
}
}
},
methods: {
async startCalculation() {
@ -275,17 +277,7 @@ export default {
margin: 3rem;
}
.edit-calculation-spinner-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3.6rem;
flex: 1 1 auto;
font-size: 1.6rem;
}
.edit-calculation-spinner {
.spinner-box {
font-size: 1.6rem;
width: 24rem;
height: 12rem;

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
import logger from "@/logger.js";

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest, {performDownload, performUpload} from "@/backend.js";
export const useBulkOperationStore = defineStore('bulkOperation', {
@ -39,6 +39,14 @@ export const useBulkOperationStore = defineStore('bulkOperation', {
this.stopTimer();
if(!restart)
useNotificationStore().addNotification({
title: 'Bulk operation',
message: 'All your bulk operations have been completed.',
type: 'success',
icon: 'stack',
})
if(restart) {
this.startTimer();
}

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
export const useContainerRateStore = defineStore('containerRate', {

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import {useStageStore} from "@/store/stage.js";
import {usePropertySetsStore} from "@/store/propertySets.js";
import performRequest from "@/backend.js";

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
import logger from "@/logger.js";

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
export const useMatrixRateStore = defineStore('matrixRate', {

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
import logger from "@/logger.js";

View file

@ -4,22 +4,30 @@ import {toRaw} from "vue";
import {getCsrfToken} from "@/backend.js";
import logger from "@/logger.js";
export const useErrorStore = defineStore('error', {
export const useNotificationStore = defineStore('notification', {
state() {
return {
notifications: [],
sendCache: [],
autoSubmitInterval: 30000,
autoSubmitTimer: null
autoSubmitTimer: null,
spinnerMessage: null
}
},
getters: {
lastError: (state) => state.notifications.length > 0 ? state.notifications[state.notifications.length - 1].error : null,
hasSpinner(state) {
return state.spinnerMessage !== null;
},
spinnerMsg(state) {
return state.spinnerMessage;
}
},
actions: {
clearErrors() {
this.notifications = [];
console.log("Cleared errors");
setSpinner(msg) {
this.spinnerMessage = msg;
},
clearSpinner() {
this.spinnerMessage = null;
},
popNotification() {
return this.notifications.pop();
@ -118,7 +126,7 @@ export const useErrorStore = defineStore('error', {
const pinia = this.$pinia || getActivePinia()
if (pinia && pinia._s) {
pinia._s.forEach((store, storeId) => {
if (storeId !== 'error' && store.$state) {
if (storeId !== 'error' && storeId !== 'errorLog' && store.$state) {
storeState[storeId] = {
...toRaw(store.$state)
}
@ -147,20 +155,20 @@ export const useErrorStore = defineStore('error', {
// Global Error Handler Setup
export function setupErrorBuffer() {
const errorStore = useErrorStore()
const errorStore = useNotificationStore()
//Unhandled Promise Rejections
window.addEventListener('unhandledrejection', (event) => {
const error = {
code: "Unhandled rejection",
title: "Frontend error",
message: event.reason?.message || 'Unhandled Promise Rejection',
traceCombined: event.reason?.stack,
};
errorStore.addError(error, {global: true}).then(r => {} );
})
// window.addEventListener('unhandledrejection', (event) => {
//
// const error = {
// code: "Unhandled rejection",
// title: "Frontend error",
// message: event.reason?.message || 'Unhandled Promise Rejection',
// traceCombined: event.reason?.stack,
// };
//
// errorStore.addError(error, {global: true}).then(r => {} );
// })
// // JavaScript Errors
window.addEventListener('error', (event) => {

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
import logger from "@/logger.js";

View file

@ -1,7 +1,7 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {toRaw} from "vue";
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import logger from "@/logger.js"
import performRequest from '@/backend.js'

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import { useStageStore } from './stage.js'
import {usePropertySetsStore} from "@/store/propertySets.js";
import performRequest from "@/backend.js";

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import { useStageStore } from './stage.js'
import performRequest from "@/backend.js";

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
export const useStageStore = defineStore('stage', {

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import performRequest from "@/backend.js";
export const useStagedRatesStore = defineStore('stagedRates', {

View file

@ -1,6 +1,6 @@
import {defineStore} from 'pinia'
import {config} from '@/config'
import {useErrorStore} from "@/store/error.js";
import {useNotificationStore} from "@/store/notification.js";
import { useStageStore } from './stage.js'
import performRequest from "@/backend.js";

View file

@ -16,9 +16,9 @@ public class AsyncConfig {
@Bean(name = "calculationExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setCorePoolSize(16);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("calc-");
executor.initialize();
return executor;
@ -27,9 +27,9 @@ public class AsyncConfig {
@Bean(name = "customLookupExecutor")
public Executor customLookupExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setCorePoolSize(16);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("lookup-");
executor.initialize();
return executor;
@ -40,7 +40,7 @@ public class AsyncConfig {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("bulk-");
executor.setWaitForTasksToCompleteOnShutdown(true);

View file

@ -2,6 +2,8 @@ package de.avatic.lcc.service.api;
import eu.europa.ec.taxation.taric.client.*;
import jakarta.xml.bind.JAXBElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.ws.client.core.WebServiceTemplate;
@ -17,6 +19,7 @@ public class EUTaxationApiService {
private final WebServiceTemplate webServiceTemplate;
private final ObjectFactory objectFactory;
private Logger logger = LoggerFactory.getLogger(EUTaxationApiService.class);
public EUTaxationApiService(WebServiceTemplate webServiceTemplate) {
this.webServiceTemplate = webServiceTemplate;
@ -25,6 +28,9 @@ public class EUTaxationApiService {
@Async("customLookupExecutor")
public CompletableFuture<GoodsDescrForWsResponse> getGoodsDescription(String goodsCode, String languageCode) {
logger.info("Start lookup for {} and {}", goodsCode, languageCode);
GoodsDescrForWs request = new GoodsDescrForWs();
request.setGoodsCode(goodsCode);
request.setLanguageCode(languageCode);
@ -64,4 +70,12 @@ public class EUTaxationApiService {
throw new RuntimeException("Fehler beim Erstellen des Datums", e);
}
}
public Logger getLogger() {
return logger;
}
public void setLogger(Logger logger) {
this.logger = logger;
}
}

View file

@ -325,6 +325,10 @@ public class PreCalculationCheckService {
private void materialCheck(Premise premise) {
if(premise.getHsCode() == null || premise.getHsCode().length() < 10)
throw new PremiseValidationError("Invalid HS code.");
var isDeclarable = eUTaxationResolverService.validate(premise.getHsCode());
if (!isDeclarable)

View file

@ -637,8 +637,8 @@ CREATE TABLE IF NOT EXISTS sys_error
title VARCHAR(255) NOT NULL,
code VARCHAR(255) NOT NULL,
message VARCHAR(1024) NOT NULL,
request TEXT,
pinia TEXT,
request MEDIUMTEXT,
pinia MEDIUMTEXT,
calculation_job_id INT DEFAULT NULL,
bulk_operation_id INT DEFAULT NULL,
type CHAR(16) NOT NULL DEFAULT 'BACKEND',

View file

@ -637,8 +637,8 @@ CREATE TABLE IF NOT EXISTS sys_error
title VARCHAR(255) NOT NULL,
code VARCHAR(255) NOT NULL,
message VARCHAR(1024) NOT NULL,
request TEXT,
pinia TEXT,
request MEDIUMTEXT,
pinia MEDIUMTEXT,
calculation_job_id INT DEFAULT NULL,
bulk_operation_id INT DEFAULT NULL,
type CHAR(16) NOT NULL DEFAULT 'BACKEND',