@hed-hog/core 0.0.291 → 0.0.293

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/auth/auth.controller.d.ts +5 -5
  2. package/dist/auth/auth.service.d.ts +5 -5
  3. package/dist/challenge/challenge.service.d.ts +2 -2
  4. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
  5. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  6. package/dist/install/dto/install.dto.d.ts +2 -0
  7. package/dist/install/dto/install.dto.d.ts.map +1 -1
  8. package/dist/install/dto/install.dto.js +12 -0
  9. package/dist/install/dto/install.dto.js.map +1 -1
  10. package/dist/install/install.controller.d.ts +1 -0
  11. package/dist/install/install.controller.d.ts.map +1 -1
  12. package/dist/install/install.service.d.ts +6 -2
  13. package/dist/install/install.service.d.ts.map +1 -1
  14. package/dist/install/install.service.js +52 -4
  15. package/dist/install/install.service.js.map +1 -1
  16. package/dist/profile/profile.controller.d.ts +6 -6
  17. package/dist/profile/profile.service.d.ts +6 -6
  18. package/dist/session/session.controller.d.ts +2 -2
  19. package/dist/session/session.service.d.ts +3 -3
  20. package/dist/setting/setting.controller.d.ts +9 -9
  21. package/dist/setting/setting.service.d.ts +10 -10
  22. package/dist/user/user.controller.d.ts +6 -6
  23. package/dist/user/user.service.d.ts +12 -12
  24. package/hedhog/data/dashboard_component_role.yaml +223 -223
  25. package/hedhog/data/dashboard_role.yaml +18 -18
  26. package/hedhog/data/setting_group.yaml +11 -0
  27. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +0 -2
  28. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -62
  29. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -29
  30. package/hedhog/frontend/app/mail/template/page.tsx.ejs +17 -2
  31. package/package.json +7 -7
  32. package/src/ai/ai.service.ts +3 -3
  33. package/src/auth/auth.controller.ts +11 -11
  34. package/src/auth/auth.service.ts +8 -8
  35. package/src/install/dto/install.dto.ts +15 -1
  36. package/src/install/install.service.ts +67 -3
  37. package/src/task/task.service.ts +5 -5
@@ -4,21 +4,21 @@
4
4
  role_id:
5
5
  where:
6
6
  slug: admin-access
7
- - dashboard_id:
8
- where:
9
- slug: user
10
- role_id:
11
- where:
12
- slug: user
13
- - dashboard_id:
14
- where:
15
- slug: user
16
- role_id:
17
- where:
18
- slug: admin-access
19
- - dashboard_id:
20
- where:
21
- slug: config
22
- role_id:
23
- where:
24
- slug: admin
7
+ - dashboard_id:
8
+ where:
9
+ slug: user
10
+ role_id:
11
+ where:
12
+ slug: user
13
+ - dashboard_id:
14
+ where:
15
+ slug: user
16
+ role_id:
17
+ where:
18
+ slug: admin-access
19
+ - dashboard_id:
20
+ where:
21
+ slug: config
22
+ role_id:
23
+ where:
24
+ slug: admin
@@ -28,6 +28,17 @@
28
28
  pt: A URL da API do sistema
29
29
  value: http://localhost:3100
30
30
  user_override: false
31
+ - slug: installed
32
+ type: boolean
33
+ component: switch
34
+ name:
35
+ en: Installed
36
+ pt: Instalado
37
+ description:
38
+ en: Whether the system has already been installed
39
+ pt: Se o sistema ja foi instalado
40
+ value: false
41
+ user_override: false
31
42
  - slug: maintenance
32
43
  icon: database-cog
33
44
  name:
@@ -119,8 +119,6 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
119
119
  static: false,
120
120
  }));
121
121
 
122
- console.log('📊 Layout recebido do backend:', gridLayout);
123
-
124
122
  setLayout(gridLayout);
125
123
  setWidgets(userLayout);
