diff --git a/backend/DOSPORTAL/migrations/0016_alter_detector_sn.py b/backend/DOSPORTAL/migrations/0016_alter_detector_sn.py new file mode 100644 index 0000000..982d900 --- /dev/null +++ b/backend/DOSPORTAL/migrations/0016_alter_detector_sn.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.3 on 2026-03-31 15:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('DOSPORTAL', '0015_detectorlogbook_metadata'), + ] + + operations = [ + migrations.AlterField( + model_name='detector', + name='sn', + field=models.CharField(max_length=80, unique=True), + ), + ] diff --git a/backend/DOSPORTAL/models/__init__.py b/backend/DOSPORTAL/models/__init__.py index ac5a151..53e85cb 100644 --- a/backend/DOSPORTAL/models/__init__.py +++ b/backend/DOSPORTAL/models/__init__.py @@ -1,23 +1,58 @@ from .soft_delete import SoftDeleteModel, SoftDeleteManager, SoftDeleteQuerySet -from .utils import UUIDMixin, Profile -from .detectors import DetectorManufacturer, DetectorType, DetectorCalib, Detector, DetectorLogbook +from .utils import UUIDMixin, Profile, ProcessingStatusMixin +from .detectors import ( + DetectorManufacturer, + DetectorType, + DetectorCalib, + Detector, + DetectorLogbook, +) from .organizations import Organization, OrganizationUser, OrganizationInvite from .flights import CARImodel, Airports, Flight, Trajectory, TrajectoryPoint from .measurements import ( - _validate_data_file, _validate_metadata_file, _validate_log_file, - MeasurementDataFlight, MeasurementArtifact, - MeasurementCampaign, Measurement, MeasurementSegment, SpectrumData + _validate_data_file, + _validate_metadata_file, + _validate_log_file, + MeasurementDataFlight, + MeasurementArtifact, + MeasurementCampaign, + Measurement, + MeasurementSegment, + SpectrumData, ) from .files import File from .spectrals import SpectralRecord, SpectralRecordArtifact __all__ = [ - "SoftDeleteModel", "SoftDeleteManager", "SoftDeleteQuerySet", - "UUIDMixin", "Profile", - "DetectorManufacturer", "DetectorType", "DetectorCalib", "Detector", "DetectorLogbook", - "Organization", "OrganizationUser", "OrganizationInvite", - "_validate_data_file", "_validate_metadata_file", "_validate_log_file", - "CARImodel", "Airports", "Flight", "MeasurementDataFlight", "MeasurementArtifact", - "MeasurementCampaign", "Measurement", "MeasurementSegment", "File", "Trajectory", "TrajectoryPoint", "SpectrumData", - "SpectralRecord", "SpectralRecordArtifact" -] \ No newline at end of file + "SoftDeleteModel", + "SoftDeleteManager", + "SoftDeleteQuerySet", + "UUIDMixin", + "ProcessingStatusMixin", + "Profile", + "DetectorManufacturer", + "DetectorType", + "DetectorCalib", + "Detector", + "DetectorLogbook", + "Organization", + "OrganizationUser", + "OrganizationInvite", + "_validate_data_file", + "_validate_metadata_file", + "_validate_log_file", + "CARImodel", + "Airports", + "Flight", + "MeasurementDataFlight", + "MeasurementArtifact", + "MeasurementCampaign", + "Measurement", + "MeasurementSegment", + "File", + "Trajectory", + "TrajectoryPoint", + "SpectrumData", + "SpectralRecord", + "SpectralRecordArtifact", +] diff --git a/backend/DOSPORTAL/models/detectors.py b/backend/DOSPORTAL/models/detectors.py index 9bae6dd..9be5928 100644 --- a/backend/DOSPORTAL/models/detectors.py +++ b/backend/DOSPORTAL/models/detectors.py @@ -90,9 +90,9 @@ def __str__(self) -> str: class Detector(UUIDMixin, SoftDeleteModel): - sn = models.CharField( max_length=80, + unique=True, ) name = models.CharField( _("Detector name"), diff --git a/frontend/src/App.css b/frontend/src/App.css index 2ef6f71..898b09a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -4,16 +4,7 @@ * Add only critical fixes here; prefer styling in components with Tailwind. */ -:root { - --bg: #ffffff; - --card: #ffffff; - --text: #111827; - --muted: #6b7280; - --primary: #0d6efd; /* bootstrap primary */ - --green: #198754; /* bootstrap success */ - --red: #dc3545; /* bootstrap danger */ - --border: #e5e7eb; -} + * { box-sizing: border-box; @@ -37,96 +28,6 @@ body { overflow-x: hidden; } -.bg { display: none; } - -.auth-layout { - position: relative; - z-index: 1; - display: grid; - grid-template-columns: 1fr 1.2fr; - gap: 2rem; - max-width: 1100px; - margin: 0 auto; - padding: 3rem 1.25rem; -} - -.auth-layout.single { - max-width: 520px; - grid-template-columns: 1fr; -} - -.content { - position: relative; - z-index: 1; - max-width: 1100px; - margin: 0 auto; - padding: 2.5rem 1.25rem 3rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -.hero { - background: var(--card); - border: 1px solid var(--border); - border-radius: 10px; - padding: 1.5rem; - display: flex; - justify-content: space-between; - gap: 1rem; - flex-wrap: wrap; -} - -.hero h1 { - margin: 0.15rem 0 0.35rem 0; -} - -.hero-actions { - display: flex; - gap: 0.5rem; - align-items: center; - flex-wrap: wrap; -} - -.eyebrow { - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 0.8rem; - color: var(--muted); - margin: 0; -} - -@media (max-width: 880px) { - .auth-layout { - grid-template-columns: 1fr; - padding: 2rem 1rem; - } -} - -.login-card { - background: var(--card); - border: 1px solid var(--border); - border-radius: 10px; - padding: 2rem; - box-shadow: 0 6px 18px rgba(0,0,0,0.08); -} - -.brand { - margin: 0 0 0.25rem 0; - letter-spacing: 0.04em; -} - -.subtitle { - margin-top: 0; - color: var(--muted); - font-size: 0.95rem; -} - -.login-form { - margin-top: 1.5rem; - display: grid; - gap: 1rem; -} .field label { display: block; @@ -165,11 +66,6 @@ button.primary:disabled { cursor: not-allowed; } -.help { - margin-top: 0.75rem; - color: var(--muted); -} - .error { margin-top: 0.75rem; color: var(--red); @@ -193,87 +89,4 @@ button.primary:disabled { justify-content: space-between; gap: 1rem; margin-bottom: 0.75rem; -} - -.panel-header button { - padding: 0.5rem 0.8rem; - border-radius: 8px; - border: 1px solid var(--primary); - background: var(--primary); - color: #ffffff; - cursor: pointer; -} - -.panel-body { - text-align: left; -} - -.logbook-list { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 0.75rem; -} - -.logbook-item { - border: 1px solid var(--border); - border-radius: 10px; - padding: 0.9rem 1rem; - background: #ffffff; -} - -.logbook-item .meta { - display: flex; - align-items: center; - gap: 0.75rem; - margin-bottom: 0.5rem; - color: var(--muted); -} - -.badge { - display: inline-block; - padding: 0.15rem 0.5rem; - border: 1px solid #d1d5db; - border-radius: 9999px; - font-size: 0.75rem; - color: var(--text); -} - -.text { - margin: 0.2rem 0 0.5rem 0; -} - -.maplink { - font-size: 0.85rem; - color: var(--primary); -} - -.muted { - color: var(--muted); - margin: 0; -} - -.pill { - color: #ffffff; - text-decoration: none; - padding: 0.35rem 0.75rem; - border-radius: 999px; - border: 1px solid var(--primary); - background: var(--primary); - cursor: pointer; - display: inline-flex; - align-items: center; - gap: 0.35rem; - font-size: 0.95rem; -} - -.pill.ghost { - background: transparent; - color: var(--text); - border-color: #d1d5db; -} - -.pill:hover { - filter: brightness(1.05); -} +} \ No newline at end of file diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index cfe52ff..8e0da55 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -5,17 +5,20 @@ import { AuthProvider } from '@/features/auth/context/AuthProvider' import { router } from './router' import { Toaster } from '@/components/ui/sonner' import { ThemeProvider } from '@/components/theme-provider' +import { TooltipProvider } from '@/components/ui/tooltip' const queryClient = new QueryClient() function App() { return ( - - - - - + + + + + + + ) diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..cacff11 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..2cb6924 --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,88 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..7413f6e --- /dev/null +++ b/frontend/src/components/ui/tooltip.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import { Tooltip as TooltipPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } diff --git a/frontend/src/components/ui/typography.tsx b/frontend/src/components/ui/typography.tsx new file mode 100644 index 0000000..b6553d6 --- /dev/null +++ b/frontend/src/components/ui/typography.tsx @@ -0,0 +1,104 @@ +// Canonical shadcn-inspired typography primitives +// Use these instead of raw

,

,

,

, etc. + +import React from 'react' + +export function TypographyH1({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} + +export function TypographyH2({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} + +export function TypographyH3({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} + +export function TypographyH4({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} + +export function TypographyP({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} + +export function TypographyBlockquote({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +
+ {children} +
+ ) +} + +export function TypographyTable({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +
+ {children}
+
+ ) +} + +export function TypographyList({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +
    li]:mt-2 ${className}`} {...props}> + {children} +
+ ) +} + +export function TypographyInlineCode({ children, className = '', ...props }: React.HTMLAttributes) { + return ( + + {children} + + ) +} + +export function TypographyLead({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} + +export function TypographyLarge({ children, className = '', ...props }: React.HTMLAttributes) { + return
{children}
+} + +export function TypographySmall({ children, className = '', ...props }: React.HTMLAttributes) { + return ( + + {children} + + ) +} + +export function TypographyMuted({ children, className = '', ...props }: React.HTMLAttributes) { + return ( +

+ {children} +

+ ) +} diff --git a/frontend/src/features/auth/pages/LoginPage.tsx b/frontend/src/features/auth/pages/LoginPage.tsx index feed378..aee322c 100644 --- a/frontend/src/features/auth/pages/LoginPage.tsx +++ b/frontend/src/features/auth/pages/LoginPage.tsx @@ -2,6 +2,8 @@ import { useState } from 'react' import loginBg from '../../../assets/img/login_background.jpg' import { useAuthContext } from '@/features/auth/hooks/useAuthContext' import { Button } from '@/shared/components/Button/Button' +import { Button as ShadcnButton } from '@/components/ui/button' +import { Section } from '@/shared/components/Layout/Section' export const LoginPage = () => { const { login } = useAuthContext() @@ -41,19 +43,14 @@ export const LoginPage = () => { }} >
-
-
-

- Please sign in -

- +
+
{error && (
{error}
)} - -
+
{ required />
-
{ required />
- - -
- -
+
- Not registered yet? Sign up here + Not registered yet?{' '} + + Sign up here +
-
+
diff --git a/frontend/src/features/auth/pages/SignupPage.tsx b/frontend/src/features/auth/pages/SignupPage.tsx index 02a1eba..175346c 100644 --- a/frontend/src/features/auth/pages/SignupPage.tsx +++ b/frontend/src/features/auth/pages/SignupPage.tsx @@ -4,6 +4,8 @@ import loginBg from '../../../assets/img/login_background.jpg' import { theme } from '@/theme' import { useAuthContext } from '@/features/auth/hooks/useAuthContext' import { Button } from '@/shared/components/Button/Button' +import { Button as ShadcnButton } from '@/components/ui/button' +import { Section } from '@/shared/components/Layout/Section' export const SignupPage = () => { const { signup } = useAuthContext() @@ -49,19 +51,14 @@ export const SignupPage = () => { }} >
-
-
-

- Create account -

- +
+
{error && (
{error}
)} - -
+
{ required />
-
{ required />
-
{ required />
-
{ required />
-
{ required />
-
{ required />
-
- -
+
- Already have an account? Sign in here + Already have an account?{' '} + + Sign in here +
-
+
diff --git a/frontend/src/features/auth/pages/SignupSuccessPage.tsx b/frontend/src/features/auth/pages/SignupSuccessPage.tsx index 344c010..526f219 100644 --- a/frontend/src/features/auth/pages/SignupSuccessPage.tsx +++ b/frontend/src/features/auth/pages/SignupSuccessPage.tsx @@ -1,3 +1,4 @@ +import { TypographyH1 } from '@/components/ui/typography' import loginBg from '../../../assets/img/login_background.jpg' import { theme } from '@/theme' @@ -22,9 +23,9 @@ export const SignupSuccessPage = () => {
-

+ Registration successful -

+

Your registration was successful. Your account must be approved by an administrator diff --git a/frontend/src/features/devices/components/DetectorCard.tsx b/frontend/src/features/devices/components/DetectorCard.tsx index 4597e8f..b992b4a 100644 --- a/frontend/src/features/devices/components/DetectorCard.tsx +++ b/frontend/src/features/devices/components/DetectorCard.tsx @@ -1,5 +1,6 @@ import { useNavigate } from 'react-router-dom' import { theme } from '@/theme' +import { TypographyH3 } from '@/components/ui/typography' interface DetectorCardProps { detector: { @@ -36,9 +37,9 @@ export const DetectorCard = ({ detector }: DetectorCardProps) => { e.currentTarget.style.transform = 'translateY(0)' }} > -

+ {detector.name} -

+
Type: {detector.type?.name || 'N/A'} diff --git a/frontend/src/features/devices/pages/DetectorCreatePage.tsx b/frontend/src/features/devices/pages/DetectorCreatePage.tsx index 115d55f..fc6b7c5 100644 --- a/frontend/src/features/devices/pages/DetectorCreatePage.tsx +++ b/frontend/src/features/devices/pages/DetectorCreatePage.tsx @@ -67,7 +67,7 @@ export const DetectorCreatePage = () => { e.preventDefault(); setError(null); try { - await createMutation.mutateAsync({ + const response = await createMutation.mutateAsync({ data: { sn, name, @@ -77,7 +77,7 @@ export const DetectorCreatePage = () => { ...(imageFile ? { image: imageFile } : {}), } as DetectorRequest, }); - navigate("/logbooks"); + navigate(`/device/${response.data.id}`); } catch (err) { const detail = axios.isAxiosError(err) ? (err.response?.data as Record | undefined)?.detail ?? err.message diff --git a/frontend/src/features/devices/pages/DevicePage.tsx b/frontend/src/features/devices/pages/DevicePage.tsx index 509c8bc..414ad03 100644 --- a/frontend/src/features/devices/pages/DevicePage.tsx +++ b/frontend/src/features/devices/pages/DevicePage.tsx @@ -1,9 +1,13 @@ import { useState } from 'react' -import { useParams, useNavigate } from 'react-router-dom' -import { Button } from '@/shared/components/Button/Button' +import { Link, useParams, useNavigate } from 'react-router-dom' +import { Button as ShadcnButton, buttonVariants } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import ReactMarkdown from 'react-markdown' import { PageLayout } from '@/shared/components/Layout/PageLayout' import { Section } from '@/shared/components/Layout/Section' +import { SortableTable } from '@/shared/components/SortableTable/SortableTable' +import type { TableColumn } from '@/shared/components/SortableTable/SortableTable' import { theme } from '@/theme' import logbookBg from '@/assets/img/SPACEDOS01.jpg' import { @@ -11,8 +15,27 @@ import { useLogbooksList, detectorsQrRetrieve, } from '@/api/detectors/detectors' -import type { DetectorLogbook } from '@/api/model' +import { useSpectralRecordsList } from '@/api/spectral-records/spectral-records' +import type { DetectorLogbook, SpectralRecord } from '@/api/model' import axios from 'axios' +import { TypographyH3 } from '@/components/ui/typography' +import { formatDate } from '@/shared/utils/formatDate' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { DownloadIcon, SquarePenIcon, UploadIcon } from 'lucide-react' + +const PROCESSING_STATUS_LABELS: Record = { + PENDING: 'Pending', + IN_PROGRESS: 'In Progress', + COMPLETED: 'Completed', + FAILED: 'Failed', +} + +const PROCESSING_STATUS_COLORS: Record = { + PENDING: theme.colors.muted, + IN_PROGRESS: theme.colors.primary, + COMPLETED: theme.colors.success, + FAILED: theme.colors.danger, +} export const DevicePage = () => { const { id } = useParams<{ id: string }>() @@ -21,20 +44,83 @@ export const DevicePage = () => { const detectorQuery = useDetectorsRetrieve(id ?? '', { query: { enabled: !!id } }) const logbookQuery = useLogbooksList({ detector: id }, { query: { enabled: !!id } }) + const recordsQuery = useSpectralRecordsList(undefined, { query: { enabled: !!id } }) const detector = detectorQuery.data?.data const logbook: DetectorLogbook[] = logbookQuery.data?.data?.results ?? [] + const records: SpectralRecord[] = (recordsQuery.data?.data?.results ?? []).filter( + (record) => record.detector === id, + ) const loading = detectorQuery.isLoading || logbookQuery.isLoading + const recordsLoading = recordsQuery.isLoading const queryError = detectorQuery.error ? `Failed to load detector: ${(detectorQuery.error as Error).message}` : logbookQuery.error ? `Failed to load logbook: ${(logbookQuery.error as Error).message}` + : recordsQuery.error + ? `Failed to load spectral records: ${(recordsQuery.error as Error).message}` : null const error = queryError ?? qrError + const columns: TableColumn[] = [ + { + id: 'name', + key: 'name', + label: 'Name', + render: (value) => ( + + {String(value)} + + ), + }, + { + id: 'processing_status', + key: 'processing_status', + label: 'Status', + render: (value) => { + const status = String(value ?? '') + const label = PROCESSING_STATUS_LABELS[status] || status || '—' + const color = PROCESSING_STATUS_COLORS[status] || theme.colors.textSecondary + return {label} + }, + }, + { + id: 'owner', + key: 'owner', + label: 'Owner', + render: (_value, row) => + row.owner?.name ? row.owner.name : , + }, + { + id: 'author', + key: 'author', + label: 'Author', + render: (_value, row) => + row.author?.username ? `@${row.author.username}` : , + }, + { + id: 'artifacts_count', + key: 'artifacts_count', + label: 'Artifacts', + align: 'center', + render: (value) => String(value || 0), + }, + { + id: 'created', + key: 'created', + label: 'Created', + render: (value) => formatDate(value as string), + }, + ] + const handleQrDownload = async () => { if (!detector) return setQrError(null) @@ -69,26 +155,20 @@ export const DevicePage = () => { >
- QR - + + + + QR + + + + Download PNG with QR code + + ) : undefined } > @@ -104,16 +184,11 @@ export const DevicePage = () => { marginBottom: theme.spacing.lg, }} > -

+ Detector Information -

+
{
)} -
-

- Logbook Entries -

- {logbook.length === 0 && !loading ? ( -

No logbook entries yet.

- ) : ( -
    - {logbook.map((item) => ( -
  • -
    - {item.entry_type} - - {item.location_text && ( - - {item.location_text} - - )} - {item.author?.username && ( - - by @{item.author.username} - - )} - -
    -
    - {item.text} -
    - {item.modified && ( -
    - Last modified: {new Date(item.modified).toLocaleString()} - {item.modified_by?.username && ( - by @{item.modified_by.username} + + + Service Logbook + Data + + + + {logbook.length === 0 && !loading ? ( +

    No logbook entries yet.

    + ) : ( +
      + {logbook.map((item) => ( +
    • +
      + {item.entry_type} + + {item.location_text && ( + {item.location_text} )} + {item.author?.username && ( + by @{item.author.username} + )} +
      - )} - {item.latitude && item.longitude ? ( - - View on map - - ) : null} -
    • - ))} -
    - )} -
    +
    + {item.text} +
    + {item.modified && ( +
    + Last modified: {new Date(item.modified).toLocaleString()} + {item.modified_by?.username && ( + by @{item.modified_by.username} + )} +
    + )} + {item.latitude && item.longitude ? ( + + + View on map + + + ) : null} +
  • + ))} +
+ )} + + + + + + Add Entry + + + + Create new service logbook record + + + - + + {recordsLoading ? ( +
+ Loading spectral records... +
+ ) : ( + { + navigate(`/spectral-record/${record.id}`) + }} + defaultSortField="created" + defaultSortDirection="desc" + getRowKey={(record) => record.id} + emptyMessage="No spectral records available for this detector." + /> + )} + + + + Upload Logs + + +
+
) diff --git a/frontend/src/features/devices/pages/LogbookEntryPage.tsx b/frontend/src/features/devices/pages/LogbookEntryPage.tsx index 0b677d0..a4a5302 100644 --- a/frontend/src/features/devices/pages/LogbookEntryPage.tsx +++ b/frontend/src/features/devices/pages/LogbookEntryPage.tsx @@ -7,6 +7,7 @@ import logbookBg from '@/assets/img/SPACEDOS01.jpg' import { useDetectorsRetrieve, useLogbooksRetrieve, useLogbooksCreate, useLogbooksUpdate } from '@/api/detectors/detectors' import type { EntryTypeEnum, SourceEnum } from '@/api/model' import { Button } from '@/shared/components/Button/Button' +import { TypographyH2, TypographyH3 } from '@/components/ui/typography' export const LogbookEntryPage = () => { const { id, entryId } = useParams<{ id: string; entryId?: string }>() @@ -119,6 +120,7 @@ export const LogbookEntryPage = () => { return ( +
@@ -128,9 +130,9 @@ export const LogbookEntryPage = () => { > ← Back to Logbook -

+ {isEditMode ? 'Edit Logbook Entry' : 'Create Logbook Entry'} -

+ {detector && (

{detector.name} (TYPE: {detector.type?.name} · ID: {detector.id}) @@ -227,9 +229,9 @@ export const LogbookEntryPage = () => { marginBottom: theme.spacing['2xl'] }}>

-

+ Location (Optional) -

+ + } > {/* Row 1: Records + Segments */} @@ -392,7 +400,7 @@ export const MeasurementEditorPage = () => { {/* Records panel */}
setShowModal(true)} disabled={isLoading}>+ Add Records + setShowModal(true)} disabled={isLoading}>+ Add Records }> {isLoading ? (
@@ -412,6 +420,8 @@ export const MeasurementEditorPage = () => { )}
+ + {/* Segments panel */}
{pendingRange && ( @@ -433,10 +443,10 @@ export const MeasurementEditorPage = () => {
)}
- - + + setPendingRange(null)}>✕
)} { const queryClient = useQueryClient() @@ -133,7 +134,7 @@ export const ProfilePage = () => {
{/* About Section */} -
+
-

About DOSPORTAL

+ About DOSPORTAL -

+ This web application has been developed and maintained by UST (Universal Scientific Technologies s.r.o.), a company specializing in the manufacturing and development of both semiconductor and scintillation detectors. Our extensive range of detectors includes SPACEDOS, AIRDOS, and LABDOS, which are designed to meet the diverse needs of scientific applications. -

+ -

+ Semiconductor detectors, based on solid-state physics principles, utilize semiconductor materials to detect radiation. These detectors offer excellent energy resolution and are widely used in fields such as nuclear physics, materials science, and environmental monitoring. -

+ -

+ Scintillation detectors, on the other hand, employ scintillating materials that emit light when interacting with radiation. These detectors are highly efficient and find applications in areas such as medical imaging, homeland security, and nuclear power plants. -

+ -

+ UST collaborates closely with the Nuclear Physics Institute of the Czech Academy of Sciences to develop advanced detectors. This collaboration ensures that our detectors meet the rigorous standards required for scientific research and personal dosimetry. -

+ -

+ This website serves as a comprehensive portal for accessing fundamental information about measurements conducted with our detectors. Users can browse through individual records or contribute their own measurements to further scientific understanding. -

+ -

+ For more in-depth information about our detectors, please visit the official UST website or refer to the documentation pages, where you will find detailed specifications and technical details about our semiconductor and scintillation detectors. -

+ + + + {/* */} + Version: {import.meta.env.VITE_GIT_BRANCH || 'unknown'}@{(import.meta.env.VITE_GIT_COMMIT || 'dev').slice(0, 7)} + {/* */} + -
- Version: {import.meta.env.VITE_GIT_BRANCH || 'unknown'}@{(import.meta.env.VITE_GIT_COMMIT || 'dev').slice(0, 7)} -
diff --git a/frontend/src/shared/components/Layout/PanelHeader.tsx b/frontend/src/shared/components/Layout/PanelHeader.tsx index 1d61994..fd90edf 100644 --- a/frontend/src/shared/components/Layout/PanelHeader.tsx +++ b/frontend/src/shared/components/Layout/PanelHeader.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import { theme } from '@/theme' +import { TypographyH2 } from '@/components/ui/typography' interface PanelHeaderProps { title: string @@ -10,7 +11,7 @@ interface PanelHeaderProps { export const PanelHeader = ({ title, subtitle, actions }: PanelHeaderProps) => (
-

{title}

+ {title} {subtitle && (

{subtitle} diff --git a/frontend/src/shared/components/Layout/Section.tsx b/frontend/src/shared/components/Layout/Section.tsx index 8059a27..cdd8d75 100644 --- a/frontend/src/shared/components/Layout/Section.tsx +++ b/frontend/src/shared/components/Layout/Section.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom' import type { ReactNode } from 'react' import { theme } from '@/theme' +import { TypographyH3 } from '@/components/ui/typography' interface SectionProps { title?: string @@ -41,7 +42,7 @@ export const Section = ({ title, subtitle, actions, error, backLink, children, s ← {backLink.label} )} - {title &&

{title}

} + {title && {title}} {subtitle && (

{subtitle} diff --git a/frontend/src/shared/components/Navbar/Navbar.tsx b/frontend/src/shared/components/Navbar/Navbar.tsx index 973a201..942c3a8 100644 --- a/frontend/src/shared/components/Navbar/Navbar.tsx +++ b/frontend/src/shared/components/Navbar/Navbar.tsx @@ -76,9 +76,9 @@ export const Navbar = ({

{isAuthed ? ( <> - {/* commented out because not ready to show dark theme to users yet */} +
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index 3386fb2..9bd873c 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -4,38 +4,38 @@ export const theme = { colors: { - text: 'var(--legacy-text)', - textDark: 'var(--legacy-text-dark)', - textSecondary: 'var(--legacy-text-secondary)', - muted: 'var(--legacy-muted)', - mutedLight: 'var(--legacy-muted-light)', - mutedLighter: 'var(--legacy-muted-lighter)', + text: 'var(--text)', + textDark: 'var(--text-dark)', + textSecondary: 'var(--text-secondary)', + muted: 'var(--muted-foreground)', + mutedLight: 'var(--muted-light)', + mutedLighter: 'var(--muted-lighter)', - bg: 'var(--legacy-bg)', - card: 'var(--legacy-card)', + bg: 'var(--background)', + card: 'var(--card)', - border: 'var(--legacy-border)', + border: 'var(--border)', - primary: 'var(--legacy-primary)', - primaryHover: 'var(--legacy-primary-hover)', - primaryLight: 'var(--legacy-primary-light)', + primary: 'var(--primary)', + primaryHover: 'var(--primary-hover)', + primaryLight: 'var(--primary-light)', - success: 'var(--legacy-success)', - successLight: 'var(--legacy-success-light)', - successBg: 'var(--legacy-success-bg)', - successText: 'var(--legacy-success-text)', - successBorder: 'var(--legacy-success-border)', + success: 'var(--success)', + successLight: 'var(--success-light)', + successBg: 'var(--success-bg)', + successText: 'var(--success-text)', + successBorder: 'var(--success-border)', - danger: 'var(--legacy-danger)', + danger: 'var(--destructive)', - warning: 'var(--legacy-warning)', - warningBg: 'var(--legacy-warning-bg)', - warningBorder: 'var(--legacy-warning-border)', + warning: 'var(--warning)', + warningBg: 'var(--warning-bg)', + warningBorder: 'var(--warning-border)', - infoBg: 'var(--legacy-info-bg)', - infoBgGreen: 'var(--legacy-info-bg-green)', - infoBorder: 'var(--legacy-info-border)', - uploadHoverBg: 'var(--legacy-upload-hover-bg)', + infoBg: 'var(--info-bg)', + infoBgGreen: 'var(--info-bg-green)', + infoBorder: 'var(--info-border)', + uploadHoverBg: 'var(--upload-hover-bg)', }, spacing: {