riligar-dev-dashboard — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited riligar-dev-dashboard (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Regras concretas baseadas na arquitetura real dos projetos. Não são genéricas — são do código que existe.
[!IMPORTANT] Sempre respeite também: - @[.agent/skills/riligar-design-system] — UI exclusivo via Mantine, zero CSS - Rules em .agent/rules/ — clean-code, naming-conventions, code-style, javascript-only - references/dependencies.md — Pacotes e versões do frontend, config Vitesrc/
├── components/ # Componentes reutilizáveis
│ ├── Sidebar.jsx # PascalCase (SEMPRE)
│ ├── RichEditor.jsx
│ └── MediaLibrary.jsx
├── pages/ # Uma pasta por página/feature
│ ├── home.jsx # Arquivo raiz da página: kebab-case
│ ├── editor/
│ │ └── index.jsx
│ └── feeds/
│ ├── index.jsx
│ └── FeedConfig.jsx # Sub-componentes: PascalCase
├── store/ # Zustand stores — um arquivo por domínio
│ ├── auth-store.js
│ ├── feed-store.js
│ └── post-store.js
├── services/ # Chamadas HTTP — um arquivo por domínio
│ ├── api.js # Instância base do cliente HTTP
│ ├── feeds.js
│ └── posts.js
├── constants/ # Constantes estáticas do app
├── i18n/ # Internacionalização
│ ├── index.js
│ └── locales/
└── hooks/ # Custom hooks compartilhados| Tipo | Convenção | Exemplo |
|---|---|---|
| Componentes (reutilizáveis) | PascalCase | MediaLibrary.jsx |
| Página raiz | kebab-case | home.jsx, index.jsx |
| Sub-componentes de página | PascalCase | FeedConfig.jsx |
| Stores | kebab-case + sufixo -store | feed-store.js |
| Services | kebab-case | feeds.js |
| Hooks | camelCase com prefixo use | useDebounce.js |
Estado global sempre Zustand. Sem Context para estado compartilhado. Sem Redux.
import { create } from 'zustand'
import { feedsService } from '../services/feeds'
const useFeedStore = create((set, get) => ({
feeds: [],
activeFeed: null,
isLoading: false,
fetchFeeds: async () => {
set({ isLoading: true })
const feeds = await feedsService.getAll()
set({ feeds, isLoading: false })
},
setActiveFeed: (feed) => set({ activeFeed: feed }),
}))get isTrialing() {}) — não são reativos no Zustand. Derive no componente ou use um selector.persist middleware apenas quando necessário (ex: activeFeed).useStore((state) => state.feeds) — não useStore() inteiro.// ❌ Getter não-reativo — nunca vai atualizar o componente
const useSubscriptionStore = create((set) => ({
subscription: null,
get isTrialing() { return this.subscription?.status === 'trialing' }
}))
// ✅ Derive no componente
const subscription = useSubscriptionStore((s) => s.subscription)
const isTrialing = subscription?.status === 'trialing't()O projeto usa i18next com 3 idiomas (pt-BR, en, es). Nenhuma string visível ao usuário pode ser hardcoded.
import { useTranslation } from 'react-i18next'
const MyComponent = () => {
const { t } = useTranslation()
return (
<Button onClick={handleSave}>{t('common.save')}</Button>
)
}t(). Sem isso, crash em runtime.t('namespace.key') — nunca hardcode.t().feeds., posts., media.) + common. para compartilhados.// ❌ String hardcoded visível ao usuário
<Button>Salvar Alterações</Button>
<Text>Nenhuma mídia encontrada.</Text>
// ❌ t() sem import — crash
const handleSave = async () => {
showNotification({ title: t('common.success') }) // ReferenceError
}// ✅
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
<Button>{t('common.save')}</Button>
<Text>{t('media.empty')}</Text>Todas as chamadas de API vão por services/. Componentes e stores não chamam HTTP diretamente.
// services/feeds.js
import { api } from './api'
export const feedsService = {
getAll: () => api.get('feeds').json(),
getById: (id) => api.get(`feeds/${id}`).json(),
create: (data) => api.post('feeds', { json: data }).json(),
update: (id, data) => api.put(`feeds/${id}`, { json: data }).json(),
remove: (id) => api.delete(`feeds/${id}`).json(),
}api.js tem a instância base com token de auth e tratamento de erro.api.js. Nunca redefina em outro arquivo.HoldButton / ButtonDelete do components/Buttons.jsx. Nunca usar confirm() nativo.styles, withBorder, hover via &:hover no styles). Nunca manipular DOM diretamente via e.currentTarget.style.// ❌ Manipulação direta de DOM
onMouseEnter={(e) => e.currentTarget.style.background = '#eee'}
// ✅ Mantine styles prop
styles={{ root: { '&:hover': { backgroundColor: 'var(--mantine-color-dimmed)' } } }}Se você precisa de um Center, TextInput, Button — use o do Mantine. Nunca crie um local com o mesmo nome, isso gera shadow e confusão.
constants/ ou .env.// ❌
const message = `Olá, preciso de ajuda com meu plano.`
// ✅ constants/whatsapp.js
export const WHATSAPP_SUPPORT_NUMBER = '...'
export const WHATSAPP_SUPPORT_MESSAGE = '...' // ou via i18n se traduzível| ❌ Problema real encontrado | ✅ Como resolver |
|---|---|
slugify duplicado em 2 arquivos | Extrair para utils/slugify.js |
stripHtml duplicado em 2 arquivos | Extrair para utils/stripHtml.js |
API_URL redefinido fora do api.js | Importar do services/api.js |
t() usado sem useTranslation importado | Sempre verificar import |
| Componente Mantine shadowed por local | Deletar o local, usar Mantine |
| Código comentado espalhado | Deletar. Se precisa, usa git. |
confirm() misturado com HoldButton | Usa HoldButton sempre |
<style> tag com CSS raw | Usa styles prop do Mantine (exceção: libs externas como ProseMirror que precisam de CSS global) |
| Getters no Zustand store | Derive no componente |
onMouseEnter manipulando style diretamente | Usa styles prop do Mantine |
Estruturas que repetem pela codebase. Copie o esqueleto, ajuste apenas o conteúdo domínio-específico.
Presente em todas as pages. Estrutura idêntica sempre:
<Box py="xl">
<Group justify="space-between" align="flex-end" mb="xl">
<Stack gap={0}>
<Text size="xs" fw={700} c="dimmed" tt="uppercase" lts="0.1em">
{t('namespace.subtitle')}
</Text>
<Title order={1} style={{ letterSpacing: '-0.04em' }}>
{t('namespace.title')}
</Title>
</Stack>
{/* CTA opcional — ex: <Button leftSection={<IconPlus size={16} />}> */}
</Group>
{/* Conteúdo da página */}
</Box>Usado quando uma lista está vazia. Card com borda dashed, icone grande, texto e CTA:
<Card padding="xl" radius="md" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
<Stack align="center" py="xl">
<IconDominio size={48} stroke={1} color="var(--mantine-color-gray-2)" />
<Text c="dimmed" size="sm">{t('namespace.emptyMessage')}</Text>
<Button onClick={handleCreate} leftSection={<IconPlus size={16} />}>
{t('namespace.createFirst')}
</Button>
</Stack>
</Card>Loader só aparece quando não há dados ainda (não sobrescreve lista existente):
{loading && data.length === 0 ? (
<Center style={{ height: 300 }}>
<Loader />
</Center>
) : (
/* conteúdo normal */
)}Layout responsivo padrão para listas de cards:
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }}>
{items.map((item) => (
<Card key={item.id} padding="lg" radius="md" withBorder>
{/* conteúdo do card */}
</Card>
))}
</SimpleGrid>Para galerias (mais itens pequenos): cols={{ base: 1, sm: 2, md: 3, lg: 4 }}
Sempre usa useDisclosure. Header com borderBottom específico:
const [opened, { open, close }] = useDisclosure(false)
<Modal
opened={opened}
onClose={close}
centered
radius="md"
padding="xl"
title={<Text fw={700}>{t('namespace.modalTitle')}</Text>}
styles={{ header: { borderBottom: '1px solid var(--mantine-color-gray-2)', marginBottom: 20 } }}
>
{/* corpo do modal */}
</Modal>Para múltiplos modais na mesma page, renomeia as funções: { open: openEdit, close: closeEdit }
Mapeia status → configuração visual via função que recebe t:
const getStatusConfig = (t) => ({
draft: { color: 'gray', icon: <IconCircleDotted size={16} />, label: t('posts.status.draft') },
scheduled: { color: 'blue', icon: <IconClock size={16} />, label: t('posts.status.scheduled') },
published: { color: 'green', icon: <IconCircleCheck size={16} />, label: t('posts.status.published') },
failed: { color: 'red', icon: <IconCircleX size={16} />, label: t('posts.status.failed') },
})
// Uso
const config = getStatusConfig(t)[status]
<Badge variant="dot" color={config.color}>{config.label}</Badge>Filtro local sem chamada de API — estado local + filter inline:
const [search, setSearch] = useState('')
const filtered = items.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
)
<TextInput
placeholder={t('common.search')}
leftSection={<IconSearch size={16} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>Template exato que todas as actions seguem. Imports do service como namespace:
import { create } from 'zustand'
import * as feedsService from '../services/feeds.js'
export const useFeedStore = create((set) => ({
feeds: [],
loading: false,
error: null,
fetchFeeds: async () => {
set({ loading: true, error: null })
try {
const feeds = await feedsService.getAll()
set({ feeds, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
throw error
}
},
}))Nota: services são importados como import * as service (namespace), não como objeto exportado.
Atualizações imutáveis via set(state => ...) com spread + map/filter:
// Atualizar item na lista
updateFeed: (id, data) => set((state) => ({
feeds: state.feeds.map((f) => (f.id === id ? { ...f, ...data } : f))
})),
// Remover item
removeFeed: (id) => set((state) => ({
feeds: state.feeds.filter((f) => f.id !== id)
})),
// Adicionar item
addFeed: (feed) => set((state) => ({
feeds: [...state.feeds, feed]
})),Shape e convenção de cores consistente em toda a app:
import { notifications } from '@mantine/notifications'
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react'
// ✅ Sucesso
notifications.show({
title: t('common.success'),
message: t('namespace.savedMessage'),
color: 'green',
icon: <IconCheck size={18} />,
})
// ✅ Erro
notifications.show({
title: t('common.error'),
message: error.message,
color: 'red',
icon: <IconX size={18} />,
})
// ✅ Warning
notifications.show({
title: t('common.warning'),
message: t('namespace.warningMessage'),
color: 'yellow',
icon: <IconAlertCircle size={18} />,
})Icones de notification: sempre size={18}. Mensagens de erro: usa error.message diretamente (já vem traduzido do backend).
Locale do dayjs sincroniza com o idioma do i18n:
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useTranslation } from 'react-i18next'
dayjs.extend(relativeTime)
const MyComponent = () => {
const { i18n } = useTranslation()
useEffect(() => {
dayjs.locale(i18n.language)
}, [i18n.language])
// Formatos usados no projeto:
// dayjs(date).format('DD MMM, HH:mm') — compacto com hora
// dayjs(date).format('DD/MM/YYYY [at] HH:mm') — completo
// dayjs(date).fromNow() — relativo ("há 2 dias")
}Componente que protege rotas verificando estado do store:
import { Navigate } from 'react-router-dom'
import { useFeedStore } from '../store/feed-store.js'
const RequireFeed = ({ children }) => {
const activeFeed = useFeedStore((s) => s.activeFeed)
if (!activeFeed) return <Navigate to="/" />
return children
}Usado na definição de rotas: <RequireFeed><EditorPage /></RequireFeed>
Após redirects externos (OAuth, Stripe checkout), status vem via query params:
import { useSearchParams } from 'react-router-dom'
const SubscriptionPage = () => {
const [searchParams, setSearchParams] = useSearchParams()
const { t } = useTranslation()
useEffect(() => {
if (searchParams.get('success')) {
notifications.show({ title: t('common.success'), message: t('subscription.successMessage'), color: 'green', icon: <IconCheck size={18} /> })
setSearchParams({})
} else if (searchParams.get('canceled')) {
notifications.show({ title: t('common.warning'), message: t('subscription.canceledMessage'), color: 'yellow', icon: <IconAlertCircle size={18} /> })
setSearchParams({})
}
}, [searchParams])
}Padrão usado no editor — debounce com state machine de status:
const [saveStatus, setSaveStatus] = useState('idle') // 'idle' | 'saving' | 'saved'
useEffect(() => {
if (!content) return
const timeout = setTimeout(async () => {
setSaveStatus('saving')
try {
await postsService.update(postId, { content })
setSaveStatus('saved')
// Reset para idle após 3s
setTimeout(() => setSaveStatus('idle'), 3000)
} catch {
setSaveStatus('idle')
}
}, 2000) // debounce de 2s
return () => clearTimeout(timeout)
}, [content, postId])Hierarquia consistente — sempre de @tabler/icons-react:
| Contexto | Size | Exemplo |
|---|---|---|
| Menu items / nav | 14 | Sidebar links |
| Inline / badges | 16 | Botões, labels, leftSection |
| Notifications | 18 | Icons nas notifications |
| Card headers | 20 | Ação principal do card |
| Feature cards | 24 | Cards de destaque |
| Empty states | 48 | Icone do empty state (com stroke={1}) |
Empty states usam stroke={1} para parecer mais leve. Icones decorativos genéricos usam stroke={1.5}.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.