126
124
  } else {
@@ -37,24 +37,24 @@ function CustomTooltip({
37
37
  );
38
38
  }
39
39
 
40
- interface PermissionsChartProps {
41
- widget?: {
42
- name?: string;
43
- } | null;
44
- onRemove?: () => void;
45
- }
46
-
47
- interface PermissionDistributionItem {
48
- name: string;
49
- value: number;
50
- color: string;
51
- }
52
-
53
- interface UserStatsData {
54
- charts?: {
55
- permissionDistribution?: PermissionDistributionItem[];
56
- };
57
- }
40
+ interface PermissionsChartProps {
41
+ widget?: {
42
+ name?: string;
43
+ } | null;
44
+ onRemove?: () => void;
45
+ }
46
+
47
+ interface PermissionDistributionItem {
48
+ name: string;
49
+ value: number;
50
+ color: string;
51
+ }
52
+
53
+ interface UserStatsData {
54
+ charts?: {
55
+ permissionDistribution?: PermissionDistributionItem[];
56
+ };
57
+ }
58
58
 
59
59
  export default function PermissionsChart({
60
60
  widget,
@@ -70,10 +70,10 @@ export default function PermissionsChart({
70
70
  } = useWidgetData<UserStatsData>({
71
71
  endpoint: '/dashboard-core/stats/overview/users',
72
72
  queryKey: 'dashboard-stats-users',
73
- });
74
-
75
- const data = statsData?.charts?.permissionDistribution || [];
76
- const total = data.reduce((sum, item) => sum + item.value, 0);
73
+ });
74
+
75
+ const data = statsData?.charts?.permissionDistribution || [];
76
+ const total = data.reduce((sum, item) => sum + item.value, 0);
77
77
 
78
78
  return (
79
79
  <WidgetWrapper
@@ -90,19 +90,19 @@ export default function PermissionsChart({
90
90
  >
91
91
  <IconGripVertical className="text-muted-foreground/50 size-4 shrink-0" />
92
92
  </div>
93
- <CardHeader className="pb-2 pt-4 pl-10">
94
- <CardTitle className="text-base font-semibold">
95
- {t('permissionsDistributionTitle')}
96
- </CardTitle>
93
+ <CardHeader className="pb-2 pt-4 pl-10">
94
+ <CardTitle className="text-base font-semibold">
95
+ {t('permissionsDistributionTitle')}
96
+ </CardTitle>
97
97
  <CardDescription>
98
98
  {t('permissionsDistributionDescription')}
99
99
  </CardDescription>
100
- </CardHeader>
101
- <CardContent className="flex-1 pt-0">
102
- <div className="flex h-full items-center justify-between gap-4 overflow-hidden">
103
- <div className="relative h-[280px] w-[280px] shrink-0">
104
- <ResponsiveContainer width="100%" height="100%">
105
- <PieChart>
100
+ </CardHeader>
101
+ <CardContent className="flex-1 pt-0">
102
+ <div className="flex h-full items-center justify-between gap-4 overflow-hidden">
103
+ <div className="relative h-[280px] w-[280px] shrink-0">
104
+ <ResponsiveContainer width="100%" height="100%">
105
+ <PieChart>
106
106
  <Pie
107
107
  data={data}
108
108
  cx="50%"
@@ -110,14 +110,14 @@ export default function PermissionsChart({
110
110
  innerRadius={70}
111
111
  outerRadius={110}
112
112
  paddingAngle={4}
113
- dataKey="value"
114
- animationDuration={1200}
115
- strokeWidth={0}
116
- >
117
- {data.map((entry, index: number) => (
118
- <Cell key={`cell-${index}`} fill={entry.color} />
119
- ))}
120
- </Pie>
113
+ dataKey="value"
114
+ animationDuration={1200}
115
+ strokeWidth={0}
116
+ >
117
+ {data.map((entry, index: number) => (
118
+ <Cell key={`cell-${index}`} fill={entry.color} />
119
+ ))}
120
+ </Pie>
121
121
  <Tooltip content={<CustomTooltip />} />
122
122
  </PieChart>
123
123
  </ResponsiveContainer>
@@ -125,28 +125,28 @@ export default function PermissionsChart({
125
125
  <span className="text-2xl font-bold text-foreground">
126
126
  {total}
127
127
  </span>
128
- <span className="text-[10px] text-muted-foreground">
129
- {t('total')}
130
- </span>
131
- </div>
132
- </div>
133
- <div className="grid max-h-[280px] min-w-0 flex-1 grid-cols-2 content-start gap-x-4 gap-y-2 overflow-y-auto pr-2">
134
- {data.map((item) => (
135
- <div key={item.name} className="flex min-w-0 items-start gap-2.5">
136
- <div
137
- className="mt-1 h-2.5 w-2.5 shrink-0 rounded-sm"
138
- style={{ backgroundColor: item.color }}
139
- />
140
- <div className="flex min-w-0 flex-col">
141
- <span className="break-words text-xs font-medium leading-tight text-foreground">
142
- {item.name}
143
- </span>
144
- <span className="text-[11px] text-muted-foreground">
145
- {item.value} ({total ? Math.round((item.value / total) * 100) : 0}%)
146
- </span>
147
- </div>
148
- </div>
149
- ))}
128
+ <span className="text-[10px] text-muted-foreground">
129
+ {t('total')}
130
+ </span>
131
+ </div>
132
+ </div>
133
+ <div className="grid max-h-[280px] min-w-0 flex-1 grid-cols-2 content-start gap-x-4 gap-y-2 overflow-y-auto pr-2">
134
+ {data.map((item) => (
135
+ <div key={item.name} className="flex min-w-0 items-start gap-2.5">
136
+ <div
137
+ className="mt-1 h-2.5 w-2.5 shrink-0 rounded-sm"
138
+ style={{ backgroundColor: item.color }}
139
+ />
140
+ <div className="flex min-w-0 flex-col">
141
+ <span className="break-words text-xs font-medium leading-tight text-foreground">
142
+ {item.name}
143
+ </span>
144
+ <span className="text-[11px] text-muted-foreground">
145
+ {item.value} ({total ? Math.round((item.value / total) * 100) : 0}%)
146
+ </span>
147
+ </div>
148
+ </div>
149
+ ))}
150
150
  </div>
151
151
  </div>
152
152
  </CardContent>
@@ -1,29 +1,29 @@
1
- 'use client';
2
-
3
- import { Dashboard } from '@hed-hog/api-types';
4
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
5
- import { useRouter } from 'next/navigation';
6
- import { useEffect } from 'react';
7
-
8
- export default function DashboardRedirectPage() {
9
- const router = useRouter();
10
- const { request, currentLocaleCode } = useApp();
11
-
12
- const { data: dashboardData, isLoading } = useQuery<Dashboard | null>({
13
- queryKey: ['dashboard-home-redirect', currentLocaleCode],
14
- queryFn: async () => {
15
- const response = await request<Dashboard>({
16
- url: '/dashboard-core/home',
17
- });
18
- return response.data ?? null;
19
- },
20
- });
21
-
22
- useEffect(() => {
23
- if (isLoading) return;
24
-
25
- router.replace(`/core/dashboard/${dashboardData?.slug ?? 'default'}`);
26
- }, [dashboardData?.slug, isLoading, router]);
27
-
28
- return null;
29
- }
1
+ 'use client';
2
+
3
+ import { Dashboard } from '@hed-hog/api-types';
4
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
5
+ import { useRouter } from 'next/navigation';
6
+ import { useEffect } from 'react';
7
+
8
+ export default function DashboardRedirectPage() {
9
+ const router = useRouter();
10
+ const { request, currentLocaleCode } = useApp();
11
+
12
+ const { data: dashboardData, isLoading } = useQuery<Dashboard | null>({
13
+ queryKey: ['dashboard-home-redirect', currentLocaleCode],
14
+ queryFn: async () => {
15
+ const response = await request<Dashboard>({
16
+ url: '/dashboard-core/home',
17
+ });
18
+ return response.data ?? null;
19
+ },
20
+ });
21
+
22
+ useEffect(() => {
23
+ if (isLoading) return;
24
+
25
+ router.replace(`/core/dashboard/${dashboardData?.slug ?? 'default'}`);
26
+ }, [dashboardData?.slug, isLoading, router]);
27
+
28
+ return null;
29
+ }
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import dynamic from 'next/dynamic';
3
4
  import type React from 'react';
4
5
 
5
6
  import {
@@ -9,7 +10,6 @@ import {
9
10
  PaginationFooter,
10
11
  SearchBar,
11
12
  } from '@/components/entity-list';
12
- import { RichTextEditor } from '@/components/rich-text-editor';
13
13
  import {
14
14
  AlertDialog,
15
15
  AlertDialogAction,
@@ -70,6 +70,19 @@ import { useTranslations } from 'next-intl';
70
70
  import { useEffect, useState } from 'react';
71
71
  import { toast } from 'sonner';
72
72
 
73
+ const RichTextEditor = dynamic(
74
+ () =>
75
+ import('@/components/rich-text-editor').then((mod) => ({
76
+ default: mod.RichTextEditor,
77
+ })),
78
+ {
79
+ ssr: false,
80
+ loading: () => (
81
+ <div className="min-h-48 bg-muted rounded-md animate-pulse" />
82
+ ),
83
+ }
84
+ );
85
+
73
86
  interface Mail {
74
87
  id: number;
75
88
  slug: string;
@@ -779,7 +792,9 @@ export default function EmailTemplatesPage() {
779
792
  <TableHead>{t('tableSubject')}</TableHead>
780
793
  <TableHead>{t('tableVariables')}</TableHead>
781
794
  <TableHead>{t('tableUpdated')}</TableHead>
782
- <TableHead className="text-right">{t('tableActions')}</TableHead>
795
+ <TableHead className="text-right">
796
+ {t('tableActions')}
797
+ </TableHead>
783
798
  </TableRow>
784
799
  </TableHeader>
785
800
  <TableBody>
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.291",
3
+ "version": "0.0.293",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
7
- "@aws-sdk/client-s3": "^3.918.0",
8
- "@aws-sdk/s3-request-presigner": "^3.918.0",
7
+ "@aws-sdk/client-s3": "^3.1014.0",
8
+ "@aws-sdk/s3-request-presigner": "^3.1014.0",
9
9
  "@azure/storage-blob": "^12.27.0",
10
10
  "@google-cloud/storage": "^7.16.0",
11
11
  "@nestjs/axios": "*",
@@ -30,12 +30,12 @@
30
30
  "sharp": "^0.34.2",
31
31
  "speakeasy": "^2.0.0",
32
32
  "uuid": "^11.1.0",
33
- "@hed-hog/api-types": "0.0.1",
34
- "@hed-hog/api-pagination": "0.0.6",
35
33
  "@hed-hog/api": "0.0.4",
34
+ "@hed-hog/api-pagination": "0.0.6",
35
+ "@hed-hog/api-locale": "0.0.13",
36
+ "@hed-hog/api-types": "0.0.1",
36
37
  "@hed-hog/api-mail": "0.0.8",
37
- "@hed-hog/api-prisma": "0.0.5",
38
- "@hed-hog/api-locale": "0.0.13"
38
+ "@hed-hog/api-prisma": "0.0.5"
39
39
  },
40
40
  "exports": {
41
41
  ".": {
@@ -1,5 +1,5 @@
1
- import { PaginationDTO } from '@hed-hog/api-pagination';
2
- import { Prisma, PrismaService } from '@hed-hog/api-prisma';
1
+ import { PaginationDTO } from '@hed-hog/api-pagination';
2
+ import { Prisma, PrismaService } from '@hed-hog/api-prisma';
3
3
  import {
4
4
  BadRequestException,
5
5
  forwardRef,
@@ -8,7 +8,7 @@ import {
8
8
  Logger,
9
9
  NotFoundException,
10
10
  } from '@nestjs/common';
11
- import axios from 'axios';
11
+ import axios from 'axios';
12
12
  import { createHash } from 'crypto';
13
13
  import pdfParse from 'pdf-parse';
14
14
  import { DeleteDTO } from '../dto/delete.dto';
@@ -1,19 +1,19 @@
1
1
  import { Public, Role, User, UserOptional } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
- import {
4
- BadRequestException,
5
- Body,
6
- Controller,
7
- forwardRef,
3
+ import {
4
+ BadRequestException,
5
+ Body,
6
+ Controller,
7
+ forwardRef,
8
8
  Get,
9
9
  Headers,
10
10
  Inject,
11
- Ip,
12
- Post,
13
- Req,
14
- Res,
15
- UnauthorizedException,
16
- } from '@nestjs/common';
11
+ Ip,
12
+ Post,
13
+ Req,
14
+ Res,
15
+ UnauthorizedException,
16
+ } from '@nestjs/common';
17
17
  import { TokenService } from '../token/token.service';
18
18
  import { CreateWithEmailAndPasswordDTO } from '../user/dto/create-with-email-and-password.dto';
19
19
  import { UserService } from '../user/user.service';
@@ -1,14 +1,14 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PrismaService } from '@hed-hog/api-prisma';
3
3
  import { User } from '@hed-hog/api-types';
4
- import {
5
- BadRequestException,
6
- forwardRef,
7
- Inject,
8
- Injectable,
9
- NotFoundException,
10
- UnauthorizedException,
11
- } from '@nestjs/common';
4
+ import {
5
+ BadRequestException,
6
+ forwardRef,
7
+ Inject,
8
+ Injectable,
9
+ NotFoundException,
10
+ UnauthorizedException,
11
+ } from '@nestjs/common';
12
12
  import { ChallengeService } from '../challenge/challenge.service';
13
13
  import { MailService as MailManagerService } from '../mail/mail.service';
14
14
  import { SecurityService } from '../security/security.service';
@@ -1,5 +1,5 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
- import { IsOptional, IsString, MinLength } from 'class-validator';
2
+ import { IsOptional, IsString, IsUrl, MinLength } from 'class-validator';
3
3
  import { IsEmailWithSettings } from '../../validators/is-email-with-settings.validator';
4
4
 
5
5
  export class InstallDTO {
@@ -26,4 +26,18 @@ export class InstallDTO {
26
26
  @IsString({ message: 'Senha deve ter pelo menos 6 caracteres' })
27
27
  @MinLength(6, { message: 'Senha deve ter pelo menos 6 caracteres' })
28
28
  password?: string = 'changeme';
29
+
30
+ @IsOptional()
31
+ @IsUrl(
32
+ { require_protocol: true, require_tld: false },
33
+ { message: 'URL da admin invalida' },
34
+ )
35
+ adminUrl?: string = 'http://localhost:3200';
36
+
37
+ @IsOptional()
38
+ @IsUrl(
39
+ { require_protocol: true, require_tld: false },
40
+ { message: 'URL da API invalida' },
41
+ )
42
+ apiUrl?: string = 'http://localhost:3100';
29
43
  }
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from 'fs';
5
5
  import { writeFile } from 'fs/promises';
6
6
  import { resolve } from 'path';
7
7
  import { SecurityService } from '../security/security.service';
8
+ import { SettingService } from '../setting/setting.service';
8
9
  import { InstallDTO } from './dto/install.dto';
9
10
 
10
11
  @Injectable()
@@ -15,9 +16,14 @@ export class InstallService {
15
16
  constructor(
16
17
  private readonly security: SecurityService,
17
18
  private readonly prisma: PrismaService,
18
- private readonly configService: ConfigService,
19
+ private readonly configService: ConfigService,
20
+ private readonly settingService: SettingService,
19
21
  ) { }
20
22
 
23
+ private normalizeUrl(value: string) {
24
+ return value.trim().replace(/\/$/, '');
25
+ }
26
+
21
27
  private async forceReset() {
22
28
 
23
29
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -56,6 +62,19 @@ export class InstallService {
56
62
  }
57
63
 
58
64
  private async checkInstallation() {
65
+ const installedSetting = await this.prisma.setting.findFirst({
66
+ where: {
67
+ slug: 'installed',
68
+ },
69
+ select: {
70
+ value: true,
71
+ },
72
+ });
73
+
74
+ if (installedSetting?.value === 'true') {
75
+ return true;
76
+ }
77
+
59
78
  const usersCount = await this.prisma.user.count();
60
79
  return usersCount > 0;
61
80
  }
@@ -135,12 +154,23 @@ export class InstallService {
135
154
  return Buffer.from(str).toString('base64');
136
155
  }
137
156
 
138
- async install({ appName, email, password, slogan, userName }: InstallDTO) {
157
+ async install({
158
+ adminUrl,
159
+ apiUrl,
160
+ appName,
161
+ email,
162
+ password,
163
+ slogan,
164
+ userName,
165
+ }: InstallDTO) {
139
166
 
140
167
  if (await this.checkInstallation()) {
141
168
  throw new BadRequestException('Application is already installed.');
142
169
  }
143
170
 
171
+ const normalizedAdminUrl = this.normalizeUrl(adminUrl ?? 'http://localhost:3200');
172
+ const normalizedApiUrl = this.normalizeUrl(apiUrl ?? 'http://localhost:3100');
173
+
144
174
  await this.prisma.$transaction(async (prisma) => {
145
175
 
146
176
  this.logger.log('Starting installation process...');
@@ -167,6 +197,24 @@ export class InstallService {
167
197
  }
168
198
  });
169
199
 
200
+ await prisma.setting.update({
201
+ where: {
202
+ slug: 'url',
203
+ },
204
+ data: {
205
+ value: normalizedAdminUrl,
206
+ },
207
+ });
208
+
209
+ await prisma.setting.update({
210
+ where: {
211
+ slug: 'api-url',
212
+ },
213
+ data: {
214
+ value: normalizedApiUrl,
215
+ },
216
+ });
217
+
170
218
  this.logger.log('System slogan set.');
171
219
 
172
220
  this.logger.log(`Setting system email to: ${email}`);
@@ -236,6 +284,17 @@ export class InstallService {
236
284
 
237
285
  this.logger.log(`Roles assigned to user ID: ${user.id}`);
238
286
 
287
+ await prisma.setting.update({
288
+ where: {
289
+ slug: 'installed',
290
+ },
291
+ data: {
292
+ value: 'true',
293
+ },
294
+ });
295
+
296
+ this.logger.log('Installation flag set to true.');
297
+
239
298
  const isDevelopment = process.env.NODE_ENV !== 'production';
240
299
  if (isDevelopment) {
241
300
  await this.updateEnvSecrets(pepper);
@@ -250,12 +309,17 @@ export class InstallService {
250
309
 
251
310
  });
252
311
 
312
+ this.settingService.clearCache();
313
+
253
314
  return { success: true };
254
315
 
255
316
  }
256
317
 
257
318
  async check() {
258
- return { success: true };
319
+ return {
320
+ success: true,
321
+ installed: await this.checkInstallation(),
322
+ };
259
323
  }
260
324
 
261
325
  async generateMailMigration({
@@ -1,7 +1,7 @@
1
- import { Prisma, PrismaService } from '@hed-hog/api-prisma';
2
- import { Injectable, Logger } from '@nestjs/common';
3
- import { Cron, CronExpression } from '@nestjs/schedule';
4
- import { SettingService } from '../setting/setting.service';
1
+ import { Prisma, PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable, Logger } from '@nestjs/common';
3
+ import { Cron, CronExpression } from '@nestjs/schedule';
4
+ import { SettingService } from '../setting/setting.service';
5
5
 
6
6
  @Injectable()
7
7
  export class TasksService {
@@ -225,4 +225,4 @@ export class TasksService {
225
225
  `Unverified MFA with expired challenges cleaned up in ${Date.now() - startAt}ms`,
226
226
  );
227
227
  }
228
- }
228
+ }