Files
Keys/keys.html

2019 lines
91 KiB
HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VaultUI - Gerenciador de Credenciais</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-tertiary: #1a1a24;
--bg-card: #15151f;
--border-color: #2a2a3a;
--border-glow: #3a3a4a;
--accent-cyan: #00d4ff;
--accent-purple: #7c3aed;
--accent-green: #10b981;
--accent-red: #ef4444;
--accent-yellow: #f59e0b;
--accent-orange: #f97316;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'Inter', system-ui, sans-serif;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--shadow-glow: 0 0 20px rgba(0, 212, 255, 0.15);
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse at 20% 20%, rgba(124, 58, 237, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(0, 212, 255, 0.06) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Login Screen */
.login-screen {
position: fixed;
inset: 0;
background: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
transition: var(--transition);
}
.login-screen.hidden {
opacity: 0;
pointer-events: none;
}
.login-box {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 40px;
width: 100%;
max-width: 400px;
box-shadow: var(--shadow-card), var(--shadow-glow);
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.login-logo {
text-align: center;
margin-bottom: 30px;
}
.login-logo svg {
width: 64px;
height: 64px;
color: var(--accent-cyan);
filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.5));
}
.login-logo h1 {
font-family: var(--font-mono);
font-size: 1.5rem;
color: var(--text-primary);
margin-top: 10px;
}
.login-logo p {
color: var(--text-muted);
font-size: 0.875rem;
margin-top: 5px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 0.9rem;
transition: var(--transition);
}
.form-group input:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 20px;
border: none;
border-radius: var(--radius-sm);
font-family: var(--font-sans);
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-cyan), #0099cc);
color: var(--bg-primary);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.4);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
border-color: var(--accent-cyan);
background: rgba(0, 212, 255, 0.1);
}
.btn-danger {
background: rgba(239, 68, 68, 0.1);
color: var(--accent-red);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn-danger:hover {
background: rgba(239, 68, 68, 0.2);
}
.btn-icon {
padding: 8px;
background: transparent;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
}
.btn-icon:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: rgba(0, 212, 255, 0.1);
}
.btn-icon svg {
width: 18px;
height: 18px;
}
.login-error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--accent-red);
padding: 12px;
border-radius: var(--radius-sm);
font-size: 0.875rem;
margin-bottom: 20px;
display: none;
animation: shake 0.5s ease;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
/* Header */
.header {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 16px 24px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
box-shadow: var(--shadow-card);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo svg {
width: 36px;
height: 36px;
color: var(--accent-cyan);
filter: drop-shadow(0 0 8px rgba(0, 212, 255, 0.5));
}
.logo h1 {
font-family: var(--font-mono);
font-size: 1.25rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stats {
display: flex;
gap: 20px;
}
.stat {
text-align: center;
}
.stat-value {
font-family: var(--font-mono);
font-size: 1.25rem;
font-weight: 700;
color: var(--accent-cyan);
}
.stat-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.header-actions {
display: flex;
gap: 8px;
}
/* Search & Filters */
.filters-section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
margin-bottom: 24px;
box-shadow: var(--shadow-card);
}
.search-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.search-input {
flex: 1;
display: flex;
align-items: center;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0 16px;
transition: var(--transition);
}
.search-input:focus-within {
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
.search-input svg {
width: 20px;
height: 20px;
color: var(--text-muted);
}
.search-input input {
flex: 1;
padding: 12px;
background: transparent;
border: none;
color: var(--text-primary);
font-size: 0.9rem;
}
.search-input input::placeholder {
color: var(--text-muted);
}
.search-input input:focus {
outline: none;
}
.filter-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.service-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
.service-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 20px;
color: var(--text-secondary);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.service-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: rgba(0, 212, 255, 0.1);
}
.service-btn.active {
background: rgba(0, 212, 255, 0.15);
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.service-btn .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.service-label {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.filter-select {
padding: 10px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 0.875rem;
cursor: pointer;
transition: var(--transition);
min-width: 150px;
}
.filter-select:focus {
outline: none;
border-color: var(--accent-cyan);
}
.filter-select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
.filter-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.filter-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 20px;
font-size: 0.75rem;
color: var(--accent-cyan);
}
.filter-tag button {
background: none;
border: none;
color: var(--accent-cyan);
cursor: pointer;
padding: 0;
display: flex;
}
.filter-tag button:hover {
color: var(--accent-red);
}
/* Cards Grid */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
@media (max-width: 480px) {
.cards-grid {
grid-template-columns: 1fr;
}
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
transition: var(--transition);
box-shadow: var(--shadow-card);
animation: cardFadeIn 0.4s ease;
}
@keyframes cardFadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card:hover {
border-color: var(--border-glow);
box-shadow: var(--shadow-card), 0 0 30px rgba(0, 212, 255, 0.1);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.card-title {
display: flex;
align-items: center;
gap: 10px;
}
.card-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 212, 255, 0.1);
border-radius: var(--radius-sm);
color: var(--accent-cyan);
}
.card-icon svg {
width: 20px;
height: 20px;
}
.card-title h3 {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
}
.card-actions {
display: flex;
gap: 6px;
}
.card-actions .btn-icon {
padding: 6px;
}
.card-actions .btn-icon svg {
width: 16px;
height: 16px;
}
.card-badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-type {
background: rgba(124, 58, 237, 0.15);
color: #a78bfa;
border: 1px solid rgba(124, 58, 237, 0.3);
}
.badge-category {
background: rgba(16, 185, 129, 0.15);
color: var(--accent-green);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.badge-paid {
background: rgba(245, 158, 11, 0.15);
color: var(--accent-yellow);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.badge-expiring {
background: rgba(249, 115, 22, 0.15);
color: var(--accent-orange);
border: 1px solid rgba(249, 115, 22, 0.3);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.card-value {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 12px;
margin-bottom: 12px;
}
.card-value-label {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.card-value-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.card-value-text {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--accent-cyan);
word-break: break-all;
flex: 1;
}
.card-value-text.masked {
color: var(--text-muted);
}
.card-meta {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.card-meta-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
color: var(--text-secondary);
}
.card-meta-item svg {
width: 14px;
height: 14px;
color: var(--text-muted);
}
.card-meta-item a {
color: var(--accent-cyan);
text-decoration: none;
}
.card-meta-item a:hover {
text-decoration: underline;
}
.card-login-info {
display: flex;
gap: 12px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--border-color);
}
.card-login-field {
flex: 1;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 8px 10px;
}
.card-login-field-label {
font-size: 0.65rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.card-login-field-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.card-login-field-text {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--accent-cyan);
word-break: break-all;
}
.card-login-field-text.masked {
color: var(--text-muted);
}
.card-login-copy {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 2px;
display: flex;
transition: var(--transition);
}
.card-login-copy:hover {
color: var(--accent-cyan);
}
.card-login-copy svg {
width: 14px;
height: 14px;
}
/* Toast */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-card);
animation: toastIn 0.3s ease;
min-width: 280px;
}
@keyframes toastIn {
from { opacity: 0; transform: translateX(100px); }
to { opacity: 1; transform: translateX(0); }
}
.toast.success {
border-color: var(--accent-green);
}
.toast.success svg {
color: var(--accent-green);
}
.toast.error {
border-color: var(--accent-red);
}
.toast.error svg {
color: var(--accent-red);
}
.toast svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.toast span {
font-size: 0.875rem;
color: var(--text-primary);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1500;
opacity: 0;
pointer-events: none;
transition: var(--transition);
padding: 20px;
}
.modal-overlay.active {
opacity: 1;
pointer-events: all;
}
.modal {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
transform: scale(0.9);
transition: var(--transition);
}
.modal-overlay.active .modal {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
font-size: 1.125rem;
font-weight: 600;
}
.modal-body {
padding: 24px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid var(--border-color);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 480px) {
.form-row {
grid-template-columns: 1fr;
}
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-group textarea,
.form-group input {
width: 100%;
padding: 12px 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 0.9rem;
transition: var(--transition);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--accent-cyan);
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
}
.paid-section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 16px;
margin-top: 16px;
}
.paid-section h4 {
font-size: 0.875rem;
font-weight: 600;
color: var(--accent-yellow);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.checkbox-group input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: var(--accent-cyan);
cursor: pointer;
}
.checkbox-group label {
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
}
.paid-fields {
display: none;
}
.paid-fields.show {
display: block;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 1.125rem;
color: var(--text-secondary);
margin-bottom: 8px;
}
.empty-state p {
font-size: 0.875rem;
}
/* Confirm Dialog */
.confirm-dialog {
text-align: center;
}
.confirm-dialog svg {
width: 60px;
height: 60px;
color: var(--accent-red);
margin-bottom: 20px;
}
.confirm-dialog h3 {
font-size: 1.125rem;
margin-bottom: 10px;
}
.confirm-dialog p {
color: var(--text-secondary);
font-size: 0.875rem;
margin-bottom: 24px;
}
.confirm-actions {
display: flex;
justify-content: center;
gap: 12px;
}
/* Responsive */
@media (max-width: 768px) {
.container {
padding: 12px;
}
.header {
padding: 12px 16px;
}
.header-left {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.stats {
width: 100%;
justify-content: space-around;
}
.search-row {
flex-direction: column;
}
.filter-row {
flex-direction: column;
}
.filter-select {
width: 100%;
}
.header-actions {
width: 100%;
justify-content: flex-end;
}
.modal-footer {
flex-direction: column;
}
.modal-footer .btn {
width: 100%;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-glow);
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-cyan);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* No results */
.no-results {
text-align: center;
padding: 40px;
color: var(--text-muted);
}
.no-results svg {
width: 48px;
height: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
</style>
</head>
<body>
<!-- Login Screen -->
<div class="login-screen" id="loginScreen">
<div class="login-box">
<div class="login-logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
<circle cx="12" cy="16" r="1"></circle>
</svg>
<h1>VaultUI</h1>
<p>Gerenciador de Credenciais</p>
</div>
<div class="login-error" id="loginError">
Usuário ou senha incorretos
</div>
<form id="loginForm">
<div class="form-group">
<label for="username">Usuário</label>
<input type="text" id="username" placeholder="Digite seu usuário" autocomplete="username" required>
</div>
<div class="form-group">
<label for="password">Senha</label>
<input type="password" id="password" placeholder="Digite sua senha" autocomplete="current-password" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
<polyline points="10 17 15 12 10 7"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
Entrar
</button>
</form>
</div>
</div>
<!-- Main App -->
<div class="container" id="appContainer" style="display: none;">
<!-- Header -->
<header class="header">
<div class="header-left">
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
<circle cx="12" cy="16" r="1"></circle>
</svg>
<h1>VaultUI</h1>
</div>
<div class="stats">
<div class="stat">
<div class="stat-value" id="totalCount">0</div>
<div class="stat-label">Total</div>
</div>
<div class="stat">
<div class="stat-value" id="paidCount">0</div>
<div class="stat-label">Pagas</div>
</div>
<div class="stat">
<div class="stat-value" id="expiringCount">0</div>
<div class="stat-label">Expirando</div>
</div>
</div>
</div>
<div class="header-actions">
<button class="btn-icon" id="exportBtn" title="Exportar JSON">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button class="btn-icon" id="refreshBtn" title="Atualizar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
<button class="btn btn-primary" id="addBtn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Novo
</button>
<button class="btn btn-secondary" id="logoutBtn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
</button>
</div>
</header>
<!-- Filters -->
<section class="filters-section">
<div class="search-row">
<div class="search-input">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="searchInput" placeholder="Buscar por nome, categoria ou tags...">
</div>
</div>
<div class="filter-row">
<select class="filter-select" id="categoryFilter">
<option value="">Todas Categorias</option>
</select>
<select class="filter-select" id="typeFilter">
<option value="">Todos os Tipos</option>
</select>
<select class="filter-select" id="paidFilter">
<option value="">Todas</option>
<option value="paid">Apenas Pagas</option>
<option value="free">Apenas Gratuitas</option>
</select>
</div>
<div class="service-buttons" id="serviceButtons">
<div class="service-label">Serviços:</div>
</div>
</section>
<!-- Cards Grid -->
<div class="cards-grid" id="cardsGrid">
<!-- Cards will be rendered here -->
</div>
<!-- Empty State -->
<div class="empty-state" id="emptyState" style="display: none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<h3>Nenhuma credencial encontrada</h3>
<p>Adicione uma nova credencial ou ajuste os filtros</p>
</div>
</div>
<!-- Modal: Add/Edit -->
<div class="modal-overlay" id="modalForm">
<div class="modal">
<div class="modal-header">
<h2 id="modalTitle">Nova Credencial</h2>
<button class="btn-icon" id="closeModalForm">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<form id="credentialForm">
<input type="hidden" id="editId">
<div class="form-row">
<div class="form-group">
<label for="name">Nome *</label>
<input type="text" id="name" placeholder="Ex: Coolify - App Key" required>
</div>
<div class="form-group">
<label for="dataType">Tipo do Dado *</label>
<select id="dataType" required>
<option value="">Selecione...</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="category">Categoria *</label>
<select id="category" required>
<option value="">Selecione...</option>
</select>
</div>
<div class="form-group">
<label for="subCategory">Subcategoria</label>
<input type="text" id="subCategory" placeholder="Ex: orchestrator">
</div>
</div>
<div class="form-group full-width">
<label for="value">Valor *</label>
<textarea id="value" rows="3" placeholder="Cole o valor aqui..." required></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="url">URL</label>
<input type="url" id="url" placeholder="https://exemplo.com">
</div>
<div class="form-group">
<label for="associatedService">Serviço Associado</label>
<input type="text" id="associatedService" placeholder="Ex: Coolify">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="loginUsername">Usuário Web (para login)</label>
<input type="text" id="loginUsername" placeholder="Usuário para autenticação web">
</div>
<div class="form-group">
<label for="loginPassword">Senha Web (para login)</label>
<input type="password" id="loginPassword" placeholder="Senha para autenticação web">
</div>
</div>
<div class="form-group full-width">
<label for="notes">Notas</label>
<textarea id="notes" rows="2" placeholder="Anotações adicionais..."></textarea>
</div>
<div class="form-group full-width">
<label for="tags">Tags (separadas por vírgula)</label>
<input type="text" id="tags" placeholder="Ex: coolify, api, production">
</div>
<div class="paid-section">
<h4>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="1" x2="12" y2="23"></line>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>
Informações de Pagamento
</h4>
<div class="checkbox-group">
<input type="checkbox" id="isPaid">
<label for="isPaid">Esta é uma credencial de serviço pago</label>
</div>
<div class="paid-fields" id="paidFields">
<div class="form-row">
<div class="form-group">
<label for="cancellationDate">Data de Vencimento/Cancelamento</label>
<input type="date" id="cancellationDate">
</div>
<div class="form-group">
<label for="reminderDays">Lembrete (dias antes)</label>
<input type="number" id="reminderDays" value="7" min="1" max="30">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="cost">Custo</label>
<input type="text" id="cost" placeholder="Ex: R$ 5/mês">
</div>
<div class="form-group">
<label for="billingCycle">Ciclo de Cobrança</label>
<select id="billingCycle">
<option value="monthly">Mensal</option>
<option value="yearly">Anual</option>
<option value="pay-as-you-go">Pay-as-you-go</option>
</select>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="cancelForm">Cancelar</button>
<button type="submit" form="credentialForm" class="btn btn-primary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Salvar
</button>
</div>
</div>
</div>
<!-- Modal: Confirm Delete -->
<div class="modal-overlay" id="modalConfirm">
<div class="modal" style="max-width: 400px;">
<div class="modal-body">
<div class="confirm-dialog">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<h3>Confirmar Exclusão</h3>
<p>Tem certeza que deseja excluir esta credencial? Esta ação não pode ser desfeita.</p>
<div class="confirm-actions">
<button class="btn btn-secondary" id="cancelDelete">Cancelar</button>
<button class="btn btn-danger" id="confirmDelete">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Excluir
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<script>
// State
let data = null;
let filteredData = [];
let currentFilter = {
search: '',
category: '',
type: '',
paid: '',
service: ''
};
let deleteTargetId = null;
let hiddenValues = new Set();
// Auth
const CREDENTIALS = {
username: 'tracksteel',
password: '@@@Ke03Br;;;'
};
// DOM Elements
const loginScreen = document.getElementById('loginScreen');
const appContainer = document.getElementById('appContainer');
const loginForm = document.getElementById('loginForm');
const loginError = document.getElementById('loginError');
const cardsGrid = document.getElementById('cardsGrid');
const emptyState = document.getElementById('emptyState');
const searchInput = document.getElementById('searchInput');
const categoryFilter = document.getElementById('categoryFilter');
const typeFilter = document.getElementById('typeFilter');
const paidFilter = document.getElementById('paidFilter');
const modalForm = document.getElementById('modalForm');
const modalConfirm = document.getElementById('modalConfirm');
const credentialForm = document.getElementById('credentialForm');
const toastContainer = document.getElementById('toastContainer');
// Icons
const icons = {
key: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg>',
password: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>',
token: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>',
url: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>',
apiKey: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><line x1="19" y1="8" x2="19" y2="14"></line><line x1="22" y1="11" x2="16" y2="11"></line></svg>',
botToken: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"></path><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="m9.09 9-3 3"></path><circle cx="12" cy="12" r="1"></circle></svg>',
credentials: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>',
copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>',
eyeOff: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>',
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>',
trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>',
link: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>',
calendar: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>',
bell: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>',
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>',
alert: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>'
};
// Type icon mapping
const typeIcons = {
'key': icons.key,
'password': icons.password,
'token': icons.token,
'url': icons.link,
'chave-publica': icons.key,
'api-key': icons.apiKey,
'jwt-secret': icons.token,
'uri': icons.link,
'secret': icons.password,
'bot-token': icons.botToken,
'credentials': icons.credentials,
'username': icons.apiKey
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
setupEventListeners();
});
function checkAuth() {
const isAuthenticated = sessionStorage.getItem('vault_auth') === 'true';
if (isAuthenticated) {
showApp();
}
}
function setupEventListeners() {
loginForm.addEventListener('submit', handleLogin);
document.getElementById('logoutBtn').addEventListener('click', handleLogout);
document.getElementById('addBtn').addEventListener('click', () => openModalForm());
document.getElementById('exportBtn').addEventListener('click', handleExport);
document.getElementById('refreshBtn').addEventListener('click', loadData);
document.getElementById('closeModalForm').addEventListener('click', () => closeModal(modalForm));
document.getElementById('cancelForm').addEventListener('click', () => closeModal(modalForm));
document.getElementById('cancelDelete').addEventListener('click', () => closeModal(modalConfirm));
document.getElementById('confirmDelete').addEventListener('click', handleConfirmDelete);
credentialForm.addEventListener('submit', handleFormSubmit);
searchInput.addEventListener('input', debounce(handleSearch, 300));
categoryFilter.addEventListener('change', handleFilterChange);
typeFilter.addEventListener('change', handleFilterChange);
paidFilter.addEventListener('change', handleFilterChange);
document.getElementById('isPaid').addEventListener('change', togglePaidFields);
}
function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (username === CREDENTIALS.username && password === CREDENTIALS.password) {
sessionStorage.setItem('vault_auth', 'true');
loginError.style.display = 'none';
showApp();
} else {
loginError.style.display = 'block';
document.getElementById('password').value = '';
}
}
function handleLogout() {
sessionStorage.removeItem('vault_auth');
loginScreen.classList.remove('hidden');
appContainer.style.display = 'none';
}
function showApp() {
loginScreen.classList.add('hidden');
appContainer.style.display = 'block';
loadData();
}
// EMBEDDED DATA - Dados incluídos diretamente no HTML
const EMBEDDED_DATA = {"version": "1.0", "lastUpdated": "2026-03-20T14:30:00Z", "credentials": [{"id": "coolify-001", "name": "Coolify - App Key", "category": "infraestrutura", "subCategory": "orchestrator", "dataType": "key", "value": "base64:b47bUe7FBv1cGpzW5zSIEfTVsfXqw+wDg37p6BIOCVE=", "url": "https://coolify.reifonas.cloud", "notes": "Usada para criptografia de sessões do Coolify", "tags": ["coolify", "criptografia"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "coolify-002", "name": "Coolify - Database Password (PostgreSQL)", "category": "infraestrutura", "subCategory": "database", "dataType": "password", "value": "IbPDBCzJPeinra5tCfCZaDz9QOkIHBFofOSpnr626/s=", "url": "https://coolify.reifonas.cloud", "notes": "Senha do banco PostgreSQL interno do Coolify", "tags": ["coolify", "postgresql", "database"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "coolify-003", "name": "Coolify - Redis Password", "category": "infraestrutura", "subCategory": "cache", "dataType": "secret", "value": "kkMadIcUAkDM1oUrcY3tCC+r9Uw9RWBpt+terc5TfMU=", "url": "https://coolify.reifonas.cloud", "notes": "Senha do Redis para cache do Coolify", "tags": ["coolify", "redis", "cache"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "coolify-004", "name": "Coolify - Pusher App ID", "category": "infraestrutura", "subCategory": "websocket", "dataType": "key", "value": "9874b73ff9652e5327b5b70952929f4abdc67589802a6efc1665a2652ed0696b", "url": "https://coolify.reifonas.cloud", "notes": "App ID do Pusher para WebSocket em tempo real", "tags": ["coolify", "pusher", "websocket"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "coolify-005", "name": "Coolify - Pusher App Key", "category": "infraestrutura", "subCategory": "websocket", "dataType": "key", "value": "7a4125c71f34f83bf2ee722434a596f135f53021d72d7513b58c6e1aa1b9ed5e", "url": "https://coolify.reifonas.cloud", "notes": "App Key do Pusher para WebSocket em tempo real", "tags": ["coolify", "pusher", "websocket"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "coolify-006", "name": "Coolify - Pusher App Secret", "category": "infraestrutura", "subCategory": "websocket", "dataType": "secret", "value": "2244c9076f690153e7400179fb6fb429020a74801d61644fa29ede5ae17b7523", "url": "https://coolify.reifonas.cloud", "notes": "App Secret do Pusher para autenticação", "tags": ["coolify", "pusher", "websocket", "secret"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-001", "name": "Supabase - Dashboard Studio User", "category": "api-externa", "subCategory": "dashboard", "dataType": "username", "value": "2FfdSSoPDostr0wA", "url": "https://supabase.reifonas.cloud/dashboard", "notes": "Usuário do painel administrativo do Supabase Studio", "tags": ["supabase", "admin", "studio"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-002", "name": "Supabase - Dashboard Studio Password", "category": "api-externa", "subCategory": "dashboard", "dataType": "password", "value": "Mg0WcYG7XPXA0oWf4SOUfIFmkW5Bs5Cr", "url": "https://supabase.reifonas.cloud/dashboard", "notes": "Senha do painel administrativo do Supabase Studio", "tags": ["supabase", "admin", "studio"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-003", "name": "Supabase - Database Credentials", "category": "banco-de-dados", "subCategory": "postgresql", "dataType": "credentials", "value": "postgresql://supabase_admin:Xz0oyb6ArGYG5uAVTVwcvJxRrMuT7EIJ@localhost:5432/postgres", "url": "https://supabase.reifonas.cloud/dashboard/project/_/database/tables", "notes": "Credenciais completas do banco PostgreSQL. Porta: 5432", "tags": ["supabase", "postgresql", "database"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-004", "name": "Supabase - Service Role Key", "category": "api-externa", "subCategory": "api-keys", "dataType": "api-key", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc3Mjk5NTUwMCwiZXhwIjo0OTI4NjY5MTAwLCJyb2xlIjoic2VydmljZV9yb2xlIn0._n2Kj2f29z1u0pOYUGqAr-1Xjt-xQpK9KDhhhGvOIro", "url": "https://supabase.reifonas.cloud/dashboard/project/_/settings/api", "notes": "Chave admin para operações no servidor. NUNCA expor no frontend!", "tags": ["supabase", "api", "admin", "service-role"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-005", "name": "Supabase - Anon Key", "category": "api-externa", "subCategory": "api-keys", "dataType": "api-key", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc3Mjk5NTUwMCwiZXhwIjo0OTI4NjY5MTAwLCJyb2xlIjoiYW5vbiJ9.kOAYmQJlNd3LsssUHaNyvWZpa2sunfpLj24F_X-PRNY", "url": "https://supabase.reifonas.cloud/dashboard/project/_/settings/api", "notes": "Chave pública para uso no frontend (nível anon)", "tags": ["supabase", "api", "public", "anon"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-006", "name": "Supabase - JWT Secret", "category": "api-externa", "subCategory": "auth", "dataType": "jwt-secret", "value": "BJTT52gbXWcUeWMgGGQ90I2ei80RDCZ0", "url": "https://supabase.reifonas.cloud/dashboard/project/_/auth/providers", "notes": "Secret para validar tokens JWT do Supabase Auth", "tags": ["supabase", "auth", "jwt"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-007", "name": "Supabase - MinIO Access Key", "category": "storage", "subCategory": "s3", "dataType": "key", "value": "JcIIUSqslL5JCG3y", "url": "https://supabase.reifonas.cloud/dashboard/project/_/storage", "notes": "Access Key para MinIO (storage S3-compatible)", "tags": ["supabase", "storage", "minio", "s3"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-008", "name": "Supabase - MinIO Secret Key", "category": "storage", "subCategory": "s3", "dataType": "secret", "value": "pR8v6UUvVDcCxTitjVQkqgdaHcsQ8vCv", "url": "https://supabase.reifonas.cloud/dashboard/project/_/storage", "notes": "Secret Key para MinIO (storage S3-compatible)", "tags": ["supabase", "storage", "minio", "s3", "secret"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-009", "name": "Supabase - Vault Encryption Key", "category": "api-externa", "subCategory": "security", "dataType": "key", "value": "IgDixpOtM6uhRL2LaarNwnSrZmanUtvc", "url": "https://supabase.reifonas.cloud/dashboard/project/_/vault", "notes": "Chave de criptografia para o Vault do Supabase", "tags": ["supabase", "vault", "encryption", "security"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "supabase-010", "name": "Supabase - Logflare API Key", "category": "api-externa", "subCategory": "analytics", "dataType": "api-key", "value": "s5tbGFZV0rCoA8OZpP5Eg8rZnBlykvOr", "url": "https://supabase.reifonas.cloud/dashboard/project/_/logs", "notes": "API Key para integração com Logflare (logs analytics)", "tags": ["supabase", "logs", "analytics", "logflare"], "isPaid": false, "paidInfo": null, "associatedService": "Supabase", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "logto-001", "name": "Logto - Database Credentials", "category": "banco-de-dados", "subCategory": "postgresql", "dataType": "credentials", "value": "postgresql://bhWbMdkltdZej1RG:Szt31khElk0RczDuk0CJq2eDMsp6oo0e@localhost:5432/logto", "url": "https://logto.reifonas.cloud", "notes": "Credenciais do banco PostgreSQL do Logto. DB Name: logto", "tags": ["logto", "auth", "postgresql", "database"], "isPaid": false, "paidInfo": null, "associatedService": "Logto", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "logto-002", "name": "Logto - Admin Endpoint", "category": "autenticacao", "subCategory": "oauth", "dataType": "url", "value": "https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io", "url": "https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io", "loginUsername": "logto-admin", "loginPassword": "@@Gi05Br;;;", "notes": "Endpoint administrativo do Logto (SSLip.io temporário)", "tags": ["logto", "auth", "admin", "oauth"], "isPaid": false, "paidInfo": null, "associatedService": "Logto", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gitea-001", "name": "Gitea - Admin Credentials", "category": "git", "subCategory": "git-server", "dataType": "credentials", "value": "Usuário: admtracksteel / Marcos | Senha: @@Gi05Br;;;", "url": "https://git.reifonas.cloud", "notes": "Credenciais de admin para Gitea (usar para ambos usuários)", "tags": ["gitea", "git", "admin"], "isPaid": false, "paidInfo": null, "associatedService": "Gitea", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gitea-002", "name": "Gitea - Internal Token", "category": "git", "subCategory": "git-server", "dataType": "token", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NzMxMDg3Mjl9.beKMVnmwBwdIyBhApfihXHMxvIMc3mXjJJQ0gLuwPAo", "url": "https://git.reifonas.cloud", "notes": "Token interno para autenticação API do Gitea", "tags": ["gitea", "git", "api", "token"], "isPaid": false, "paidInfo": null, "associatedService": "Gitea", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gitea-003", "name": "Gitea - OAuth2 JWT Secret", "category": "git", "subCategory": "oauth", "dataType": "jwt-secret", "value": "hfHyav79UnnuhN0x9cVHvgedRVS6Y_xumnRW62d0Xk8", "url": "https://git.reifonas.cloud", "notes": "JWT Secret para OAuth2 do Gitea", "tags": ["gitea", "oauth", "jwt"], "isPaid": false, "paidInfo": null, "associatedService": "Gitea", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gitea-004", "name": "Gitea - LFS JWT Secret", "category": "git", "subCategory": "lfs", "dataType": "jwt-secret", "value": "gvasUkABbQ9scDHYdKRwsIEURWsbFBXUnOFD91Gf8A4", "url": "https://git.reifonas.cloud", "notes": "JWT Secret para Git LFS do Gitea", "tags": ["gitea", "lfs", "jwt"], "isPaid": false, "paidInfo": null, "associatedService": "Gitea", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "telegram-001", "name": "Telegram Bot - Bot Token", "category": "bot", "subCategory": "telegram", "dataType": "bot-token", "value": "8272877120:AAEKPhLGZPyj8XL9HGSowGLAFzXZPtXHMa4", "url": "https://t.me/AdmTrackSteelBot", "notes": "Token do bot Telegram para integração com VPS", "tags": ["telegram", "bot", "notification"], "isPaid": false, "paidInfo": null, "associatedService": "Telegram Bot", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "telegram-002", "name": "Telegram Bot - Chat ID Admin", "category": "bot", "subCategory": "telegram", "dataType": "key", "value": "8768212834", "url": "https://t.me/AdmTrackSteelBot", "notes": "Chat ID do usuário admin (Marcos) autorizado", "tags": ["telegram", "bot", "admin"], "isPaid": false, "paidInfo": null, "associatedService": "Telegram Bot", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gpi-001", "name": "GPI Local - MongoDB URI", "category": "banco-de-dados", "subCategory": "mongodb", "dataType": "uri", "value": "mongodb+srv://admtracksteel:29OHAHpKTI8XcCNt@cluster0.a4xiilu.mongodb.net/ts_gpi?retryWrites=true&w=majority&appName=Cluster0", "url": null, "notes": "URI do MongoDB Atlas para o projeto GPI local", "tags": ["gpi", "mongodb", "atlas", "database"], "isPaid": true, "paidInfo": {"cancellationDate": null, "reminderDays": 7, "cost": "Free tier (500MB)", "billingCycle": "monthly"}, "associatedService": "GPI Local", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gpi-002", "name": "GPI Local - Clerk Publishable Key", "category": "api-externa", "subCategory": "auth", "dataType": "key", "value": "pk_live_Y2xlcmsucmVpZm9uYXMuY2xvdWQk", "url": "https://dashboard.clerk.com", "notes": "Chave pública do Clerk para autenticação", "tags": ["gpi", "clerk", "auth", "frontend"], "isPaid": false, "paidInfo": null, "associatedService": "GPI Local", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gpi-003", "name": "GPI Local - JWT Secret", "category": "api-externa", "subCategory": "auth", "dataType": "jwt-secret", "value": "kd6wmHgnOk5xLy2ybtUiGPaPtVEwv/L4m3vFkIvSYrE=", "url": "https://dashboard.clerk.com", "notes": "JWT Secret para validar tokens localmente", "tags": ["gpi", "jwt", "auth"], "isPaid": false, "paidInfo": null, "associatedService": "GPI Local", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gpi-004", "name": "GPI Local - Database Local (Postgres)", "category": "banco-de-dados", "subCategory": "postgresql", "dataType": "uri", "value": "postgresql://postgres:postgres@localhost:5432/pintura_db?schema=public", "url": null, "notes": "URI do banco PostgreSQL local para desenvolvimento", "tags": ["gpi", "postgresql", "local", "development"], "isPaid": false, "paidInfo": null, "associatedService": "GPI Local", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "anthropic-001", "name": "Anthropic - API Key", "category": "api-externa", "subCategory": "llm", "dataType": "api-key", "value": "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", "url": "https://console.anthropic.com", "notes": "API Key para Claude API. Cuidado: valor muito longo!", "tags": ["anthropic", "claude", "llm", "api"], "isPaid": true, "paidInfo": {"cancellationDate": null, "reminderDays": 7, "cost": "Pay-as-you-go", "billingCycle": "monthly"}, "associatedService": "Anthropic", "lastRotated": null, "createdAt": "2026-03-20", "updatedAt": "2026-03-20"}, {"id": "ssh-001", "name": "SSH - Public Key", "category": "ssh", "subCategory": "authentication", "dataType": "chave-publica", "value": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILXuodcZ2QF/nmNZaBBiSZIF3t1kU8F7oDI4BgSBibDZ admtracksteel@gmail.com", "url": null, "notes": "Chave pública SSH para autenticação em servidores. Usuário: root", "tags": ["ssh", "ed25519", "authentication", "server"], "isPaid": false, "paidInfo": null, "associatedService": "SSH", "lastRotated": null, "createdAt": "2026-03-18", "updatedAt": "2026-03-20"}, {"id": "gitea-token-001", "name": "Token Gitea Coolify", "category": "deploy", "subCategory": "ci-cd", "dataType": "token", "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NDU...", "url": "https://git.reifonas.cloud", "notes": "Token para integração do Coolify com Gitea", "tags": ["coolify", "gitea", "deploy", "token"], "isPaid": false, "paidInfo": null, "associatedService": "Coolify-Gitea", "lastRotated": null, "createdAt": "2026-03-10", "updatedAt": "2026-03-10"}], "metadata": {"categories": {"destino": ["infraestrutura", "banco-de-dados", "api-externa", "deploy", "ssh", "autenticacao", "storage", "git", "bot"], "tipo-dado": ["key", "token", "senha", "url", "chave-publica", "api-key", "jwt-secret", "uri", "secret", "password", "bot-token", "credentials", "username"]}}};
async function loadData() {
// Usar dados inline ao invés de carregar do arquivo (para funcionar offline/file://)
data = EMBEDDED_DATA;
populateFilters();
renderCards();
}
function populateFilters() {
const categories = [...new Set(data.credentials.map(c => c.category))];
const types = [...new Set(data.credentials.map(c => c.dataType))];
categoryFilter.innerHTML = '<option value="">Todas Categorias</option>' +
categories.map(c => `<option value="${c}">${formatLabel(c)}</option>`).join('');
typeFilter.innerHTML = '<option value="">Todos os Tipos</option>' +
types.map(t => `<option value="${t}">${formatLabel(t)}</option>`).join('');
const dataTypeSelect = document.getElementById('dataType');
dataTypeSelect.innerHTML = '<option value="">Selecione...</option>' +
data.metadata.categories['tipo-dado'].map(t => `<option value="${t}">${formatLabel(t)}</option>`).join('');
const categorySelect = document.getElementById('category');
categorySelect.innerHTML = '<option value="">Selecione...</option>' +
data.metadata.categories.destino.map(c => `<option value="${c}">${formatLabel(c)}</option>`).join('');
populateServiceButtons();
}
function populateServiceButtons() {
const services = [...new Set(data.credentials.map(c => c.associatedService).filter(s => s))];
const serviceButtons = document.getElementById('serviceButtons');
const buttonsHtml = services.map(service => {
const count = data.credentials.filter(c => c.associatedService === service).length;
return `<button class="service-btn" data-service="${service}"><span class="dot"></span>${service} (${count})</button>`;
}).join('');
serviceButtons.innerHTML = `<div class="service-label">Serviços:</div>${buttonsHtml}`;
serviceButtons.querySelectorAll('.service-btn').forEach(btn => {
btn.addEventListener('click', () => {
const service = btn.dataset.service;
if (currentFilter.service === service) {
currentFilter.service = '';
btn.classList.remove('active');
} else {
document.querySelectorAll('.service-btn').forEach(b => b.classList.remove('active'));
currentFilter.service = service;
btn.classList.add('active');
}
renderCards();
});
});
}
function renderCards() {
applyFilters();
updateStats();
if (filteredData.length === 0) {
cardsGrid.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
cardsGrid.innerHTML = filteredData.map((cred, index) => createCardHTML(cred, index)).join('');
document.querySelectorAll('.card').forEach(card => {
const id = card.dataset.id;
card.querySelector('.copy-btn').addEventListener('click', () => copyToClipboard(id));
card.querySelector('.toggle-btn').addEventListener('click', () => toggleValue(id));
card.querySelector('.edit-btn').addEventListener('click', () => openModalForm(id));
card.querySelector('.delete-btn').addEventListener('click', () => openConfirmDelete(id));
});
}
function createCardHTML(cred, index) {
const icon = typeIcons[cred.dataType] || icons.key;
const isHidden = hiddenValues.has(cred.id);
const displayValue = isHidden ? '••••••••••••••••••••••••' : cred.value;
const isExpiringSoon = checkExpiringSoon(cred);
let expirationHtml = '';
if (cred.isPaid && cred.paidInfo && cred.paidInfo.cancellationDate) {
const reminderDate = new Date(cred.paidInfo.cancellationDate);
reminderDate.setDate(reminderDate.getDate() - (cred.paidInfo.reminderDays || 7));
expirationHtml = `
<div class="card-meta-item">
${icons.calendar}
<span>Vencimento: ${formatDate(cred.paidInfo.cancellationDate)}</span>
</div>
${isExpiringSoon ? `
<div class="card-meta-item" style="color: var(--accent-orange);">
${icons.bell}
<span>Lembrete em ${getDaysUntil(reminderDate)} dias</span>
</div>
` : ''}
`;
}
let loginInfoHtml = '';
if (cred.loginUsername || cred.loginPassword) {
const isLoginHidden = hiddenValues.has(cred.id + '_login');
const displayUser = isLoginHidden ? '••••••••' : (cred.loginUsername || '-');
const displayPass = isLoginHidden ? '••••••••' : (cred.loginPassword || '-');
loginInfoHtml = `
<div class="card-login-info">
${cred.loginUsername ? `
<div class="card-login-field">
<div class="card-login-field-label">Usuário Web</div>
<div class="card-login-field-content">
<span class="card-login-field-text ${isLoginHidden ? 'masked' : ''}">${displayUser}</span>
<button class="card-login-copy" onclick="copyToClipboard('${cred.id}', 'loginUsername')" title="Copiar usuário">
${icons.copy}
</button>
</div>
</div>
` : ''}
${cred.loginPassword ? `
<div class="card-login-field">
<div class="card-login-field-label">Senha Web</div>
<div class="card-login-field-content">
<span class="card-login-field-text ${isLoginHidden ? 'masked' : ''}">${displayPass}</span>
<button class="card-login-copy" onclick="copyToClipboard('${cred.id}', 'loginPassword')" title="Copiar senha">
${icons.copy}
</button>
</div>
</div>
` : ''}
</div>
`;
}
return `
<div class="card" data-id="${cred.id}" style="animation-delay: ${index * 50}ms">
<div class="card-header">
<div class="card-title">
<div class="card-icon">${icon}</div>
<h3>${cred.name}</h3>
</div>
<div class="card-actions">
<button class="btn-icon copy-btn" title="Copiar">
${icons.copy}
</button>
<button class="btn-icon toggle-btn" title="${isHidden ? 'Mostrar' : 'Ocultar'}">
${isHidden ? icons.eye : icons.eyeOff}
</button>
<button class="btn-icon edit-btn" title="Editar">
${icons.edit}
</button>
<button class="btn-icon delete-btn" title="Excluir">
${icons.trash}
</button>
</div>
</div>
<div class="card-badges">
<span class="badge badge-type">${formatLabel(cred.dataType)}</span>
<span class="badge badge-category">${formatLabel(cred.category)}</span>
${cred.isPaid ? '<span class="badge badge-paid">💳 Paga</span>' : '<span class="badge badge-category">Grátis</span>'}
${isExpiringSoon ? '<span class="badge badge-expiring">⚠️ Expirando</span>' : ''}
</div>
<div class="card-value">
<div class="card-value-label">Valor</div>
<div class="card-value-content">
<span class="card-value-text ${isHidden ? 'masked' : ''}">${displayValue}</span>
</div>
</div>
<div class="card-meta">
${cred.url ? `
<div class="card-meta-item">
${icons.link}
<a href="${cred.url}" target="_blank" rel="noopener noreferrer">${cred.url}</a>
</div>
` : ''}
${cred.associatedService ? `
<div class="card-meta-item">
${icons.credentials}
<span>Serviço: ${cred.associatedService}</span>
</div>
` : ''}
${expirationHtml}
${cred.notes ? `
<div class="card-meta-item" style="flex-direction: column; align-items: flex-start;">
<span style="font-size: 0.75rem; color: var(--text-muted);">Notas:</span>
<span>${cred.notes}</span>
</div>
` : ''}
${loginInfoHtml}
</div>
</div>
`;
}
function applyFilters() {
filteredData = data.credentials.filter(cred => {
const searchMatch = currentFilter.search === '' ||
cred.name.toLowerCase().includes(currentFilter.search.toLowerCase()) ||
cred.category.toLowerCase().includes(currentFilter.search.toLowerCase()) ||
cred.tags?.some(tag => tag.toLowerCase().includes(currentFilter.search.toLowerCase())) ||
cred.notes?.toLowerCase().includes(currentFilter.search.toLowerCase());
const categoryMatch = currentFilter.category === '' || cred.category === currentFilter.category;
const typeMatch = currentFilter.type === '' || cred.dataType === currentFilter.type;
const paidMatch = currentFilter.paid === '' ||
(currentFilter.paid === 'paid' && cred.isPaid) ||
(currentFilter.paid === 'free' && !cred.isPaid);
const serviceMatch = currentFilter.service === '' || cred.associatedService === currentFilter.service;
return searchMatch && categoryMatch && typeMatch && paidMatch && serviceMatch;
});
}
function updateStats() {
document.getElementById('totalCount').textContent = data.credentials.length;
document.getElementById('paidCount').textContent = data.credentials.filter(c => c.isPaid).length;
document.getElementById('expiringCount').textContent = data.credentials.filter(c => checkExpiringSoon(c)).length;
}
function checkExpiringSoon(cred) {
if (!cred.isPaid || !cred.paidInfo || !cred.paidInfo.cancellationDate) return false;
const reminderDate = new Date(cred.paidInfo.cancellationDate);
reminderDate.setDate(reminderDate.getDate() - (cred.paidInfo.reminderDays || 7));
const today = new Date();
const diffDays = Math.ceil((reminderDate - today) / (1000 * 60 * 60 * 24));
return diffDays <= 7 && diffDays >= 0;
}
function handleSearch(e) {
currentFilter.search = e.target.value;
renderCards();
}
function handleFilterChange() {
currentFilter.category = categoryFilter.value;
currentFilter.type = typeFilter.value;
currentFilter.paid = paidFilter.value;
renderCards();
}
async function copyToClipboard(id, field = null) {
const cred = data.credentials.find(c => c.id === id);
if (!cred) return;
try {
let textToCopy = cred.value;
let label = cred.name;
if (field === 'loginUsername') {
textToCopy = cred.loginUsername;
label = `${cred.name} - Usuário`;
} else if (field === 'loginPassword') {
textToCopy = cred.loginPassword;
label = `${cred.name} - Senha`;
}
await navigator.clipboard.writeText(textToCopy);
showToast(`Copiado: ${label}`, 'success');
} catch (error) {
showToast('Erro ao copiar', 'error');
}
}
function toggleValue(id) {
if (hiddenValues.has(id)) {
hiddenValues.delete(id);
} else {
hiddenValues.add(id);
}
renderCards();
}
function openModalForm(id = null) {
credentialForm.reset();
document.getElementById('paidFields').classList.remove('show');
document.getElementById('isPaid').checked = false;
if (id) {
const cred = data.credentials.find(c => c.id === id);
if (cred) {
document.getElementById('editId').value = id;
document.getElementById('modalTitle').textContent = 'Editar Credencial';
document.getElementById('name').value = cred.name;
document.getElementById('dataType').value = cred.dataType;
document.getElementById('category').value = cred.category;
document.getElementById('subCategory').value = cred.subCategory || '';
document.getElementById('value').value = cred.value;
document.getElementById('url').value = cred.url || '';
document.getElementById('associatedService').value = cred.associatedService || '';
document.getElementById('loginUsername').value = cred.loginUsername || '';
document.getElementById('loginPassword').value = cred.loginPassword || '';
document.getElementById('notes').value = cred.notes || '';
document.getElementById('tags').value = cred.tags?.join(', ') || '';
if (cred.isPaid) {
document.getElementById('isPaid').checked = true;
document.getElementById('paidFields').classList.add('show');
document.getElementById('cancellationDate').value = cred.paidInfo?.cancellationDate || '';
document.getElementById('reminderDays').value = cred.paidInfo?.reminderDays || 7;
document.getElementById('cost').value = cred.paidInfo?.cost || '';
document.getElementById('billingCycle').value = cred.paidInfo?.billingCycle || 'monthly';
}
}
} else {
document.getElementById('editId').value = '';
document.getElementById('modalTitle').textContent = 'Nova Credencial';
}
openModal(modalForm);
}
function togglePaidFields() {
const isPaid = document.getElementById('isPaid').checked;
document.getElementById('paidFields').classList.toggle('show', isPaid);
}
async function handleFormSubmit(e) {
e.preventDefault();
const editId = document.getElementById('editId').value;
const isPaid = document.getElementById('isPaid').checked;
const loginUsername = document.getElementById('loginUsername').value || null;
const loginPassword = document.getElementById('loginPassword').value || null;
const credData = {
name: document.getElementById('name').value,
dataType: document.getElementById('dataType').value,
category: document.getElementById('category').value,
subCategory: document.getElementById('subCategory').value || null,
value: document.getElementById('value').value,
url: document.getElementById('url').value || null,
associatedService: document.getElementById('associatedService').value || null,
loginUsername: loginUsername,
loginPassword: loginPassword,
notes: document.getElementById('notes').value || null,
tags: document.getElementById('tags').value ?
document.getElementById('tags').value.split(',').map(t => t.trim()).filter(t => t) : [],
isPaid: isPaid,
paidInfo: isPaid ? {
cancellationDate: document.getElementById('cancellationDate').value || null,
reminderDays: parseInt(document.getElementById('reminderDays').value) || 7,
cost: document.getElementById('cost').value || null,
billingCycle: document.getElementById('billingCycle').value
} : null,
updatedAt: new Date().toISOString().split('T')[0]
};
if (editId) {
const index = data.credentials.findIndex(c => c.id === editId);
if (index !== -1) {
credData.createdAt = data.credentials[index].createdAt;
data.credentials[index] = { ...data.credentials[index], ...credData };
}
} else {
credData.id = generateId();
credData.createdAt = new Date().toISOString().split('T')[0];
data.credentials.push(credData);
}
data.lastUpdated = new Date().toISOString();
try {
await saveData();
closeModal(modalForm);
renderCards();
showToast(editId ? 'Credencial atualizada!' : 'Credencial criada!', 'success');
} catch (error) {
showToast('Erro ao salvar dados', 'error');
}
}
function openConfirmDelete(id) {
deleteTargetId = id;
openModal(modalConfirm);
}
async function handleConfirmDelete() {
if (!deleteTargetId) return;
const index = data.credentials.findIndex(c => c.id === deleteTargetId);
if (index !== -1) {
data.credentials.splice(index, 1);
data.lastUpdated = new Date().toISOString();
try {
await saveData();
closeModal(modalConfirm);
hiddenValues.delete(deleteTargetId);
renderCards();
showToast('Credencial excluída!', 'success');
} catch (error) {
showToast('Erro ao excluir', 'error');
}
}
deleteTargetId = null;
}
async function saveData() {
// Atualizar dados inline na memória
Object.assign(EMBEDDED_DATA, data);
showToast('Dados atualizados na memória (exportar para backup)', 'success');
}
function handleExport() {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `keys-backup-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast('Backup exportado!', 'success');
}
function openModal(modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal(modal) {
modal.classList.remove('active');
document.body.style.overflow = '';
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
${type === 'success' ? icons.check : icons.alert}
<span>${message}</span>
`;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastIn 0.3s ease reverse';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
function formatLabel(str) {
if (!str) return '';
return str.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('pt-BR');
}
function getDaysUntil(date) {
const today = new Date();
return Math.ceil((date - today) / (1000 * 60 * 60 * 24));
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
</script>
</body>
</html>